什么是反射
反射本质上是.Net Framework 提供的一个帮助类库。
先从C#代码执行原理上了解一下反射。
如上图,通过编写的代码(高级语言,人类能够看懂),在VS内置的编译器中进行编译操作(首次编译),从而产生DLL(类库生成DLL)或执行文件;如果需要打开首次编译的文件,是需要一个依赖环境,即,CLR(安装.Net Framework时,自动配置的环境,里面包含了一些基本类库等)+ JIT(即时编译器);然后,执行文件在JIT的编译(二次编译)下,最终形成机器语言。
为什么需要二次编译呢?是希望能够在不同的平台使用。
CLR就像一个“适配器”,会根据不同的平台产生不同版本CLR,这里简单提一下,网上有很多资料可供查阅。
编译器如同一个“中间层”,像语法糖、泛型等都是源自于此。那么编译器生成的DLL和执行文件(.EXE后缀)本质是一样的;里面都包含两大内容:Metadata(元数据)和IL(中间语言)。
IL是一种面向对象的语言;Metadata是一个描述性的清单数据,里面包含了当前DLL或执行文件的具体“描述”,比如里面有哪些方法、属性、字段等等;统称为元数据。反射就是用来读取和使用Metadata的。
反射创建对象
1. 反射的基本使用步骤
-
加载程序集的三种方法
//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");
-
获取类型
//获取类型 Type type = assemblyLoad.GetType("DB.MySql.MySqlHelper");
-
创建程序集对象(创建时调用相关构造函数)
- 创建时调用相关构造函数
- 与通过new关键字创建的对象相同
- 当前产生的对象还无法调用方法等,需要转换;是因为编译器不认可。
//创建对象 调用相关程序集的构造函数 object oMySql = Activator.CreateInstance(type);
-
类型转换并调用方法
//类型转化并调用其方法 MySqlHelper mySqlHelper = (MySqlHelper)oMySql; mySqlHelper.Query();
-
步骤总结
2. 反射封装(反射+简单工厂+配置文件)
工厂就是用来创建对象的
-
App.config配置
<appSettings> <add key="IDBHelperConfig" value="DB.MySql,DB.MySql.MySqlHelper"/> </appSettings>
-
创建工厂
工厂使用时,静态字段在第一次调用方法、属性或实例化之前,就已经完成了初始化;注意异常的处理。
using 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; } } }
-
调用封装
Console.WriteLine("******** Reflection + 简单工厂 + 配置文件 ******"); IDBHelper dBHelper = Factory.CreateHelper(); dBHelper.Query();
截止当前,发现反射很麻烦,多步操作才能操作对象;即便通过封装,也看到实际效果。
接下来的操作,就会眼前一亮,也会慢慢体会到反射的强大。
完成封装并对代码重新生成,在程序集(DLL)目录下,如图,打开配置文件,并在其中直接更改相关配置信息;
随着配置文件更改,输出结果在没有更改代码的前提下,也达到预期效果;这种情况称为程序的可配置。
如果将其他项目的程序集复制过来(例如OracleHelper),通过程序的可配置,可实现同样的效果;这种情况称为程序的可配置可扩展。
之所以能够实现程序的可配置可拓展,就是因为反射是动态的,依赖的是字符串。也是反射的核心特点。
概括来讲,这就是IOC;IOC容器就是通过反射+配置文件+工厂实现的。
3. 破坏单例
所谓破坏单例就是可以调用私有函数。因为在常规的单例模式中,私有函数是无法进行调用,从而也无法实例化的。通过下面的例子具体了解一下反射是如何破坏单例。
using System;
namespace DB.SqlServer
{
/// <summary>
/// 单例模式
/// </summary>
public sealed class Singleton
{
private static Singleton _singleton = null;
private Singleton()
{
Console.WriteLine("Singleton的私有函数被构造");
}
static Singleton()
{
_singleton = new Singleton();
}
public static Singleton GetInstance()
{
return _singleton;
}
}
}
创建单例模式后,并对其调用;这里主要是说明一下CreateInstance(type, true)
的第二个参数,为true时,可调用私有函数。
Console.WriteLine("******************** Reflection 破坏单例 **********************");
Assembly assembly = Assembly.Load("DB.SqlServer");
Type type = assembly.GetType("DB.SqlServer.Singleton");
Singleton singleton = (Singleton)Activator.CreateInstance(type, true);
4. 映射参数类型不同构造函数的对象
同预想中的一样,映射默认是调用无参构造函数。
如果想调用有参构造函数,则需对
CreateInstance()
方法继续深挖。
创建测试类并调用,如上图,方法的第二个参数传入不同类型的对象数组,则调用不同类型构造函数。
5. 映射泛型对象
Console.WriteLine("******************** Reflection 泛型 **********************");
Assembly assembly = Assembly.Load("DB.SqlServer");
//务必写明泛型的占位符
Type type = assembly.GetType("DB.SqlServer.GenericTest`3");
//在类型信息的基础上,设定数组类型
Type newType = type.MakeGenericType(new Type[] { typeof(int), typeof(string), typeof(int) });
object genericTest = Activator.CreateInstance(newType);
如上,创建测试类并调用;要注意以下内容:
- 通过反射获取泛型类型时,字符串中务必写明占位符(具体详见上篇的《泛型》);如缺失,则获取不到泛型的类型信息。
- 类型信息需两次设定(泛型方法的调用,则继续往下看)。
反射调用方法
通过反射调用方法最具体的例子就是体现在MVC上;通过MVC的路由路径,底层架构便获取到类和方法。
继续在ReflectionTest类中创建几个测试方法,并调用。
#region Method
//无参方法
public void Show1()
{
Console.WriteLine($"{GetType()}的无参方法Show1");
}
//有参方法
public void Show2(int id)
{
Console.WriteLine($"{GetType()}的入参为整数的方法Show2");
}
//重载方法
public void Show3(int id, string str)
{
Console.WriteLine($"{GetType()}的重载方法Show3,入参为整数、字符串");
}
public void Show3(string str, int id)
{
Console.WriteLine($"{GetType()}的重载方法Show3,入参为字符串、整数");
}
//私有方法
private void Show4(string str)
{
Console.WriteLine($"{GetType()}的私有方法Show4");
}
//静态方法
public static void Show5(int id)
{
Console.WriteLine($"{typeof(ReflectionTest)}的私有方法Show5");
}
#endregion
Console.WriteLine("******************** Reflection 方法操作 **********************");
Assembly assembly = Assembly.Load("DB.SqlServer");
Type type = assembly.GetType("DB.SqlServer.ReflectionTest");
object oTest = Activator.CreateInstance(type);
//无参方法
{
MethodInfo show1Method = type.GetMethod("Show1");
show1Method.Invoke(oTest, new object[] { });
}
//有参方法
{
MethodInfo show2Method = type.GetMethod("Show2");
show2Method.Invoke(oTest, new object[] { 123 });
}
//重载方法1
{
MethodInfo show3Method = type.GetMethod("Show3", new Type[] { typeof(int), typeof(string) });
show3Method.Invoke(oTest, new object[] { 111, "重载方法" });
}
//重载方法2
{
MethodInfo showMethod = type.GetMethod("Show3", new Type[] { typeof(string), typeof(int) });
showMethod.Invoke(oTest, new object[] { "重载方法", 111 });
}
//私有方法
{
MethodInfo privateMethod = type.GetMethod("Show4", BindingFlags.Instance | BindingFlags.NonPublic);
privateMethod.Invoke(oTest, new object[] { "私有方法" });
}
从调用中能够看出,不同方法的调用主要是掌握GetMethod()
和Invoke()
方法;具体不再赘述,结合例子和方法API便能很好的掌握。
泛型方法的调用比较有趣,继续往下看。
/// <summary>
/// 泛型类
/// </summary>
/// <typeparam name="T"></typeparam>
public class GenericMethod<T>
{
public void Show<X, Y>(T t, X x, Y y)
{
Console.WriteLine($"t.type={t.GetType().Name}, x.type={x.GetType().Name}, y.type={y.GetType().Name}");
}
}
Console.WriteLine("******************** Reflection 泛型方法调用 **********************");
Assembly assembly = Assembly.Load("DB.SqlServer");
Type type = assembly.GetType("DB.SqlServer.GenericMethod`1");
Type newType = type.MakeGenericType(new Type[] { typeof(int) });
object oTest = Activator.CreateInstance(newType);
//根据类型获取方法
MethodInfo methodInfo = newType.GetMethod("Show");
//制定泛型方法参数类型
MethodInfo newMethodInfo = methodInfo.MakeGenericMethod(new Type[] { typeof(string), typeof(int) });
//方法调用
newMethodInfo.Invoke(oTest, new object[] { 123, "泛型方法", 123 });
反射泛型对象,并调用泛型方法;需再次确定泛型方法的参数类型。
反射获取、设置字段和属性
关于属性和字段的操作,更多是在ORM中使用。
首先创建一个People类。
using System;
namespace DB.Model
{
public class People
{
/// <summary>
/// 无参构造函数
/// </summary>
public People()
{
Console.WriteLine($"{GetType().FullName}被创建");
}
/// <summary>
/// ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 备注
/// </summary>
public string Notes;
}
}
通常会将此类作为数据的载体,实例化并赋值,如下:
Console.WriteLine("******************** Reflection 属性和字段 **********************");
var people = new People
{
Id = 1,
Name = "Rock",
Notes = "People类"
};
Console.WriteLine($"People.Id = {people.Id}");
Console.WriteLine($"People.Name = {people.Name}");
Console.WriteLine($"People.Notes = {people.Notes}");
相关的API较多,这里仅描述常用的几个,例如SetValue()、GetValue(),以及获取属性和字段集合的方法GetProperties()、GetFields()。通过反射对实体进行操作:
var type = typeof(People);
var peopleObj = Activator.CreateInstance(type);
foreach (var prop in type.GetProperties())
{
if (prop.Name.Equals("Id"))
{
prop.SetValue(peopleObj, 2);
}
else if (prop.Name.Equals("Name"))
{
prop.SetValue(peopleObj, "David");
}
Console.WriteLine($"{type.Name}.{prop.Name} = {prop.GetValue(peopleObj)}");
}
foreach (var field in type.GetFields())
{
if (field.Name.Equals("Notes"))
{
field.SetValue(peopleObj, "备注内容");
}
Console.WriteLine($"{type.Name}.{field.Name} = {field.GetValue(peopleObj)}");
}
当前,似乎没有看出反射在属性和字段上带来的便利;通过普通实例化和设置更为优越。
实际上,反射在属性和字段上实践主要用于以下两个场景。
-
属性和字段的遍历;通常的实例对属性或字段的遍历,是直接写死的,比如上面的例子提到的
Console.WriteLine($"People.Id = {people.Id}")
;Id属性是直接写死在代码中的。而通过反射,无论实体属性(字段)如何增减,都会通过相应方法动态反馈出来。 -
DB实体赋值给展示实体;以往一般都会这么做:
//DB实体 var people = new People { Id = 1, Name = "Rock", Notes = "People类" }; //展示实体 var peopleDto = new PeopleDTO { Id = people.Id, Name = people.Name, Notes = people.Notes };
此方式通常叫做硬编码。同样是“写死”了,后期维护同样需要改动代码。而通过反射就得以解决。
先呈现“基本思路”的代码:
var 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)); } }
优化后的代码:
var 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普及后,更可宽心使用。