1.准备工作
在接触反射之前,我们首先需要了解一下什么是元数据(Metadata)。元数据,就是描述数据的数据,比如数据集的名称、关系、字段、约束等。我们编写的C#代码经过编译器编译后,会生成相应的中间语言(IL),同时也会生成一套用于描述它的清单,即元数据。
2.什么是反射?
反射是指对程序集中的元数据进行检查的过程。 那么利用反射可以做什么事情呢?
- 它允许在运行时查看特性(attribute)信息。
- 它允许审查集合中的各种类型,以及实例化这些类型。
- 它允许延迟绑定的方法和属性(property)。
- 它允许在运行时创建新类型,然后使用这些类型执行一些任务。
3.如何使用反射?
我们可以通过System.Type
的实例来访问类型的元数据,该对象包含了对类型实例的成员进行枚举的方法。下面是System.Type
提供的部分成员。
Type type = typeof(string);
// 类型名称
string typeName = type.Name;
// 类型是否public
bool typeIsPublic = type.IsPublic;
// 类型的基类
Type? baseType = type.BaseType;
// 类型实现的接口
Type[] interfaces = type.GetInterfaces();
// 类型在哪个程序集定义
Assembly typeAssembly = type.Assembly;
// 类型的属性
PropertyInfo[] properties = type.GetProperties();
// 类型的方法
MethodInfo[] methods = type.GetMethods();
// 类型的字段
FieldInfo[] fields = type.GetFields();
// 类型的特性
IEnumerable<Attribute> attributes = type.GetCustomAttributes();
从上面的实例可以看出,我们要使用反射首先要获取System.Type
的实例。获取Type的实例有如下两种方式:
GetType()
适用于可以实例化的类型,需要通过类型的实例调用。
DateTime dateTime = new DateTime();
Type type = dateTime.GetType();
typeof()
可以通过类型直接获取。
Type type = typeof(string);
获取到Type对象后,就可以访问到目标类型的元数据。例如我们可以获取到类型的构造方法,然后通过该构造方法创建一个实例:
class Student
{
public string Name { get; set; }
public int Age { get; set; }
public Student()
{
Name = "AAA";
Age = 12;
}
public Student(string name)
{
Name = name;
}
private Student(string name, int age)
{
Name = name;
Age = age;
}
public void PrintStudent()
{
Console.WriteLine($"姓名:{Name} 年龄:{Age}");
}
}
public static void ReflectPracticeMain()
{
Type stuType = typeof(Student);
var constructors = stuType.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
var constructor = Array.Find(constructors, c => c.GetParameters().Length == 2);
if (constructor != null)
{
Student student =(Student) constructor.Invoke(new object[]{"李四",12});
student.PrintStudent();
}
}
其输出结果为:
在泛型类型中使用反射
反射同样可以运用在泛型类型中。例如用来判断类型参数的类型:
class MyList<T>
{
public void Add(T item)
{
var type = typeof(T);
if (type == typeof(int))
{
// ...
}
}
}
通过Type类型中的ContainsGenericParameters
属性判断类或方法是否包含尚未设置的泛型参数;IsGenericType
属性可以判断类型是否是泛型:
var type1 = typeof(MyList<>);
Console.WriteLine(type1.ContainsGenericParameters);
Console.WriteLine(type1.IsGenericType);
var type2 = typeof(MyList<int>);
Console.WriteLine(type2.ContainsGenericParameters);
Console.WriteLine(type2.IsGenericType);
其输出结果如下:
通过GetGenericArguments()
方法可以获取泛型实参的列表:
var type2 = typeof(Action<int,float,string>);
Type[] genericArguments = type2.GetGenericArguments();
foreach (var argument in genericArguments)
{
Console.WriteLine($"param:{argument.FullName}");
}
其输出结果如下:
4.使用dynamic调用反射
反射的关键功能之一是动态查找和调用特定类型的成员,这要求在执行时识别成员名或其他特征。dynamic动态对象提供了更简单的办法来通过反射调用成员。但限制是编译时需要知道成员名和签名。
下面来看一段示例代码:
dynamic data = "这是一段字符串";
Console.WriteLine(data);
data = (double)data.Length;
Console.WriteLine($"长度:{data}");
输出如下
相较于前面所说的反射,这种将对象声明为dynamic类型的方式并没有查找特定的MethodInfo实例进行调用。相反,在dynamic类型实例上调用方法,编译器并不会检查该成员是否可用。但这并不意味着类型安全被完全放弃,类型检查器会在执行时为dynamic类型调用。如果执行时发现实际并没有这个成员,则会抛出RuntimeBinderException。
dynamic的特征
- dynamic是通知编译器生成代码的指令。当运行时遇到第一个dynamic调用时,可以将请求编译成CIL,再调用新编译的调用。将类型指定成dynamic,相当于从概念上包装了原始类型。这样便不会发生编译时验证。此外在运行时调用一个成员时,包装器会解释调用,并相应地调度(或拒绝)它。
- 任何能转换成object的类型都能转换成dynamic。
- 从dynamic到一个替代类型的成功转换需要依赖基础类型的支持。
- dynamic的基础类型在每次赋值时都可能改变。这一点和
var
不同,var
不能重新赋值成一个不同的类型。 - 验证基础类型上是否存在指定签名要推迟到运行时进行。
- 任何dynamic成员调用都将返回dynamic对象。即便为data调用
ToString()
方法,也不会返回一个string对象。 - 用dynamic实现的反射不支持扩展方法。这一点和使用Type实现的反射一样,只有实现类型才能调用扩展方法。
参考文献:
[1]马克·米凯利斯.C#8.0本质论[M].机械工业出版社.