美文网首页
反射(Reflection)

反射(Reflection)

作者: 地坛公园 | 来源:发表于2018-07-11 18:33 被阅读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

    相关文章

      网友评论

          本文标题:反射(Reflection)

          本文链接:https://www.haomeiwen.com/subject/sbtuuftx.html