反射(Reflection)

2018-07-11  本文已影响0人  地坛公园

概念:
提到反射(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
上一篇下一篇

猜你喜欢

热点阅读