无处不在的反射

先贴一张网上的图:


image.png

介绍几个概念:

  1. ILspy:逆向工程工具:可以把Dll/Exe文件反编译回C#代码;
  2. IL:是对标于C#代码的代码,不太好阅读
  3. metadata:是一个清单数据,只是记录有什么,而不是展示所有的实现;(明细账本)
    比如像下面这个熟悉的List类,只是列出了所有的方法供调用,并没有把这些方法的所有实现全都写在里面;


    image.png
  4. 反射是System.Reflection命名空间,可以读取metadata,并使用metadata;是微软提供的一个帮助类库;

正题

举例项目的目录结构


image.png
  1. 当我们在写程序的时候,通常创建对象,调用对象方法,会使用如下方式
IDBHelper dBHelper = new SqlServerHelper();
dBHelper.Query();

这种方式,就要在开头有一段这样的引用将涉及到的类的命名空间引用进来

using XXXX.AspNetCore.DB.SqlServer;
  1. 如果通过反射的方式,要实现想上面那种创建对象,调用方法,怎么做呢?
    我们分步骤来:
    首先,应该像上面那样引用对应的命名空间
//1.using System.Reflection  有下列几种引入方式,自行选择
Assembly assembly = Assembly.Load("Xxxx.AspNetCore.DB.SqlServer"); //Dll名称,不需要后缀
//Assembly assembly1 = Assembly.LoadFrom(@"这里写的是目录中SqlServerHelper类所在类库编译后的dll文件的全路径"); //全路径 从头写到尾
//Assembly assembly3 = Assembly.LoadFrom(@"Xxxx.AspNetCore.DB.SqlServer.dll"); //dll名称(需要后缀) 
//Assembly assembly2 = Assembly.LoadFile(@"同样是全路径"); //从头写到尾 全路径

其次,引入命名空间后,我们就可以获取类型

Type type = assembly.GetType("Xxxx.AspNetCore.DB.SqlServer.SqlServerHelper");

继续,通过类型创建对象

object obj = Activator.CreateInstance(type);

然后,调用方法,下图是SqlServerHelper类存在的方法


image.png

有下面几种方法调用的方式

obj.Query(); //行不通,不能Query,因为类型是Object声明的,在C# 语言中;变量的声明是编译时决定;因为是Object,所以不能调用;
dynamic dObject= Activator.CreateInstance(type);
dObject.Get();
dObject.Show();
//动态类型,dynamic 是运行是决定;可以避开编译器的检查(编写代码时不会报错),但是如果通过dynamic声明的对象去动态调用类里不存在的方法,在程序运行时就会抛异常。

上面创建对象和调用方法可以通过类型转换优化一下

IDBHelper dBHelper=(IDBHelper)obj //如果实际类型不一样,会报异常
IDBHelper dBHelper = obj as IDBHelper;
dBHelper.Query();

上面两种创建对象调用方法 分别是普通方式和用反射的方式实现,用反射的方式感觉代码量增加了,怎么解决?没错,封装一下 0.0, 可以封装成下面的样子

public class SimpleFactory
{      
        public static IDBHelper CreateInstance()
        {
            string ReflictionConfig = CustomConfigManager.GetConfig("ReflictionConfig");
            string tyepName= ReflictionConfig.Split(",")[0];
            string dllName = ReflictionConfig.Split(",")[1];

            //Assembly assembly = Assembly.Load(dllName); //Dll名称,不需要后缀 
            Assembly assembly3 = Assembly.LoadFrom(dllName); //dll名称(需要后缀) 
            
            Type type = assembly3.GetType(tyepName); 
            object obj = Activator.CreateInstance(type);  
            return obj as IDBHelper;
        } 
    }

    public static class CustomConfigManager
    {
        //Core 读取配置文件:appsettings
        //1.Microsoft.Extensions.Configuration;
        //2.Microsoft.Extensions.Configuration.Json 
        public static string GetConfig(string key)
        {
            //默认读取  当前运行目录
            var builder = new ConfigurationBuilder().AddJsonFile("appsettings.json");  
            IConfigurationRoot configuration = builder.Build();
            string configValue = configuration.GetSection(key).Value;
            return configValue;
        }
    }
}

温馨提示:在.net core 中 Assembly.Load 和 Assembly.LoadFrom 建议使用后者,前者使用时如果没有提前做一些处理,会出现找不到文件的异常
封装完以后,我们就可以很方便的调用代码啦,如下:

IDBHelper dBHelper=  SimpleFactory.CreateInstance();
dBHelper.Query();

细心的读者会发现,我在封装反射的时候,在调用Assembly.LoadFrom()方法这一块,本来参数是写死的DLL文件,最后改成了通过程序的配置文件的Key 获取到DLL文件这个Value的方式
这是我项目的配置文件

{
  "exclude": [
    "**/bin",
    "**/bower_components",
    "**/jspm_packages",
    "**/node_modules",
    "**/obj",
    "**/platforms"
  ],
  "ReflictionConfig": "Xxxxx.AspNetCore.DB.SqlServer,Zhaoxi.AspNetCore.DB.SqlServer"
}

这样做的好处是什么呢?答案可想而知,假设现在公司用的是SqlServer 数据库,但是突然某一天换了个技术经理,要求将公司的SqlServer 数据库改成Mysql 数据库,用传统的不是反射的方法,我们的做法是将 IDBHelper dBHelper = new SqlServerHelper();这段代码改成
IDBHelper dBHelper = new MySqlHelper(); 然后再重新编译运行发布。但是通过上面封装反射,并且通过读取配置文件的形式,并不需要修改代码,需要做的就是在程序外部 Copy Dll文件(将mysql程序集的dll文件拷贝到bin目录下)


image.png

修改配置文件,把数据库的版本给更换了
将 "ReflictionConfig": "Xxxxx.AspNetCore.DB.SqlServer,Zhaoxi.AspNetCore.DB.SqlServer" 改成
"ReflictionConfig": "Xxxxx.AspNetCore.DB.MySql.MySqlHelper,Zhaoxi.AspNetCore.DB.MySql.dll"
这样做实现了程序的可配置;程序的可扩展; 使程序更好的解耦:去掉对细节的依赖。

反射能不能做到一些普通方式做不到的事儿呢? 可以

传统的单例模式

    {
        private static Singleton _Singleton = null;
        /// <summary>
        /// 创建对象的时候执行
        /// </summary>
        private Singleton()
        {
            Console.WriteLine("Singleton被构造");
        }

        /// <summary>
        /// 被CLR 调用 整个进程中 执行且只执行一次
        /// </summary>
        static Singleton()
        {
            _Singleton = new Singleton();
        }

        public static Singleton GetInstance()
        {
            return _Singleton;
        }
    }

测试单例模式

Singleton singleton = Singleton.GetInstance();
Singleton singleton1 = Singleton.GetInstance();
Singleton singleton2 = Singleton.GetInstance();
Console.WriteLine(object.ReferenceEquals(singleton, singleton1)); 
Console.WriteLine(object.ReferenceEquals(singleton, singleton2));
Console.WriteLine(object.ReferenceEquals(singleton1, singleton2));
//ReferenceEquals,如果是同一个引用,true  否则 false

运行的结果是输出三个 true,因为静态方法在程序运行的时候只能在最开始的时候执行一次,同时这个单例模式中无参构造器用private来修饰,意味着在外部无法通过new的方法来创建Singleton对象,所以即使调用了三次GetInstance(),得出来的三个对象其实地址是一样的(同一个对象);然后利用反射的方式,反射可以突破方法的权限限制,如下:

Assembly assembly = Assembly.LoadFrom("Xxxxx.AspNetCore.DB.SqlServer.dll"); //dll名称(需要后缀)
Type type = assembly.GetType("Xxxxx.AspNetCore.DB.SqlServer.Singleton");
Singleton singleton = (Singleton)Activator.CreateInstance(type, true);
Singleton singleton1 = (Singleton)Activator.CreateInstance(type, true);
Singleton singleton2 = (Singleton)Activator.CreateInstance(type, true);
Console.WriteLine(object.ReferenceEquals(singleton, singleton1)); 
Console.WriteLine(object.ReferenceEquals(singleton, singleton2));
Console.WriteLine(object.ReferenceEquals(singleton1, singleton2));

通过在Singleton的无参构造打断点的方式来调试,发现当用反射的方法来创建类的对象时,即使无参构造器是用private修饰的,仍然可以每次都进入无参构造,结果就是输出三个false,三个对象地址都不一样。

反射几乎可以实现所以想要的功能,但是反射难道就没有局限吗?

虽然反射很强大,但是同时也存在着一些性能上的问题;下面举一个测试用例

public class Monitor
    {
        public static void Show()
        {
            Console.WriteLine("*******************Monitor*******************");
            long commonTime = 0;
            long reflectionTime = 0;
            {
                Stopwatch watch = new Stopwatch();
                watch.Start();
                for (int i = 0; i < 1000_000; i++) //1000000000
                {
                    IDBHelper iDBHelper = new SqlServerHelper();
                    iDBHelper.Query();
                }
                watch.Stop();
                commonTime = watch.ElapsedMilliseconds;
            }
            {
                Stopwatch watch = new Stopwatch();
                watch.Start();
                //Assembly assembly = Assembly.Load("Xxxx.AspNetCore.DB.SqlServer");//1 动态加载
                //Type dbHelperType = assembly.GetType("Xxxx.AspNetCore.DB.SqlServer.SqlServerHelper");//2 获取类型 消耗性能
                //缓存的使用;
                for (int i = 0; i < 1000_000; i++)
                {
                    Assembly assembly = Assembly.Load("Xxxx.AspNetCore.DB.SqlServer");//1 动态加载
                    Type dbHelperType = assembly.GetType("Xxxx.AspNetCore.DB.SqlServer.SqlServerHelper");//2 获取类型
                    object oDBHelper = Activator.CreateInstance(dbHelperType);//3 创建对象
                    IDBHelper dbHelper = (IDBHelper)oDBHelper;//4 接口强制转换
                    dbHelper.Query();//5 方法调用
                }
                watch.Stop();
                reflectionTime = watch.ElapsedMilliseconds;
            }

            Console.WriteLine($"commonTime={commonTime} reflectionTime={reflectionTime}");
        }
    }

上面代码实现的功能简单来说就是分别测试用普通的new 来创建对象和用反射来创建对象在分别创建100万次对象并调用方法的情况下所花费的时间。


image.png

很明显的差距。。。足以证明反射的性能相对来说是比较低的。
所以,在使用反射的时候,要有一种扬长避短的原则,尽量发挥反射最大的用处,至于性能上的不足,我们可以在编写代码的时候,尽量的去优化代码,"正确"的使用反射,比如上面利用反射创建100万次对象的代码,我们其实可以把1.动态加载和2.创建类型放到for循环外面,只需要进行一次操作就行,这样改进后结果如下:


image.png

性能得到了很大的优化,我又分别让两种方式都执行了一亿次。。。。。如下,
image.png

稍微总结一下,经过优化以后,其实反射性能并没有想象的那么差;所以需要理性选择;
我觉得反射没有不该用的地方,应该正确使用;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容