反射
什么是反射
反射本质上是.Net Framework 提供的一个帮助类库。
先从C#代码执行原理上了解一下反射。
如上图,通过编写的代码(高级语言,人类能够看懂),在VS内置的编译器中进行编译操作(首次编译),从而产生DLL(类库生成DLL)或执行文件;如果需要打开首次编译的文件,是需要一个依赖环境,即,CLR(安装.Net Framework时,自动配置的环境,里面包含了一些基本类库等)+ JIT(即时编译器);然后,执行文件在JIT的编译(二次编译)下,最终形成机器语言。
为什么需要二次编译呢?是希望能够在不同的平台使用。
CLR就像一个“适配器”,会根据不同的平台产生不同版本CLR,这里简单提一下,网上有很多资料可供查阅。
编译器如同一个“中间层”,像语法糖、泛型等都是源自于此。那么编译器生成的DLL和执行文件(.EXE后缀)本质是一样的;里面都包含两大内容:Metadata(元数据)和IL(中间语言)。
IL是一种面向对象的语言;Metadata是一个描述性的清单数据,里面包含了当前DLL或执行文件的具体“描述”,比如里面有哪些方法、属性、字段等等;统称为元数据。反射就是用来读取和使用Metadata的。
反射创建对象
1. 反射的基本使用步骤
加载程序集的三种方法
1
2
3
4
5
6//DLL名称 当前目录加载 (推荐使用该方法)
Assembly assemblyLoad = Assembly.Load("DB.MySql");
//完整目录加载 所以可以指定其他目录
Assembly assemblyLoadFile = Assembly.LoadFile(@"E:\Study\MyReflection\MyReflection\bin\Debug\DB.MySql.dll");
//DLL名称(带后缀)或者完整路径
Assembly assemblyLoadFrom = Assembly.LoadFrom("DB.MySql.dll");获取类型
1
2//获取类型
Type type = assemblyLoad.GetType("DB.MySql.MySqlHelper");创建程序集对象(创建时调用相关构造函数)
- 创建时调用相关构造函数
- 与通过new关键字创建的对象相同
- 当前产生的对象还无法调用方法等,需要转换;是因为编译器不认可。
1
2//创建对象 调用相关程序集的构造函数
object oMySql = Activator.CreateInstance(type);类型转换并调用方法
1
2
3//类型转化并调用其方法
MySqlHelper mySqlHelper = (MySqlHelper)oMySql;
mySqlHelper.Query();步骤总结
2. 反射封装(反射+简单工厂+配置文件)
工厂就是用来创建对象的
App.config配置
1
2
3<appSettings>
<add key="IDBHelperConfig" value="DB.MySql,DB.MySql.MySqlHelper"/>
</appSettings>创建工厂
工厂使用时,静态字段在第一次调用方法、属性或实例化之前,就已经完成了初始化;注意异常的处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30using DB.Interface;
using System;
using System.Configuration;
using System.Reflection;
namespace MyReflection
{
public class Factory
{
//获取配置文件信息
private static string dBHelperInfo = ConfigurationManager.AppSettings["IDBHelperConfig"];
//获取Load字符串
private static string loadInfo = dBHelperInfo.Split(',')[0];
//获取类型字符串
private static string typeInfo = dBHelperInfo.Split(',')[1];
/// <summary>
/// 创建对象
/// </summary>
/// <returns></returns>
public static IDBHelper CreateHelper()
{
Assembly assemblyLoad = Assembly.Load(loadInfo);
Type type = assemblyLoad.GetType(typeInfo);
object oIDBHelper = Activator.CreateInstance(type);
return (IDBHelper)oIDBHelper;
}
}
}调用封装
1
2
3Console.WriteLine("******** Reflection + 简单工厂 + 配置文件 ******");
IDBHelper dBHelper = Factory.CreateHelper();
dBHelper.Query();
截止当前,发现反射很麻烦,多步操作才能操作对象;即便通过封装,也看到实际效果。
接下来的操作,就会眼前一亮,也会慢慢体会到反射的强大。
完成封装并对代码重新生成,在程序集(DLL)目录下,如图,打开配置文件,并在其中直接更改相关配置信息;
随着配置文件更改,输出结果在没有更改代码的前提下,也达到预期效果;这种情况称为程序的可配置。
如果将其他项目的程序集复制过来(例如OracleHelper),通过程序的可配置,可实现同样的效果;这种情况称为程序的可配置可扩展。
之所以能够实现程序的可配置可拓展,就是因为反射是动态的,依赖的是字符串。也是反射的核心特点。
概括来讲,这就是IOC;IOC容器就是通过反射+配置文件+工厂实现的。
3. 破坏单例
所谓破坏单例就是可以调用私有函数。因为在常规的单例模式中,私有函数是无法进行调用,从而也无法实例化的。通过下面的例子具体了解一下反射是如何破坏单例。
1 | using System; |
创建单例模式后,并对其调用;这里主要是说明一下CreateInstance(type, true)
的第二个参数,为true时,可调用私有函数。
1 | Console.WriteLine("******************** Reflection 破坏单例 **********************"); |
4. 映射参数类型不同构造函数的对象
同预想中的一样,映射默认是调用无参构造函数。
如果想调用有参构造函数,则需对
CreateInstance()
方法继续深挖。
1 |
创建测试类并调用,如上图,方法的第二个参数传入不同类型的对象数组,则调用不同类型构造函数。
5. 映射泛型对象
1 |
1 | Console.WriteLine("******************** Reflection 泛型 **********************"); |
如上,创建测试类并调用;要注意以下内容:
- 通过反射获取泛型类型时,字符串中务必写明占位符(具体详见上篇的《泛型》);如缺失,则获取不到泛型的类型信息。
- 类型信息需两次设定(泛型方法的调用,则继续往下看)。
反射调用方法
通过反射调用方法最具体的例子就是体现在MVC上;通过MVC的路由路径,底层架构便获取到类和方法。
继续在ReflectionTest类中创建几个测试方法,并调用。
1 |
|
1 | Console.WriteLine("******************** Reflection 方法操作 **********************"); |
从调用中能够看出,不同方法的调用主要是掌握GetMethod()
和Invoke()
方法;具体不再赘述,结合例子和方法API便能很好的掌握。
泛型方法的调用比较有趣,继续往下看。
1 | /// <summary> |
1 | Console.WriteLine("******************** Reflection 泛型方法调用 **********************"); |
反射泛型对象,并调用泛型方法;需再次确定泛型方法的参数类型。
反射获取、设置字段和属性
关于属性和字段的操作,更多是在ORM中使用。
首先创建一个People类。
1 | using System; |
通常会将此类作为数据的载体,实例化并赋值,如下:
1 | Console.WriteLine("******************** Reflection 属性和字段 **********************"); |
相关的API较多,这里仅描述常用的几个,例如SetValue()、GetValue(),以及获取属性和字段集合的方法GetProperties()、GetFields()。通过反射对实体进行操作:
1 | var type = typeof(People); |
当前,似乎没有看出反射在属性和字段上带来的便利;通过普通实例化和设置更为优越。
实际上,反射在属性和字段上实践主要用于以下两个场景。
属性和字段的遍历;通常的实例对属性或字段的遍历,是直接写死的,比如上面的例子提到的
Console.WriteLine($"People.Id = {people.Id}")
;Id属性是直接写死在代码中的。而通过反射,无论实体属性(字段)如何增减,都会通过相应方法动态反馈出来。DB实体赋值给展示实体;以往一般都会这么做:
1
2
3
4
5
6
7
8
9
10
11
12
13
14//DB实体
var people = new People
{
Id = 1,
Name = "Rock",
Notes = "People类"
};
//展示实体
var peopleDto = new PeopleDTO
{
Id = people.Id,
Name = people.Name,
Notes = people.Notes
};此方式通常叫做硬编码。同样是“写死”了,后期维护同样需要改动代码。而通过反射就得以解决。
先呈现“基本思路”的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14var peopleType = typeof(People);
var peopleDtoType = typeof(PeopleDTO);
var peopleDtoTypeObj = Activator.CreateInstance(peopleDtoType);
foreach (var prop in peopleDtoType.GetProperties())
{
if (prop.Name.Equals("Id"))
{
prop.SetValue(peopleDtoTypeObj, peopleType.GetProperty("Id").GetValue(people));
}
else if (prop.Name.Equals("Name"))
{
prop.SetValue(peopleDtoTypeObj, peopleType.GetProperty("Name").GetValue(people));
}
}优化后的代码:
1
2
3
4
5
6
7var peopleType = typeof(People);
var peopleDtoType = typeof(PeopleDTO);
var peopleDtoTypeObj = Activator.CreateInstance(peopleDtoType);
foreach (var prop in peopleDtoType.GetProperties())
{
prop.SetValue(peopleDtoTypeObj, peopleType.GetProperty(prop.Name)?.GetValue(people));
}?. 是C# 6.0引进的运算符;如果对象为NULL,则不进行后面的获取成员的运算,直接返回NULL
从反射优化的代码能够看出其重要特性:动态。
细心的小伙伴也发现了这种方式的弊端,如果两个实体属性命名不同(一个是Name,另一个是UserName)就会发生异常。这个通过“特性”可以完美解决(下篇讲解的特性会讲解)。
通过泛型,可以将该方法进一步优化,从而实现不同实体之间数据的传递。
反射的优点和局限
优点:动态
这个优点凸显出实际意义;C#是面向对象的开发语言;而面向对象自然会将功能进行封装,等待调用即可;封装后,就代表着代码的“稳定”;如多处调用后,再行改动的话,便与“面向对象”相违背,也导致程序不稳定。反射拥有可配置可拓展的特点,很好的诠释了这一点。
另外,框架的搭建和反射有着密不可分的。
局限:
编写复杂
避开编译器的检查
我想这是反射最主要的软肋;编译器在编写代码的过程中是不可或缺的。当然细心也是程序员的职业素养,也不是难事^_^
性能问题
大家众说纷纭,大致都认为消耗性能。
首先,实际代码中不会像泛型一样常用,基本是用在底层或封装方法中,性能上几乎可以忽略不计。
其次,当前硬件设备性能都有目共睹,完全可以不用考虑性能上疑虑。
最后,像EF、WebAPI等,首次访问都会比较慢,其后就很快;相比较反射,就是小巫见大巫了。近年等.Net Core普及后,更可宽心使用。
发布时间: 2020-02-05
最后更新: 2020-02-09
本文标题: 反射
本文链接: http://www.selectcode.cn/2020/02/05/Reflection/
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可。转载请注明出处!