详谈C#反射及应用

x33g5p2x  于2021-10-16 转载在 C#  
字(8.3k)|赞(0)|评价(0)|浏览(537)

在学习本篇博客前,建议学习了解下这篇博客:C/# 与 .NET
在学习反射之前需要了解几个概念,什么是反射?什么是元数据?什么是 Type? 什么是程序集?

什么是反射?什么是元数据?

程序是用来处理数据的,而程序本身(类的定义和 BLC 中的类)也是数据,有关程序及其类型的数据被称为 Metadata ,保存在程序集中。程序在运行时,可以查看其他程序集或其本身 Metadata,一个运行的程序查看本身的元数据或其他程序集的元数据的行为称为反射。

什么是Type?

对于程序中用到的每个类型,CLR 都会创建一个包含这个类型信息的 Type 对象。程序中每用到一个类型都会关联到独立的 Type 类的对象。不管创建的类型有多少个实例,只有一个 Type 对象会关联到这些所有的实例。

什么是程序集?

程序集是一个可以寄宿于 CLR 中的、拥有版本号的、自解释、可配置的二进制文件,程序集的扩展名为 exe 或 dll。程序集是存放类型的集合,通过程序集可以获取程序集内所有的类型信息。

反射、DLL、IL、Metadata、Assembly

C/# 编写的源代码被编译成符合 CLI 规范的中间语言 (IL)。 IL 代码和资源(如位图和字符串)存储在扩展名通常为 .dll or .exe 的程序集中。程序集包含一个介绍程序集的类型、版本和区域性的清单,这些描述性信息称为 Metadata,而反射就需要在 Assembly 中找到这些 Metadata 然后进行对象实例化、调用方法、读取属性等。

反射 API

一、程序集

注意:加载 dll 没有错,如果没有加载依赖,调用到使用依赖的对象时就会报错

1.1、Load

XXX:dll 名称无后缀 从当前目录加载 dll。开发环境 Bin ,发布程序是入口程序集文件当前目录。

Assembly assembly = Assembly.Load("XXXX");

1.2、LoadFrom

XXX:已知程序集的文件名或路径,加载程序集,并且会自动加载其依赖程序集。

Assembly assembly = Assembly.LoadFrom("XXXX");

1.3、LoadFile

XXX:完整 dll 路径加载不会错,不会加载目标程序集所引用和依赖的其他程序集,需要自己控制并显示加载所有依赖的程序集,如果没有依赖项,使用的时候会错。

Assembly assembly = Assembly.LoadFile("XXXX");

二、类型实例

2.1、无参构造

在获取了 Assembly 可以获取 Type,并且根 Type 创建实例,创建的实例是 object 类型,为了编译器通过需进行类型转换 (XXX)

Type type = assembly.GetType("XXX.XXX");
object obj = (XXX)Activator.CreateInstance(type);

2.2、有参构造

有参构造函数在使用 Type 创建实例时,通过 new object[] 参数承载,此方法会根据 new object[] 里面的类型自动进行匹配构造函数

object obj = (XXX)Activator.CreateInstance(type, new object[] { "124", 123 });

2.3、私有构造(与单例的火花)

单例模式都知道,在代码代码里面为了避免外面进行实例化,在实现时都是以 private 修饰符对构造函数进行修饰,使其无法在外部进行实例化进行调用,但反射可以破坏这个规则。重点在 CreateInstance 方法,第二个参数 true

Assembly assembly = Assembly.LoadFrom("XXXX");
Type type = assembly.GetType("XXX.XXX");
object obj = Activator.CreateInstance(type,true);

2.4、泛型类

~n 占位符,代表几个泛型,MakeGenericType 定义泛型类型,传入 Type 数组即可

Assembly assembly = Assembly.LoadFrom("XXXX");
Type type = assembly.GetType("XXX~n");
Type typeNew = type.MakeGenericType(new Type[] { typeof(int), typeof(XXX),typeof(string)  });
object obj = Activator.CreateInstance(typeNew, new object[] { "124", 123 });

三、方法调用

3.1、普通方法

Assembly assembly = Assembly.Load("XXX");// dll
Type type = assembly.GetType("XXX");// 类型名称
object instance = Activator.CreateInstance(type); // 根据 type 实例对象
MethodInfo method = type.GetMethod("XXX");// 方法名称
method.Invoke(instance,new object[] { "方法参数1", "方法参数2" });

3.2、静态方法

静态成员,Invoke 无徐传入实例对象,因为静态成员在类里面,只有一份,确定了 Type 后就已经有了其静态成员

Assembly assembly = Assembly.Load("XXX");// dll
Type type = assembly.GetType("XXX");// 类型名称
MethodInfo method = type.GetMethod("XXX");// 方法名称
method.Invoke(null,new object[] { "方法参数1", "方法参数2" });

3.3、私有方法

众所周知,私有方法在面向对象编程语言是不可以被外部调用的,但反射可以破坏这个规则调用私有方法,重点在 GetMethod 方法第二个参数为 BindingFlags.Instance|BindingFlags.NonPublic

Assembly assembly = Assembly.Load("XXX");// dll
Type type = assembly.GetType("XXX");// 类型名称
object instance = Activator.CreateInstance(type); // 根据 type 实例对象
MethodInfo method = type.GetMethod("XXX",BindingFlags.Instance|BindingFlags.NonPublic);// 方法名称
method.Invoke(instance, new object[] { "方法参数1", "方法参数2" });

3.4、泛型方法

无论是泛型类还是泛型方法,都需要通过 MakeGenericXXXX 方法指定泛型的类型,需要注意的是泛型类在加载类型时需要占位符,而泛型方法不需要

Assembly assembly = Assembly.Load("XXX");// dll
Type type = assembly.GetType("XXX~2");// 类型名称,~2 占位符,代表几个泛型
Type typeNew = type.MakeGenericType(new Type[] { typeof(string), typeof(int) });
object instance = Activator.CreateInstance(typeNew); // 根据 type 实例对象
MethodInfo method = type.GetMethod("XXX");// 方法名称
MethodInfo methodNew = method.MakeGenericMethod(new Type[] { typeof(int) });
methodNew.Invoke(instance, new object[] { "方法参数1", "方法参数2" });

3.5、应用

学到这有的同学肯定会有疑问,这样做有意义吗,我们平时在程序里面直接 new 调用其方法他不香吗?答案是:有意义的,接着向下看

其实在我刚工作的的时候也有这样的疑问,想必大家知道 MVC ,我们在浏览器输入 IP:[控制器]/[action] 就会访问到对于后台的业务代码。

但是,他是如何匹配到我们的控制器与 action 呢?这里用的就是反射,在程序启动时,会扫描 controller 类型的类,会将其 Type 缓存起来,当有请求过来时,就会到缓存中找到对于的 Type 反射进行实例化并调用其方法(也就是 action)。说到这 mvc 的 filter 也就是在 调用方法前后加点料(反射 invoke 方法前后)。

四、属性与字段

4.1、属性读写

Assembly assembly = Assembly.Load("XXX");// dll
Type type = assembly.GetType("XXX.XXX");// 类型名称
object instance = Activator.CreateInstance(type); //实例对象
foreach (var prop in type.GetProperties())
{
    if (prop.Name.Equals("Id"))
    {
        prop.SetValue(instance, 1);
        Console.WriteLine(prop.GetValue(instance));
    }
    else if (prop.Name.Equals("Name"))
    {
        prop.SetValue(instance, "张三");
        Console.WriteLine(prop.GetValue(instance));
    }
}

4.2、字段读写

Assembly assembly = Assembly.Load("XXX");// dll
Type type = assembly.GetType("XXX.XXX");// 类型名称
object instance = Activator.CreateInstance(type); //实例对象
foreach (var field in type.GetFields())
{
    if (field.Name.Equals("id"))
    {
        field.SetValue(instance, 1);
        Console.WriteLine(field.GetValue(instance));
    }
    else if (field.Name.Equals("name"))
    {
        field.SetValue(instance, "张三");
        Console.WriteLine(field.GetValue(instance));
    }
}

4.3、应用

学到这可能又有同学会疑问,属性字段读写值有什么意义吗?答案是:当然有,接着看

这里有个 entity 与 entityDto,知道 DDD 的同学应该清楚是做什么的

public class Product
{
    public int ID { get; set; }
    public String Name { get; set; }
}
public class ProductDto
{
    public int ID { get; set; }
    public String Name { get; set; }
}

下面对 entity 承载的数据进行了实例化,并且依次赋值个了 entityDto,这种是常规写法。如果字段多了呢,还需要手动依次赋值吗?

Product product = new Product()
{
    ID = 1,
    Name = "张三"
};

ProductDto productDto = new ProductDto();
productDto.ID = product.ID;
productDto.Name = product.Name;

对于 entity entityDto 产生的问题,答案是否定的。我们可以使用反射,对 entity entityDto 之间进行自动赋值,提高开发效率。

实际应用时,会使用 T 进行封装,这里只是简单,写下核心代码,.Net 也有非常成熟的框架 AutoMapper。

Product product = new Product()
{
    ID = 1,
    Name = "张三"
};
Type productType = typeof(Product); // Product Type
Type productDtoType = typeof(ProductDto);// ProductDto Type
object productDto = Activator.CreateInstance(productDtoType); // ProductDto Instance
foreach (var prop in productDtoType.GetProperties())
{
    // 依次拿取 dto 属性名称,在 Product Type 查找,并且从 Product Instance 获取值
    object val = productType.GetProperty(prop.Name).GetValue(product);
    // ProductDto Instance Set Propertie Val
    prop.SetValue(productDto,val);
}

性能测试

这个也是大家关心的问题,网上大多数说性能不好,这有点片面,面向对象都是静态的,反射是动态的所以会有一点点性能上的开销。

接下来我们进行测试,我们分别对类进行实例化,并且调用对象的方法,这里1千万次进行分析,请看下面代码

1 . 这里我们定义一个类,并且定义一个方法

namespace ConsoleApp2
{
    public class Product
    {
        public int ID { get; set; }
        public String Name { get; set; } 

        public void PrintMsg()
        {

        }
    }
}

2 . 分表以静态编码与反射两种形式,对类进行实例化,并且调用方法

static void Main(string[] args)
{
    Stopwatch stopwatchT = new Stopwatch();
    stopwatchT.Start();
    for (int i = 0; i < 10000000; i++)
    {
        Product product = new Product();
        product.PrintMsg();
    }
    stopwatchT.Stop();
    var tTime = stopwatchT.ElapsedMilliseconds;

    Stopwatch stopwatchR = new Stopwatch();
    stopwatchR.Start();
    for (int i = 0; i < 10000000; i++)
    {
        Assembly assembly = Assembly.Load("ConsoleApp2");// dll
        Type type = assembly.GetType("ConsoleApp2.Product");// 类型名称
        Product instance = (Product)Activator.CreateInstance(type); //实例对象
        instance.PrintMsg();
    }
    stopwatchR.Stop();
    var rTime = stopwatchR.ElapsedMilliseconds;

    Console.WriteLine($"静态:{tTime};反射:{rTime}");
}

3 . 启动,可以看到静态编码耗时 135ms ,反射方式耗时 32660ms。可以看出静态编码比反射效率高约 4409 倍。

静态编码,我们看到1千万才耗时 135ms,每个才耗时 0.0000135ms,这个与平时说 new 对象耗时有点吃惊。其实只要 new 对象不做一些业务操作或者占用大量内存,就可以放心 new 对象。

反射方式,可以看到他的效率确实远低于静态编码反射,每个耗时约 0.0032ms,这个绝对值很小很小,对程序的影响已经微乎其微,所以反射的效率虽低于静态编码但对于平时对程序的影响可以说忽略不计。

4 . 其实我们的程序有些问题,就是 Assembly 与 Type 的加载是重复的操作。我们可以以空间换时间,可以将程Assembly Type 放到缓存中,如下优化后的程序

static void Main(string[] args)
{
    Stopwatch stopwatchT = new Stopwatch();
    stopwatchT.Start();
    for (int i = 0; i < 10000000; i++)
    {
        Product product = new Product();
        product.PrintMsg();
    }
    stopwatchT.Stop();
    var tTime = stopwatchT.ElapsedMilliseconds;

    Stopwatch stopwatchR = new Stopwatch();
    stopwatchR.Start();
    Assembly assembly = Assembly.Load("ConsoleApp2");// dll
    Type type = assembly.GetType("ConsoleApp2.Product");// 类型名称
    for (int i = 0; i < 10000000; i++)
    {
        Product instance = (Product)Activator.CreateInstance(type); //实例对象
        instance.PrintMsg();
    }
    stopwatchR.Stop();
    var rTime = stopwatchR.ElapsedMilliseconds;

    Console.WriteLine($"静态:{tTime};反射:{rTime}");
}

5 . 启动,可以看到此时反射的耗时,降为了 588 毫秒,比代码优化前快了约 55 倍,比静态编码只慢了 5 倍

此时反射方式的耗时,绝对值已经很小很小很小,接近静态编码耗时,这对对象性能的损耗几乎可以忽略。说到这并不是说推崇反射,而是客观的看待这个反射。

有的同学可能会钻牛角尖,说性能就是低于静态编码,那我劝就不要搞开发了,平时 mvc、IOC、orm 等是在大量使用反射,都与反射相关,你品你细品。

如果说是调用 几亿次,那可能影响到了程序性能,那时候再优化,其实平时开发 95% 程序性能问题都不会是用为反射问题,所以还是客观看待这个问题。

优缺点

上面讲了类、方法、属性、字段相关反射的应用,有人疑问硬编码方式比反射好吗?答案是:真的就比反射好,

优点
动态:反射就两个字动态,就像 MVC 就是将方法的调用动态化了。数据库或者封装的处理业务逻辑的算法等,可以使用配置文件进行动态切换使用。

缺点
coding 复杂:从教程里也能体会到,这个平时工作中用的话大家肯定深有体会,面向对象静态编码,一两行的代码反射得写个四五行。

避开编译器检查:平时写代码,在各种 IDE 里面,我们写错了,编译的时候会 error 提示我们,如果没有编译器写得代码不知道错多少。但在反射里面,编译器对类的操作不会进行检查,这也是没有办法检查,应为反射是动态的运行时的,不像普通编码是静态的。

反射的价值是什么

反射最直观的区别是,由已有的固定类型,转化为了字符串操作,且不需要在项目中进行引用(反射是动态的,依赖的是字符串)。

应为依赖的是字符串,我们的程序才可配置化、才可易扩展,包括平时的框架开发都在大量使用反射(MVC、IOC、ORM等)。

只是平时业务逻辑开发时非常少的使用,这也需要在平时的工作经验中一点一点的体会,这篇就讲到这里啦。

相关文章