反射(Reflection)
概念:
提到反射(Reflection),首先要提到元数据(metadata).
(官方文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.type?view=netframework-4.7.2)
一.什么是元数据(metadata):
在C#图解教程中,有着不错的解释.
我们用程序来读,写,操作和显示数据。这些数据包括但不限于文字,图形,声音,模型,动画这些,为了这些目的,我们要在程序中创建和使用一些类型,因此,在设计的时候,我们必须要理解所使用的类型的特征!
有关程序及其类型的数据,称为元数据(metadata),元数据保存在程序集中。(描述数据类型的数据)
在运行的过程中,查看本身的元数据或是其它程序集的元数据的“行为”,称为反射 (Reflection)
如:
对象浏览器就是显示元数据的一个示例,可以读取程序集,并显示出所包含类型及其特性和成员。
每一种类型都有自己的特性和成员。如预定义类型(int,short,float,double等),BCL库中的类型以及用户自定义的类型.
.Net中,BCL声明一个叫做Type的抽象来,用来包含类型的特性。
使用该对象,能够让我们获取到程序使用的类型信息。
有关Type类的重要事项如下:
1.程序中使用到的每一个类型,CLR都会创建一个包含这个类型信息的Type类型对象。即
每一个类型,都有一个对象的Type对象,该对象保存了类型的信息。
2.程序中使用到的每一个类型,都会关联到这个Type对象中。
3.不管创建的对象有多少个实例,只有一个Type对象会关联到这些实例。
例如:
(官方的代码)
using System;
public class Example
{
public static void Main()
{
long number1 = 1635429;
int number2 = 16203;
double number3 = 1639.41;
long number4 = 193685412;
// Get the type of number1.
Type t = number1.GetType();
// Compare types of all objects with number1.
Console.WriteLine("Type of number1 and number2 are equal: {0}",
Object.ReferenceEquals(t, number2.GetType()));
Console.WriteLine("Type of number1 and number3 are equal: {0}",
Object.ReferenceEquals(t, number3.GetType()));
Console.WriteLine("Type of number1 and number4 are equal: {0}",
Object.ReferenceEquals(t, number4.GetType()));
}
}
// The example displays the following output:
// Type of number1 and number2 are equal: False
// Type of number1 and number3 are equal: False
// Type of number1 and number4 are equal: True
%WXUF~($MDU3UX9A`HOU6BD.png
(截图摘自”C#图解教程)
MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();
OtherClass oc = new OtherClass();
程序中使用的每一个类型,CLR都会创建一个保存了该类型信息的Type对象,所以如上图的两个Type对象,分别包含了MyClass和OtherClass类型的信息
并且同一个类型的多个实例,只有一个Type对象会关联到这些实例。Mc1,mc2均是MyClass的实例,会关联到MyClass对应的Type对象上。
二.在Type对象中我们可以获取到哪些类型信息?
Name 返回类名的名字
Namespace 返回包含类型声明的命名空间
GetFields 返回类型的字段列表
GetProperties 返回类型的属性列表
GetMethods 获取类型的方法列表
所以你通过反射就可以获取你想要知道的类的所有信息了。
三.如何获取Type对象?
1.object类型包含了一个GetType()方法,返回对实例的Type对象的引用,由于每个类型都是继承object,所以我们可以在任何的对象上使用GetType方法,获取类型信息。
例:
class BaseClass{
public int BaseField = 0;
}
class DerivedClass:BaseClass{
public int DerivedField = 0;
}
创建一个基类BaseClass和派生类DerivedClass
那么CLR会为两个类型创建保存该类型信息的Type对象
var bc = new BaseClass ();
var dc = new DerivedClass ();
BaseClass[] bca = { bc, dc };
foreach (var v in bca) {
Type t = v.GetType ();
Debug.Log("Object Type:"+t.Name);
FieldInfo[] fieldInfo = t.GetFields ();
foreach (var f in fieldInfo) {
Debug.Log ("Field:" + f.Name);
}
}
2.通过运算符typeof(class)获取Type对象信息
Type t = typeof(DerivedClass);
FieldInfo[] fieldInfo = t.GetFields ();
foreach (var f in fieldInfo) {
Debug.Log ("Field:" + f.Name);
}
3.通过Type的静态方法,通过传入类名的字符串
Type t1 = Type.GetType ("DerivedClass");//通过字符串的形式
FieldInfo[] fieldInfo1 = t1.GetFields ();
foreach (var f in fieldInfo1) {
Debug.Log ("Field:" + f.Name);
}
注:GetType传递的类字符串,要带上命名空间
四、常用API解释:
1.什么是成员Memeber,类中所有的字段Field,属性Property,方法Method,事件Event,都属于成员Member.
获取成员需要类MethodInfo,直接继承自object.
MemberInfo派生出如下类:
System.Reflection.EventInfo
System.Reflection.FieldInfo
System.Reflection.MethodBase
System.Reflection.PropertyInfo
System.Reflection.TypeInfo
1.如何获取所有的MemberInfo?
看下面的类:
namespace nsperson
{
public class Person{
public string name;//Field
protected int age;//protected Field
private bool married;//private Field
private float deposit;//private Field
private string _plan;//private field
public string plan {
set {
_plan = value;
}
get {
if (plan == "") {
throw new ArgumentNullException ("you must make some concrete plans");
}
return _plan;
}
}//Property
public delegate void DelegateSomeAction(object obj);//NestedType
public DelegateSomeAction DelRoutine;//Field
public event EventHandler Elapsed;//Event
public Person()
{
name = "xiaoming";
}
public Person(string name)
{
this.name = name;
}
public Person(string name,int age)
{
this.name = name;
this.age = age;
}
public Person(string name,int age,bool married,float deposit)
{
this.name =name;
this.age = age;
this.married = married;
this.deposit = deposit;
Elapsed+=(source,args)=>{
Debug.Log("111111");
};
}
public void IncreaseDeposit(float val)
{
deposit += val;
}
public void DecreaseDeposit(float val)
{
deposit -= val;
}
public void AskPrivacy(bool access)
{
Debug.Log(access?(String.Format ("my name is {0},I am {1} years old,{2},my deposit is {3}", name, age, MarriedState(), deposit))
:"sorry,this is my privacy!");
}
private string MarriedState()
{
return married ? "I was married" : "I am not married";
}
}
}
在命名空间nsperson下,定义了Person类,包括了name,age,married,deposit几个字段Field,分别是公有,保护,私有
下面还定义了属性(Property)plan,事件Event,三个构造函数及其它一些方法Method.
获取MemberInfo的代码如下:
Type t = typeof(nsperson.Person);
MemberInfo[] members = t.GetMembers ();
foreach (var m in members) {
Debug.Log (m.ToString()+"[type]="+m.MemberType);
}
会输出所有的public的成员Member.
2.如何输出非public,如protected,private的Member呢?
GetMembers的重载函数之一,接受BindingFlags枚举参数,传入BindingFlags.NonPublic|BindingFlags.Instance即可。
*注意:BindingFlags.Instance是指定获取实例成员,另一种是BindingFlags.Static指定获取静态成员,必须使用两者中的一个,不能只写NonPublic,默认值是BindingFlags.Public|BindingFlags.Instance.
Type t = typeof(nsperson.Person);
MemberInfo[] members = t.GetMembers (BindingFlags.NonPublic|BindingFlags.Instance);
foreach (var m in members) {
Debug.Log (m.ToString()+"[type]="+m.MemberType);
}
这样就会输出类中所有的实例NoPublic成员,如:
protected int age;//protected Field
private bool married;//private Field
private float deposit;//private Field
private string MarriedState()
{
return married ? "I was married" : "I am not married";
}
3.如何通过MemberInfo创建类的实例?
Type t = typeof(nsperson.Person);
object obj = t.InvokeMember (null, BindingFlags.DeclaredOnly |
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.CreateInstance, null, null,null);
Debug.Log (obj.GetType ().ToString ());
nsperson.Person person = obj as nsperson.Person;
person.AskPrivacy(true);
重要参数:
BindingFlags.CreateInstance
指定反射来创建Type的实例,通过给定的参数,来搜索匹配的构造函数,忽略name(所以传null是可以的)
最后一个参数是用于搜索匹配构造函数的条件,传null,调用的默认无参构造函数。
如果我想调用下面的重载构造函数:
public Person(string name,int age,bool married,float deposit)
{
this.name =name;
this.age = age;
this.married = married;
this.deposit = deposit;
Elapsed+=(source,args)=>{
Debug.Log("xxxxxxx");
};
}
我就需要传递三个参数,在这里要创建一个临时的object数组,如下:
object obj = t.InvokeMember (null, BindingFlags.DeclaredOnly |
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.CreateInstance,null,null,new object[]{"Cristiano Ronaldo",33,true,100});
//new object[]{"Cristiano Ronaldo",33,true,100} 在object数组中依次传入匹配的参数即可
4.创建类的实例以后,如何读写字段Field?
方法是类似的,同样使用InvokeMember,区别也在于BindingFlags.
//write deposit field
t.InvokeMember ("deposit", BindingFlags.DeclaredOnly |
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.SetField,null,obj,new object[]{200});
关键参数:BindingFlags.SetField
为指定的字段赋值
这里要注意:因为deposit是私有成员,所以BindingFlags必须要|BindingFlags.NonPublic
下面是读取字段:
//read deposit field
object v = t.InvokeMember ("deposit", BindingFlags.DeclaredOnly |
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.GetField,null,obj,null);
Debug.Log ("deposit:" + v);
关键参数:BindingFlags.GetField
获取指定的字段
5.创建类的实例后,如何调用方法Method?
比如我要调用公共方法AskPrivacy和私有方法MarriedState
//call public void AskPrivacy
t.InvokeMember("AskPrivacy",
BindingFlags.DeclaredOnly |
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.InvokeMethod, null, obj, new object[]{true});
//call private MarriedState and return string
string marriedState =(string)t.InvokeMember("MarriedState",
BindingFlags.DeclaredOnly |
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.InvokeMethod, null, obj, null);
Debug.Log ("marriedState:" + marriedState);
关键参数:InvokeMethod
调用指定的方法Method
6.创建类的实例后,如何读写属性Property?
首先,尝试将属性设置为一个""值,这样在获取的时候,会抛出参数NULL异常
//set property
t.InvokeMember("plan",
BindingFlags.DeclaredOnly |
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.SetProperty, null, obj, new object[]{""});
//get property and will throw an ArgumentNullException
string plan = (string)t.InvokeMember("plan",
BindingFlags.DeclaredOnly |
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.GetProperty, null, obj, null);
7.创建类的实例后,如何给事件Event添加和删除委托Delegate?
EventInfo e = t.GetEvent ("Elapsed");
EventHandler handler = new EventHandler ((sender, args) => {Debug.Log("222");});
e.AddEventHandler (obj, handler);
e.RemoveEventHandler (obj, handler);
注:事件是在方法中调用的,所以没必要通过反射去单独调用事件,只需要处理增加和删除委托的操作即可
上面这些就是通过MemberInfo来创建类的实例Instance,读写字段Field,属性Property,调用方法Method,为事件Event增加
和删除委托Delegate.
上面提到:
MemberInfo派生出如下类:
System.Reflection.EventInfo
System.Reflection.FieldInfo
System.Reflection.MethodBase
System.Reflection.PropertyInfo
System.Reflection.TypeInfo
我们也可以直接使用派生的类,来分别的处理。
1.创建类的实例
Type[] types = new Type[4];
types [0] = typeof(string);
types [1] = typeof(int);
types [2] = typeof(bool);
types [3] = typeof(float);
ConstructorInfo info = t.GetConstructor (BindingFlags.Instance | BindingFlags.Public, null, CallingConventions.HasThis, types, null);
Debug.Log (info.ToString ());
nsperson.Person missi = info.Invoke (new object[]{ "Lionel Andres Messi", 31, true, 200 }) as nsperson.Person;
missi.AskPrivacy (true);
如果是构造函数是两个string,那么
Type[] types = new Type[2];
types [0] = typeof(string);
types [1] =types[0];
就可以了
1.读写字段Field
FieldInfo field = t.GetField ("deposit", BindingFlags.DeclaredOnly |
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.SetField);
field.SetValue (obj, 101);
float deposit = (float)field.GetValue (obj);
Debug.Log ("deposit:" + deposit);
通过SetValue,GetValue
2.调用方法Method
MethodInfo method = t.GetMethod ("AskPrivacy");
method.Invoke (obj, new object[]{true});
如果AskPrivacy有另外一个重载的函数:
public void AskPrivacy(bool access,string blah)
{
Debug.Log(access?(String.Format ("my name is {0},I am {1} years old,{2},my deposit is {3}, {4}", name, age, MarriedState(), deposit,blah))
:"sorry,this is my privacy!");
}
调用重载方法的实现如下:
MethodInfo method = t.GetMethod ("AskPrivacy",new Type[]{typeof(bool),typeof(string)});
method.Invoke (obj, new object[]{true,"blah blah blah....."});
需要传递Type数组以及对应的参数即可。
如果是非public方法,记得加上BindingFlags.Instance|BindingFlags.NonPublic参数
3.读写属性Property
PropertyInfo property = t.GetProperty ("plan");
property.SetValue(obj,"blah blah blah....",null);
Debug.Log ("property:"+property.GetValue(obj,null));
五、反射的缺点有哪些?
1.无法保证类型的安全性,反射严重依赖字符串,如我执行Type.GetType("int") 要通过反射在程序集中查找名为“int"的Type对象,代码会通过编译,但在运行的时候会返回null,因为int并不存在,CLR只知道“System.Int32".
2.反射速度慢,因为要不停的搜索字符串,如果字符串再设置为不区分大小写,效率会进一步降低。
3.调用成员时,比如反射调用方法,需要将实参打包成数组(pack)(new object[]{....}),在内部,需要将实参数组解包到线程栈上(unpack),而且在调用方法时,CLR要检查实参具有正确的数据类型。
六、实际应用
首先序列化和反序列化是一定会用到反射进行类型的定位,在工作当中,我印象中是在上一款项目里《节奏英雄》
有两处使用到了反射.但基本上是一个意思
1.预设绑定脚本,有些预设要绑定特定的脚本,比如BOSS,通常BOSS都要有自己独有的逻辑,比如每个BOSS你都对应的创建了相应的逻辑控制类,生成预设的时候,你需要将这些类绑定到预设上面,我是通过工具来生成预设的,所以提供一个字符串或是按照某一特定的命名规则,通过反射搜索到该类,并绑定到预设上就可以了,因为量比较大,总不能手动的去拖拽。
因为是在编辑阶段,不需要考虑性能的问题。
2.配置表,比如敌人,商店,任务,成就这些静态数据,在之前的项目中,这部分的代码是来自于以前的项目,他是通过外部的工具,将数据导出为xml格式,并生成对应的读取的类,如xxxTemplate,每个表基本上都会有不同的字段,在对应的读取类中,有共同的API,区别只是解析的字段不同,所以数据读取部分的API是通用的,但是历史遗留问题,是使用字符串为参数,通过反射找到相应的类,并完成实例的创建,进行后面的初始化工作。在业务逻辑中尽量不要使用反射。
到此为止,如果大家发现有什么不对的地方,欢迎指正,共同提高,感谢您的阅读!
编辑于2018.7.11
--闲言碎语
微信图片_20180711183214.jpg