反射(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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容

  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,651评论 0 15
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • 前几天,南方大降温,昼夜间就跌了十几度。 降温的第二天,塔塔撅着嘴向我们控诉男友的“罪行”。 刚降温那天,塔塔和男...
    林一芙阅读 398评论 0 1
  • 夜深人静街道空, 热闹小城渐入梦。 偶有车辆飞奔去, 惊得诗人心颤冷。 2017.11.23深夜
    白丰阁阅读 152评论 0 2
  • /心若没有栖息的地方,到哪里都是流浪。/ 艳如血般的唇,我衔起一支烟, 吐出一朵残败的花儿。 我是 洛。 我卑微的...
    刺猬啊刺猬阅读 325评论 0 1