第 31 节 · 第四部分 进阶特性

反射(Reflection)

反射用 Type 等对象描述程序自己的结构,运行时按名字找类型、造对象、调方法、读特性;灵活但慢、难读。

时长 9:15授课计划第 12 周

学完这一节,你能

讲义

跟着视频的顺序写。每行代码都有注释,代码和运行结果都是实际运行得到的。

1反射是什么

反射(Reflection)是程序在运行时查看、甚至修改自己的能力:平时要调哪个方法在编译前就写死了,有了反射,可以等到运行时再按名字去找。上一章贴的特性被编译进了程序里,读出它们靠的就是反射。

2四层结构

程序集(编译出来的那个程序文件)包含模块,模块包含类型(比如 Student 类),类型包含成员(字段、属性、方法)。视频里把这四层比成一张检索台上的台子、托盘、方块和零件,检索臂就是反射。

每一层在程序里都有一个对象代表它:Assembly、Module 和 Type。这些描述信息存在程序集里,叫元数据。

3用反射造对象、调方法

我们写了一个小程序,Student 类有字段 Name、属性 Age 和方法 Hello。先按名字找到类型,拿到的 t 是描述 Student 的 Type 对象:

C# · 第 21-24 行
21Type t = Type.GetType("ReflectDemo.Student");// 按名字找类型,字符串里要带上命名空间
22Console.WriteLine(t.Assembly.GetName().Name);// 所在的程序集:demo
23Console.WriteLine(t.Module.Name);// 所在的模块:demo.dll
24Console.WriteLine(t.FullName);// 类型的全名:ReflectDemo.Student

再照着 t 造对象、改字段和属性、调方法。代码里没写 new Student,造哪种对象是运行时才定的:

C# · 第 26-29 行
26object s = Activator.CreateInstance(t);// 照着 t 造出一个对象 s
27t.GetField("Name").SetValue(s, "Tom");// 按名字找到字段 Name,设为 "Tom"
28t.GetProperty("Age").SetValue(s, 16);// 按名字找到属性 Age,设为 16
29t.GetMethod("Hello").Invoke(s, null);// 按名字找到方法 Hello 并调用(null 表示没有参数)
查看完整代码(34 行)
C#
1#nullable disable// 关掉编译器对“变量可能是空值”的警告
2using System;// 引入 System 命名空间
3using System.Reflection;// 引入 System.Reflection(反射相关的类型在这里)
4
5namespace 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.GetType() == t 打印 True:从现有对象取类型,拿到的就是同一个 Type。这些名字都写在字符串里,写错了编译器发现不了,要到运行时才出错。

4优点和缺点

  • 优点:灵活、好扩展;降低耦合(类和类之间绑得没那么紧,改一个不用牵动另一个);不用提前把要用的类写死。比如插件程序,主程序事先不知道插件里有哪些类型,运行时用反射去找、去创建。
  • 缺点一是慢:每次都要按名字查找、核对参数,比直接调用慢得多。所以反射多用在框架(别人写好、供很多程序共用的一大套基础代码)这种要求灵活的地方,普通程序能直接调用就直接调用。
  • 缺点二是难读:逻辑藏在字符串里,看源代码看不出调用了什么,维护更麻烦。

5反射的四种用途

  1. 运行时查看特性信息(下面马上讲)。
  2. 审查程序集里的各种类型,并创建它们的实例,比如上面的 CreateInstance。
  3. 延迟绑定方法和属性:等到运行时再按名字决定用哪一个,比如上面的 GetMethod。
  4. 运行时创建新的类型,要用 System.Reflection.Emit,属于高级用法,这里不演示。

6读出特性

System.Reflection 是一个命名空间,MemberInfo 是里面的类型;typeof(MyClass) 返回描述 MyClass 的 Type(Type 派生自 MemberInfo,所以能存进去),并没有创建 MyClass 的对象。

C# · 第 39-44 行
39System.Reflection.MemberInfo info = typeof(MyClass);// 取到 MyClass 的类型信息,存进 MemberInfo 变量
40object[] attributes = info.GetCustomAttributes(true);// 取出贴在 MyClass 身上的所有特性;true 表示从父类型继承来的也一起找
41for (int i = 0; i < attributes.Length; i++)// for 循环,挨个处理每个特性
42{// for 开始
43 System.Console.WriteLine(attributes[i]);// 直接打印特性对象:默认打印的是它的类型名
44}// for 结束
查看完整代码(49 行)
C#
1using System;// 引入 System 命名空间
2
3[AttributeUsage(AttributeTargets.All)]// 这个特性可以贴在任何地方
4public 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
29class MyClass// 定义 MyClass 类
30{// MyClass 类开始
31}// MyClass 类结束
32
33namespace 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 的矩形并打印长、宽、面积,再读类上的特性:

C# · 第 95-99 行
95Type type = typeof(Rectangle);// 用 typeof 拿到 Rectangle 的 Type
96// 遍历 Rectangle 类的特性
97foreach (Object attributes in type.GetCustomAttributes(false))// 取出类上的特性,false 表示不找继承来的
98{// foreach 开始
99 DeBugInfo dbi = (DeBugInfo)attributes;// 强制转换成 DeBugInfo(类型不对会抛异常)

输出前三行是长、宽和面积,接着是 45 号、49 号两张卡片,实测 45 号在前,标签是小写的 Bug no(见下一小节运行结果的前 11 行)。

8遍历方法时崩了

后半段用 GetMethods 拿到所有公共方法,再把每个方法的特性都转换成 DeBugInfo 打印。

C# · 第 111-116 行
111foreach (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 行)
C#
1using System;// 引入 System 命名空间
2using System.Reflection;// 引入 System.Reflection(MethodInfo 在这里)
3namespace 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。我们单独打印每个方法和它身上的特性:

C# · 第 93-102 行
93Type type = typeof(Rectangle);// 拿到 Rectangle 的 Type
94foreach (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(DeBugInfo),只取这一种特性:

C# · 第 113 行
113foreach (Attribute a in m.GetCustomAttributes(typeof(DeBugInfo), true))// 只取 DeBugInfo 这一种特性,别的不会进循环

改完再跑,输出就是上面异常之前的 19 行,四张卡片全部打印,程序正常结束。56 号的备注是空的,因为贴它的时候没写 Message。

10小结

  • 反射让程序在运行时查看自己:程序集、模块、类型、成员,每一层都有对象描述。
  • 用 Type 可以按名字找类型、创建实例、调用方法、读写字段和属性。
  • 读特性用 GetCustomAttributes;遍历方法时别忘了还有从 Object 继承来的方法。
  • 反射灵活,但慢、难读,能直接调用就直接调用。

下一节:这一章里 Topic、Age 这些带着 get 和 set 的成员,到底是什么、该怎么写?

检查题

5 道,选完马上告诉你对不对。

  1. 1demo 最后 Console.WriteLine(s.GetType() == t); 打印?

  2. 2HelpAttribute 例子打印?

  3. 3typeof(Rectangle).GetMethods() 一共拿到几个方法?

  4. 4教程第二个例子运行结果?

  5. 5改法: