1反射是什么
反射(Reflection)是程序在运行时查看、甚至修改自己的能力:平时要调哪个方法在编译前就写死了,有了反射,可以等到运行时再按名字去找。上一章贴的特性被编译进了程序里,读出它们靠的就是反射。
2四层结构
程序集(编译出来的那个程序文件)包含模块,模块包含类型(比如 Student 类),类型包含成员(字段、属性、方法)。视频里把这四层比成一张检索台上的台子、托盘、方块和零件,检索臂就是反射。
每一层在程序里都有一个对象代表它:Assembly、Module 和 Type。这些描述信息存在程序集里,叫元数据。
3用反射造对象、调方法
我们写了一个小程序,Student 类有字段 Name、属性 Age 和方法 Hello。先按名字找到类型,拿到的 t 是描述 Student 的 Type 对象:
| 21 | Type t = Type.GetType("ReflectDemo.Student"); | // 按名字找类型,字符串里要带上命名空间 |
| 22 | Console.WriteLine(t.Assembly.GetName().Name); | // 所在的程序集:demo |
| 23 | Console.WriteLine(t.Module.Name); | // 所在的模块:demo.dll |
| 24 | Console.WriteLine(t.FullName); | // 类型的全名:ReflectDemo.Student |
再照着 t 造对象、改字段和属性、调方法。代码里没写 new Student,造哪种对象是运行时才定的:
| 26 | object s = Activator.CreateInstance(t); | // 照着 t 造出一个对象 s |
| 27 | t.GetField("Name").SetValue(s, "Tom"); | // 按名字找到字段 Name,设为 "Tom" |
| 28 | t.GetProperty("Age").SetValue(s, 16); | // 按名字找到属性 Age,设为 16 |
| 29 | t.GetMethod("Hello").Invoke(s, null); | // 按名字找到方法 Hello 并调用(null 表示没有参数) |
查看完整代码(34 行)
| 1 | #nullable disable | // 关掉编译器对“变量可能是空值”的警告 |
| 2 | using System; | // 引入 System 命名空间 |
| 3 | using System.Reflection; | // 引入 System.Reflection(反射相关的类型在这里) |
| 4 | | |
| 5 | namespace ReflectDemo | // 定义命名空间 ReflectDemo |
| 6 | { | // 命名空间 ReflectDemo 开始 |
| 7 | class Student | // 定义 Student 类 |
| 8 | { | // Student 类开始 |
| 9 | public string Name = ""; | // 字段 Name |
| 10 | public int Age { get; set; } | // 属性 Age({ get; set; } 是自动属性,下一章讲) |
| 11 | public void Hello() | // 方法 Hello |
| 12 | { | // Hello 开始 |
| 13 | Console.WriteLine("Hi, I am {0}, {1}", Name, Age); | // 打印名字和年龄 |
| 14 | } | // Hello 结束 |
| 15 | } | // Student 类结束 |
| 16 | | |
| 17 | class Program | // 定义 Program 类 |
| 18 | { | // Program 类开始 |
| 19 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 20 | { | // Main 开始 |
| 21 | Type t = Type.GetType("ReflectDemo.Student"); | // 按名字 "ReflectDemo.Student" 找到类型 |
| 22 | Console.WriteLine(t.Assembly.GetName().Name); | // 打印程序集名 |
| 23 | Console.WriteLine(t.Module.Name); | // 打印模块名 |
| 24 | Console.WriteLine(t.FullName); | // 打印类型全名 |
| 25 | | |
| 26 | object s = Activator.CreateInstance(t); | // 运行时创建 Student 对象 |
| 27 | t.GetField("Name").SetValue(s, "Tom"); | // 字段 Name 设为 "Tom" |
| 28 | t.GetProperty("Age").SetValue(s, 16); | // 属性 Age 设为 16 |
| 29 | t.GetMethod("Hello").Invoke(s, null); | // 调用 Hello:打印 Hi, I am Tom, 16 |
| 30 | | |
| 31 | Console.WriteLine(s.GetType() == t); | // 比较类型:True |
| 32 | } | // Main 结束 |
| 33 | } | // Program 类结束 |
| 34 | } | // 命名空间 ReflectDemo 结束 |
demo demo.dll ReflectDemo.Student Hi, I am Tom, 16 True
前三行对上程序集、模块、类型三层。最后一行 s. 打印 True:从现有对象取类型,拿到的就是同一个 Type。这些名字都写在字符串里,写错了编译器发现不了,要到运行时才出错。
4优点和缺点
- 优点:灵活、好扩展;降低耦合(类和类之间绑得没那么紧,改一个不用牵动另一个);不用提前把要用的类写死。比如插件程序,主程序事先不知道插件里有哪些类型,运行时用反射去找、去创建。
- 缺点一是慢:每次都要按名字查找、核对参数,比直接调用慢得多。所以反射多用在框架(别人写好、供很多程序共用的一大套基础代码)这种要求灵活的地方,普通程序能直接调用就直接调用。
- 缺点二是难读:逻辑藏在字符串里,看源代码看不出调用了什么,维护更麻烦。
5反射的四种用途
- 运行时查看特性信息(下面马上讲)。
- 审查程序集里的各种类型,并创建它们的实例,比如上面的
CreateInstance。 - 延迟绑定方法和属性:等到运行时再按名字决定用哪一个,比如上面的
GetMethod。 - 运行时创建新的类型,要用
System.,属于高级用法,这里不演示。Reflection. Emit
6读出特性
System. 是一个命名空间,MemberInfo 是里面的类型;typeof( 返回描述 MyClass 的 Type(Type 派生自 MemberInfo,所以能存进去),并没有创建 MyClass 的对象。
| 39 | System.Reflection.MemberInfo info = typeof(MyClass); | // 取到 MyClass 的类型信息,存进 MemberInfo 变量 |
| 40 | object[] attributes = info.GetCustomAttributes(true); | // 取出贴在 MyClass 身上的所有特性;true 表示从父类型继承来的也一起找 |
| 41 | for (int i = 0; i < attributes.Length; i++) | // for 循环,挨个处理每个特性 |
| 42 | { | // for 开始 |
| 43 | System.Console.WriteLine(attributes[i]); | // 直接打印特性对象:默认打印的是它的类型名 |
| 44 | } | // for 结束 |
查看完整代码(49 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | | |
| 3 | [AttributeUsage(AttributeTargets.All)] | // 这个特性可以贴在任何地方 |
| 4 | public class HelpAttribute : System.Attribute | // 自定义特性 HelpAttribute,继承 Attribute |
| 5 | { | // HelpAttribute 类开始 |
| 6 | public readonly string Url; | // 只读字段 Url |
| 7 | | |
| 8 | public string Topic // Topic 是一个命名(named)参数 | // 公共属性 Topic,贴特性时可以写成命名参数 |
| 9 | { | // Topic 属性开始 |
| 10 | get | // get 访问器:读 Topic 时执行 |
| 11 | { | // get 开始 |
| 12 | return topic; | // 返回私有字段 topic |
| 13 | } | // get 结束 |
| 14 | set | // set 访问器:给 Topic 赋值时执行 |
| 15 | { | // set 开始 |
| 16 | | |
| 17 | topic = value; | // 把赋进来的值 value 存进 topic |
| 18 | } | // set 结束 |
| 19 | } | // Topic 属性结束 |
| 20 | | |
| 21 | public HelpAttribute(string url) // url 是一个定位(positional)参数 | // 构造函数,收一个 url |
| 22 | { | // 构造函数开始 |
| 23 | this.Url = url; | // 把 url 存进 Url |
| 24 | } | // 构造函数结束 |
| 25 | | |
| 26 | private string topic; | // 私有字段 topic,存 Topic 的值 |
| 27 | } | // HelpAttribute 类结束 |
| 28 | [HelpAttribute("Information on the class MyClass")] | // 给 MyClass 贴上 HelpAttribute,传入位置参数 url |
| 29 | class MyClass | // 定义 MyClass 类 |
| 30 | { | // MyClass 类开始 |
| 31 | } | // MyClass 类结束 |
| 32 | | |
| 33 | namespace AttributeAppl | // 定义命名空间 AttributeAppl |
| 34 | { | // 命名空间 AttributeAppl 开始 |
| 35 | class Program | // 定义 Program 类 |
| 36 | { | // Program 类开始 |
| 37 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 38 | { | // Main 开始 |
| 39 | System.Reflection.MemberInfo info = typeof(MyClass); | // 拿到 MyClass 的类型信息 |
| 40 | object[] attributes = info.GetCustomAttributes(true); | // 取出它身上的所有特性 |
| 41 | for (int i = 0; i < attributes.Length; i++) | // 遍历每个特性 |
| 42 | { | // for 开始 |
| 43 | System.Console.WriteLine(attributes[i]); | // 打印特性对象 |
| 44 | } | // for 结束 |
| 45 | Console.ReadKey(); | // 等待按键(现在的 VS 不写也会停住) |
| 46 | | |
| 47 | } | // Main 结束 |
| 48 | } | // Program 类结束 |
| 49 | } | // 命名空间 AttributeAppl 结束 |
HelpAttribute
只有一行 HelpAttribute:直接打印一个对象,默认打印它的类型名,所以看到的是特性的名字,不是里面那串文字。
7读出类上的 DeBugInfo
第二个例子用上一章的 DeBugInfo 特性:Rectangle 类前面贴了 45 号和 49 号两张,GetArea 和 Display 各贴一张。
Main 先造一个 4.5 × 7.5 的矩形并打印长、宽、面积,再读类上的特性:
| 95 | Type type = typeof(Rectangle); | // 用 typeof 拿到 Rectangle 的 Type |
| 96 | // 遍历 Rectangle 类的特性 | |
| 97 | foreach (Object attributes in type.GetCustomAttributes(false)) | // 取出类上的特性,false 表示不找继承来的 |
| 98 | { | // foreach 开始 |
| 99 | DeBugInfo dbi = (DeBugInfo)attributes; | // 强制转换成 DeBugInfo(类型不对会抛异常) |
输出前三行是长、宽和面积,接着是 45 号、49 号两张卡片,实测 45 号在前,标签是小写的 Bug no(见下一小节运行结果的前 11 行)。
8遍历方法时崩了
后半段用 GetMethods 拿到所有公共方法,再把每个方法的特性都转换成 DeBugInfo 打印。
| 111 | foreach (MethodInfo m in type.GetMethods()) | // MethodInfo 是描述一个方法的对象;拿到所有公共方法 |
| 112 | { | // 外层 foreach 开始 |
| 113 | foreach (Attribute a in m.GetCustomAttributes(true)) | // 对每个方法取出它身上的所有特性 |
| 114 | { | // 内层 foreach 开始 |
| 115 | DeBugInfo dbi = (DeBugInfo)a; | // 强制转换成 DeBugInfo:遇到别的特性会直接抛异常 |
| 116 | if (null != dbi) | // 拦不住:转换失败不会得到空值 |
查看完整代码(130 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | using System.Reflection; | // 引入 System.Reflection(MethodInfo 在这里) |
| 3 | namespace BugFixApplication | // 定义命名空间 BugFixApplication |
| 4 | { | // 命名空间开始 |
| 5 | // 一个自定义特性 BugFix 被赋给类及其成员 | |
| 6 | [AttributeUsage(AttributeTargets.Class | | // 特性用途:可以贴在类、 |
| 7 | AttributeTargets.Constructor | | // 构造函数、 |
| 8 | AttributeTargets.Field | | // 字段、 |
| 9 | AttributeTargets.Method | | // 方法、 |
| 10 | AttributeTargets.Property, | // 属性上, |
| 11 | AllowMultiple = true)] | // AllowMultiple = true:同一处可以贴好几张 |
| 12 | | |
| 13 | public class DeBugInfo : System.Attribute | // 定义特性 DeBugInfo,继承 System.Attribute |
| 14 | { | // DeBugInfo 类开始 |
| 15 | private int bugNo; | // 私有字段:Bug 编号 |
| 16 | private string developer; | // 私有字段:开发人员 |
| 17 | private string lastReview; | // 私有字段:最后审查日期 |
| 18 | public string message; | // 公共字段:备注 |
| 19 | | |
| 20 | public DeBugInfo(int bg, string dev, string d) | // 构造函数,收编号、开发人员、日期 |
| 21 | { | // 构造函数开始 |
| 22 | this.bugNo = bg; | // 存编号 |
| 23 | this.developer = dev; | // 存开发人员 |
| 24 | this.lastReview = d; | // 存日期 |
| 25 | } | // 构造函数结束 |
| 26 | | |
| 27 | public int BugNo | // 只读属性 BugNo |
| 28 | { | // BugNo 开始 |
| 29 | get | // 只有 get:只能读 |
| 30 | { | // get 开始 |
| 31 | return bugNo; | // 返回编号 |
| 32 | } | // get 结束 |
| 33 | } | // BugNo 结束 |
| 34 | public string Developer | // 只读属性 Developer |
| 35 | { | // Developer 开始 |
| 36 | get | // 只有 get |
| 37 | { | // get 开始 |
| 38 | return developer; | // 返回开发人员 |
| 39 | } | // get 结束 |
| 40 | } | // Developer 结束 |
| 41 | public string LastReview | // 只读属性 LastReview |
| 42 | { | // LastReview 开始 |
| 43 | get | // 只有 get |
| 44 | { | // get 开始 |
| 45 | return lastReview; | // 返回日期 |
| 46 | } | // get 结束 |
| 47 | } | // LastReview 结束 |
| 48 | public string Message | // 可读可写属性 Message |
| 49 | { | // Message 开始 |
| 50 | get | // get 访问器 |
| 51 | { | // get 开始 |
| 52 | return message; | // 返回备注 |
| 53 | } | // get 结束 |
| 54 | set | // set 访问器 |
| 55 | { | // set 开始 |
| 56 | message = value; | // 存备注 |
| 57 | } | // set 结束 |
| 58 | } | // Message 结束 |
| 59 | } | // DeBugInfo 类结束 |
| 60 | [DeBugInfo(45, "Zara Ali", "12/8/2012", | // 给 Rectangle 贴 45 号卡片, |
| 61 | Message = "Return type mismatch")] | // 命名参数 Message 写备注 |
| 62 | [DeBugInfo(49, "Nuha Ali", "10/10/2012", | // 再贴 49 号卡片, |
| 63 | Message = "Unused variable")] | // 备注 Unused variable |
| 64 | class Rectangle | // 定义 Rectangle 类 |
| 65 | { | // Rectangle 类开始 |
| 66 | // 成员变量 | |
| 67 | protected double length; | // 长 |
| 68 | protected double width; | // 宽 |
| 69 | public Rectangle(double l, double w) | // 构造函数,收长和宽 |
| 70 | { | // 构造函数开始 |
| 71 | length = l; | // 存长 |
| 72 | width = w; | // 存宽 |
| 73 | } | // 构造函数结束 |
| 74 | [DeBugInfo(55, "Zara Ali", "19/10/2012", | // 给 GetArea 贴 55 号卡片, |
| 75 | Message = "Return type mismatch")] | // 带备注 |
| 76 | public double GetArea() | // GetArea:算面积 |
| 77 | { | // GetArea 开始 |
| 78 | return length * width; | // 返回长 × 宽 |
| 79 | } | // GetArea 结束 |
| 80 | [DeBugInfo(56, "Zara Ali", "19/10/2012")] | // 给 Display 贴 56 号卡片,没写 Message |
| 81 | public void Display() | // Display:打印长、宽、面积 |
| 82 | { | // Display 开始 |
| 83 | Console.WriteLine("Length: {0}", length); | // 打印长 |
| 84 | Console.WriteLine("Width: {0}", width); | // 打印宽 |
| 85 | Console.WriteLine("Area: {0}", GetArea()); | // 打印面积 |
| 86 | } | // Display 结束 |
| 87 | }//end class Rectangle | // Rectangle 类结束 |
| 88 | | |
| 89 | class ExecuteRectangle | // 定义 ExecuteRectangle 类 |
| 90 | { | // ExecuteRectangle 类开始 |
| 91 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 92 | { | // Main 开始 |
| 93 | Rectangle r = new Rectangle(4.5, 7.5); | // 造一个 4.5 × 7.5 的矩形 |
| 94 | r.Display(); | // 打印长、宽、面积 |
| 95 | Type type = typeof(Rectangle); | // 拿到 Rectangle 的 Type |
| 96 | // 遍历 Rectangle 类的特性 | |
| 97 | foreach (Object attributes in type.GetCustomAttributes(false)) | // 遍历类上的特性 |
| 98 | { | // foreach 开始 |
| 99 | DeBugInfo dbi = (DeBugInfo)attributes; | // 转换成 DeBugInfo |
| 100 | if (null != dbi) | // 判断不是空(其实没用,下面会看到) |
| 101 | { | // if 开始 |
| 102 | Console.WriteLine("Bug no: {0}", dbi.BugNo); | // 打印编号 |
| 103 | Console.WriteLine("Developer: {0}", dbi.Developer); | // 打印开发人员 |
| 104 | Console.WriteLine("Last Reviewed: {0}", | // 打印审查日期 |
| 105 | dbi.LastReview); | // 接上一行:{0} 处填 dbi.LastReview |
| 106 | Console.WriteLine("Remarks: {0}", dbi.Message); | // 打印备注 |
| 107 | } | // if 结束 |
| 108 | } | // foreach 结束 |
| 109 | | |
| 110 | // 遍历方法特性 | |
| 111 | foreach (MethodInfo m in type.GetMethods()) | // 遍历所有公共方法(含从 Object 继承来的) |
| 112 | { | // 外层 foreach 开始 |
| 113 | foreach (Attribute a in m.GetCustomAttributes(true)) | // 遍历方法上的所有特性 |
| 114 | { | // 内层 foreach 开始 |
| 115 | DeBugInfo dbi = (DeBugInfo)a; | // 强制转换:遇到 NullableContextAttribute 就抛异常 |
| 116 | if (null != dbi) | // 判断不是空(拦不住异常) |
| 117 | { | // if 开始 |
| 118 | Console.WriteLine("Bug no: {0}, for Method: {1}", | // 打印编号和方法名 |
| 119 | dbi.BugNo, m.Name); | // 接上一行:{0} 填编号,{1} 填方法名 |
| 120 | Console.WriteLine("Developer: {0}", dbi.Developer); | // 打印开发人员 |
| 121 | Console.WriteLine("Last Reviewed: {0}", | // 打印审查日期 |
| 122 | dbi.LastReview); | // 接上一行:{0} 处填 dbi.LastReview |
| 123 | Console.WriteLine("Remarks: {0}", dbi.Message); | // 打印备注 |
| 124 | } | // if 结束 |
| 125 | } | // 内层 foreach 结束 |
| 126 | } | // 外层 foreach 结束 |
| 127 | Console.ReadLine(); | // 等待回车(现在的 VS 不写也会停住) |
| 128 | } | // Main 结束 |
| 129 | } | // ExecuteRectangle 类结束 |
| 130 | } | // 命名空间结束 |
Length: 4.5 Width: 7.5 Area: 33.75 Bug no: 45 Developer: Zara Ali Last Reviewed: 12/8/2012 Remarks: Return type mismatch Bug no: 49 Developer: Nuha Ali Last Reviewed: 10/10/2012 Remarks: Unused variable Bug no: 55, for Method: GetArea Developer: Zara Ali Last Reviewed: 19/10/2012 Remarks: Return type mismatch Bug no: 56, for Method: Display Developer: Zara Ali Last Reviewed: 19/10/2012 Remarks: Unhandled exception. System.InvalidCastException: Unable to cast object of type 'System.Runtime.CompilerServices.NullableContextAttribute' to type 'BugFixApplication.DeBugInfo'. at BugFixApplication.ExecuteRectangle.Main(String[] args) in 反射/bugfix/Program.cs:line 115
GetArea 和 Display 的卡片打完,程序抛出 InvalidCastException:有一个 NullableContextAttribute 没法转换成 DeBugInfo。我们单独打印每个方法和它身上的特性:
| 93 | Type type = typeof(Rectangle); | // 拿到 Rectangle 的 Type |
| 94 | foreach (MethodInfo m in type.GetMethods()) | // 遍历所有公共方法 |
| 95 | { | // foreach 开始 |
| 96 | Console.Write("{0}:", m.Name); | // 先打印方法名和冒号 |
| 97 | foreach (Attribute a in m.GetCustomAttributes(true)) | // 遍历这个方法上的特性 |
| 98 | { | // 内层 foreach 开始 |
| 99 | Console.Write(" {0}", a.GetType().Name); | // 打印特性的类型名 |
| 100 | } | // 内层 foreach 结束 |
| 101 | Console.WriteLine(); | // 换行 |
| 102 | } | // foreach 结束 |
GetArea: DeBugInfo Display: DeBugInfo GetType: NullableContextAttribute IntrinsicAttribute ToString: Equals: GetHashCode:
一共六个方法,后四个是从 Object 继承来的。GetType 身上带着两个编译器和运行时内部用的特性,强制转换成 DeBugInfo 失败,就抛了异常。
9修法:只取 DeBugInfo
调用 GetCustomAttributes 时传入 typeof(,只取这一种特性:
| 113 | foreach (Attribute a in m.GetCustomAttributes(typeof(DeBugInfo), true)) | // 只取 DeBugInfo 这一种特性,别的不会进循环 |
改完再跑,输出就是上面异常之前的 19 行,四张卡片全部打印,程序正常结束。56 号的备注是空的,因为贴它的时候没写 Message。
10小结
- 反射让程序在运行时查看自己:程序集、模块、类型、成员,每一层都有对象描述。
- 用 Type 可以按名字找类型、创建实例、调用方法、读写字段和属性。
- 读特性用
GetCustomAttributes;遍历方法时别忘了还有从 Object 继承来的方法。 - 反射灵活,但慢、难读,能直接调用就直接调用。
下一节:这一章里 Topic、Age 这些带着 get 和 set 的成员,到底是什么、该怎么写?
