首发于 C# .NET
18. 反射和元数据_02_反射和调用成员

18. 反射和元数据_02_反射和调用成员

Reflecting and Invoking Members

GetMembers 方法返回类型的成员。考虑下面的类:

class Walnut
    private bool cracked;
    public void Crack() { cracked = true; }

我们可以反思它的公共成员,如下所示:

MemberInfo[] members = typeof (Walnut).GetMembers();
foreach (MemberInfo m in members)
		Console.WriteLine (m);

这是结果:

Void Crack()
System.Type GetType()
System.String ToString()
Boolean Equals(System.Object)
Int32 GetHashCode()
Void .ctor()

当不带参数调用时,GetMembers 返回一个类型(及其基类型)的所有公共成员。 GetMember 按名称检索特定成员——尽管它仍然返回一个数组,因为成员可以重载:

MemberInfo[] m = typeof (Walnut).GetMember ("Crack");
Console.WriteLine (m[0]); // Void Crack()

MemberInfo 也有一个名为 MemberType 的属性,类型为 MemberTypes。这是一个具有这些值的标志枚举:

All         Custom     Field      NestedType     TypeInfo
Constructor Event      Method     Property

调用 GetMembers 时,您可以传入 MemberTypes 实例以限制它返回的成员的种类。或者,您可以通过调用 GetMethods、GetFields、GetProperties、GetEvents、GetConstructors 或 GetNestedTypes 来限制结果集。也有每一个的单数版本以针对特定成员。

MemberInfo 对象有一个 Name 属性和两个 Type 属性:

DeclaringType

返回定义成员的类型

ReflectedType

返回调用 GetMembers 的类型

这两个在基类型中定义的成员上调用时不同: DeclaringType 返回基类型,而 ReflectedType 返回子类型。下面的示例强调了这一点:

// MethodInfo is a subclass of MemberInfo; see Figure 18-1.
MethodInfo test = typeof (Program).GetMethod ("ToString");
MethodInfo obj = typeof (object) .GetMethod ("ToString");
Console.WriteLine (test.DeclaringType); // System.Object
Console.WriteLine (obj.DeclaringType);  // System.Object
Console.WriteLine (test.ReflectedType); // Program
Console.WriteLine (obj.ReflectedType);  // System.Object
Console.WriteLine (test == obj);        // False

因为它们具有不同的 ReflectedType,所以 test 和 obj 对象不相等。然而,它们的区别纯粹是反射 API 的构造;我们的 Program 类型在底层类型系统中没有独特的 ToString 方法。我们可以通过以下两种方式之一验证两个 MethodInfo 对象是否引用相同的方法:

Console.WriteLine (test.MethodHandle == obj.MethodHandle); // True
Console.WriteLine (test.MetadataToken == obj.MetadataToken // True
										&& test.Module == obj.Module);

MethodHandle 对于流程中的每个(真正不同的)方法是唯一的; MetadataToken 在组装模块中的所有类型和成员中都是唯一的。

MemberInfo 还定义了返回自定义属性的方法(请参阅第 807 页的“在运行时检索属性”)。

Member Types

MemberInfo 本身对成员很轻,因为它是图 18-1 中所示类型的抽象基础。

您可以根据 MemberType 属性将 MemberInfo 转换为其子类型。如果您通过 GetMethod、GetField、GetProperty、GetEvent、GetConstructor 或 GetNestedType(或它们的复数形式)获得成员,则不需要强制转换。表 18-1 总结了每种 C# 结构使用的方法。

每个 MemberInfo 子类都有丰富的属性和方法,公开成员元数据的所有方面。这包括诸如可见性、修饰符、泛型类型参数、参数、返回类型和自定义属性之类的东西。以下是使用 GetMethod 的示例:

MethodInfo m = typeof (Walnut).GetMethod ("Crack");
Console.WriteLine (m);            // Void Crack()
Console.WriteLine (m.ReturnType); // System.Void

所有 *Info 实例在首次使用时都由反射 API 缓存:

MethodInfo method = typeof (Walnut).GetMethod ("Crack");
MemberInfo member = typeof (Walnut).GetMember ("Crack") [0];
Console.Write (method == member); // True

除了保留对象身份外,缓存还提高了相当慢的 API 的性能。

C# Members Versus CLR Members

上表说明了一些 C# 的功能结构与 CLR 结构没有 1:1 的映射。这是有道理的,因为 CLR 和反射 API 在设计时考虑到了所有 .NET 语言——您甚至可以在 Visual Basic 中使用反射。

就 CLR 而言,某些 C# 构造(即索引器、枚举、运算符和终结器)是发明 contrivances 。具体来说:

  • C# 索引器转换为接受一个或多个参数的属性,标记为类型的 [DefaultMember]。
  • C# 枚举转换为 System.Enum 的子类型,每个成员都有一个静态字段。
  • C# 运算符转换为特别命名的静态方法,从“op_”开始;例如,“op_Addition”。
  • C# finalizer 转换为覆盖Finalize 的方法。

另一个复杂问题是属性和事件实际上包含两部分:

  • 描述属性或事件的元数据(由 PropertyInfo 或 EventInfo 封装)
  • 一个或两个支持方法

在 C# 程序中,支持方法封装在属性或事件定义中。但是当编译为 IL 时,支持方法作为普通方法呈现,您可以像调用任何其他方法一样调用它们。这意味着 GetMethods 返回属性和事件支持方法以及普通方法:

class Test { public int X { get { return 0; } set {} } }
void Demo()
		foreach (MethodInfo mi in typeof (Test).GetMethods())
				Console.Write (mi.Name + " ");
// OUTPUT:
get_X set_X GetType ToString Equals GetHashCode

您可以通过 Method Info 中的 IsSpecialName 属性识别这些方法。 IsSpecialName 为属性、索引器和事件访问器以及运算符返回 true。它仅对传统的 C# 方法返回 false — 如果定义了终结器,则返回 Finalize 方法。

以下是 C# 生成的后备方法:

每个后备方法都有其自己关联的 MethodInfo 对象。您可以按如下方式访问它们:

PropertyInfo pi = typeof (Console).GetProperty ("Title");
MethodInfo getter = pi.GetGetMethod(); // get_Title
MethodInfo setter = pi.GetSetMethod(); // set_Title
MethodInfo[] both = pi.GetAccessors(); // Length==2

GetAddMethod 和 GetRemoveMethod 对 EventInfo 执行类似的工作。要反向进行——从 MethodInfo 到其关联的 PropertyInfo 或 EventInfo——您需要执行查询。 LINQ 非常适合这项工作:

PropertyInfo p = mi.DeclaringType.GetProperties()
		.First (x => x.GetAccessors (true).Contains (mi));

Init-only properties

C# 9 中引入的 Init-only 属性可以通过对象初始化器设置,但随后被编译器视为只读。从 CLR 的角度来看,一个 init 访问器就像一个普通的 set 访问器,但是有一个特殊的标志应用于 set 方法的返回类型(这对编译器意味着什么)。

奇怪的是,此标志未编码为约定属性。相反,它使用一种称为 modreq 的相对晦涩的机制,它确保以前版本的 C# 编译器(不识别新的 modreq)忽略访问器而不是将属性视为可写。 init-only 访问器的 modreq 称为 IsExternalInit,您可以按如下方式查询它:

bool IsInitOnly (PropertyInfo pi) => pi
		.GetSetMethod().ReturnParameter.GetRequiredCustomModifiers()
		.Any (t => t.Name == "IsExternalInit");

NullabilityContextInfo

从 .NET 6 开始,您可以使用 NullabilityInfoContext 类来获取有关字段、属性、事件或参数的可空性注释的信息:

void PrintPropertyNullability (PropertyInfo pi)
		var info = new NullabilityInfoContext().Create (pi);
		Console.WriteLine (pi.Name + " read " + info.ReadState);
		Console.WriteLine (pi.Name + " write " + info.WriteState);
		// Use info.Element to get nullability info for array elements

Generic Type Members

您可以获得未绑定和封闭泛型类型的成员元数据:

PropertyInfo unbound = typeof (IEnumerator<>) .GetProperty ("Current");
PropertyInfo closed = typeof (IEnumerator<int>).GetProperty ("Current");
Console.WriteLine (unbound); // T Current
Console.WriteLine (closed); // Int32 Current
Console.WriteLine (unbound.PropertyType.IsGenericParameter); // True
Console.WriteLine (closed.PropertyType.IsGenericParameter); // False

从未绑定和封闭泛型类型返回的 MemberInfo 对象始终是不同的,即使对于其签名不具有泛型类型参数的成员也是如此:

PropertyInfo unbound = typeof (List<>) .GetProperty ("Count");
PropertyInfo closed = typeof (List<int>).GetProperty ("Count");
Console.WriteLine (unbound); // Int32 Count
Console.WriteLine (closed);  // Int32 Count
Console.WriteLine (unbound == closed); // False
Console.WriteLine (unbound.DeclaringType.IsGenericTypeDefinition); // True
Console.WriteLine (closed.DeclaringType.IsGenericTypeDefinition);  // False

无法动态调用 dynamically invoked 未绑定泛型类型的成员。

Dynamically Invoking a Member

拥有 MethodInfo、PropertyInfo 或 FieldInfo 对象后,您可以动态调用它或获取/设置它的值。这称为后期绑定,因为您选择在运行时而不是编译时调用哪个成员。为了说明,下面使用普通的静态绑定:

string s = "Hello";
int length = s.Length;

下面是使用后期绑定动态执行的相同操作:

object s = "Hello";
PropertyInfo prop = s.GetType().GetProperty ("Length");
int length = (int) prop.GetValue (s, null); // 5

GetValue 和 SetValue 获取和设置 PropertyInfo 或 FieldInfo 的值。第一个参数是实例,对于静态成员可以为 null。访问索引器就像访问名为“Item”的属性一样,只是在调用 GetValue 或 SetValue 时提供索引器值作为第二个参数。

要动态调用方法,请在 MethodInfo 上调用 Invoke,提供要传递给该方法的参数数组。如果您弄错了任何参数类型,则会在运行时抛出异常。使用动态调用,您会失去编译时类型安全性,但您仍然拥有运行时类型安全性(就像 dynamic 关键字一样)。

Method Parameters

假设我们要动态调用字符串的 Substring 方法。静态地,我们将按如下方式执行此操作:

Console.WriteLine ("stamp".Substring(2)); // "amp"

这是使用反射和后期绑定的动态等价物:

Type type = typeof (string);
Type[] parameterTypes = { typeof (int) };
MethodInfo method = type.GetMethod ("Substring", parameterTypes);
object[] arguments = { 2 };
object returnValue = method.Invoke ("stamp", arguments);
Console.WriteLine (returnValue); // "amp"

因为 Substring 方法被重载,我们必须将参数类型数组传递给 GetMethod 以指示我们想要的版本。如果没有参数类型,GetMethod 将抛出 AmbiguousMatchException。

在 MethodBase(MethodInfo 和 ConstructorInfo 的基类)上定义的 GetParameters 方法返回参数元数据。我们可以继续我们之前的示例,如下所示:

ParameterInfo[] paramList = method.GetParameters();
foreach (ParameterInfo x in paramList)
		Console.WriteLine (x.Name);          // startIndex
		Console.WriteLine (x.ParameterType); // System.Int32

Dealing with ref and out parameters

要传递 ref 或 out 参数,请在获取方法之前对类型调用 MakeByRefType。例如,

int x;
bool successfulParse = int.TryParse ("23", out x);

您可以按如下方式动态执行此代码:

object[] args = { "23", 0 };
Type[] argTypes = { typeof (string), typeof (int).**MakeByRefType**() };
MethodInfo tryParse = typeof (int).GetMethod ("TryParse", argTypes);
bool successfulParse = (bool) tryParse.Invoke (null, args);

同样的方法适用于 ref 和 out 参数类型。

Retrieving and invoking generic methods

在调用 GetMethod 时显式指定参数类型对于消除重载方法的歧义至关重要。但是,不可能指定泛型参数类型。例如,考虑重载 Where 方法的 System.Linq.Enumerable 类,如下所示:

public static IEnumerable<TSource> Where<TSource>
		(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
public static IEnumerable<TSource> Where<TSource>
		(this IEnumerable<TSource> source, Func<TSource, int, bool> predicate);

要检索特定的重载,我们必须检索所有方法,然后手动找到所需的重载。以下查询检索 Where 的先前重载:

from m in typeof (Enumerable).GetMethods()
where m.Name == "Where" && m.IsGenericMethod
let parameters = m.GetParameters()
where parameters.Length == 2
let genArg = m.GetGenericArguments().First()
let enumerableOfT = typeof (IEnumerable<>).MakeGenericType (genArg)
let funcOfTBool = typeof (Func<,>).MakeGenericType (genArg, typeof (bool))
where parameters[0].ParameterType == enumerableOfT
&& parameters[1].ParameterType == funcOfTBool
select m

在此查询上调用 .Single() 会提供具有未绑定类型参数的正确 MethodInfo 对象。下一步是通过调用 MakeGenericMethod 来关闭类型参数:

var closedMethod = unboundMethod.MakeGenericMethod (typeof (int));

在这种情况下,我们用 int 关闭了 TSource,允许我们使用 IEnumerable<int> 类型的源和 Func<int, bool>类型的谓词调用 Enumerable.Where :

int[] source = { 3, 4, 5, 6, 7, 8 };
Func<int, bool> predicate = n => n % 2 == 1; // Odd numbers only

我们现在可以调用封闭的泛型方法:

var query = (IEnumerable<int>) closedMethod.Invoke(null, new object[] { source, predicate });
foreach (int element in query) Console.Write (element + "|"); // 3|5|7|

Using Delegates for Performance

动态调用效率相对较低,开销通常在几微秒范围内。如果您在循环中重复调用方法,则可以通过调用以动态方法为目标的动态实例化委托,将每次调用的开销转移到纳秒范围内。在下一个示例中,我们在没有显著开销的情况下动态调用字符串的 Trim 方法一百万次:

MethodInfo trimMethod = typeof (string).GetMethod ("Trim", new Type[0]);
var trim = (StringToString) Delegate.CreateDelegate(typeof (StringToString), trimMethod);
for (int i = 0; i < 1000000; i++)
   trim ("test");
delegate string StringToString (string s);

这更快,因为代价高昂的后期绑定(以粗体显示)只发生一次。

Accessing Nonpublic Members

用于探测元数据的类型的所有方法(例如,GetProperty、GetField 等)都具有采用 BindingFlags 枚举的重载。此枚举用作元数据过滤器,并允许您更改默认选择标准。最常见的用途是检索非公共成员(这仅适用于桌面应用程序)。

例如,考虑下面的类:

class Walnut
		private bool cracked;
		public void Crack() { cracked = true; }
		public override string ToString() { return cracked.ToString(); }

我们可以按如下方式破解walnut:

Type t = typeof (Walnut);
Walnut w = new Walnut();
w.Crack();
FieldInfo f = t.GetField ("cracked", **BindingFlags.NonPublic | BindingFlags.Instance**);
f.SetValue (w, false);
Console.WriteLine (w); // False

使用反射访问非公共成员很强大,但也很危险,因为您可以绕过封装,对类型的内部实现产生难以管理的依赖。

The BindingFlags enum

BindingFlags 旨在按位组合。要获得任何匹配项,您需要从以下四种组合之一开始:

BindingFlags.Public    | BindingFlags.Instance
BindingFlags.Public    | BindingFlags.Static
BindingFlags.NonPublic | BindingFlags.Instance
BindingFlags.NonPublic | BindingFlags.Static

NonPublic 包括 internal , protected , protected internal , 和 private

以下示例检索类型对象的所有公共静态成员:

BindingFlags publicStatic = BindingFlags.Public | BindingFlags.Static;
MemberInfo[] members = typeof (object).GetMembers (publicStatic);

以下示例检索类型对象的所有非公共成员,包括静态成员和实例成员:

BindingFlags nonPublicBinding =
		BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
MemberInfo[] members = typeof (object).GetMembers (nonPublicBinding);

DeclaredOnly 标志排除从基类型继承的函数,除非它们被覆盖。

Generic Methods

您不能直接调用泛型方法;以下抛出异常:

class Program
		public static T Echo<T> (T x) { return x; }
		static void Main()
				MethodInfo echo = typeof (Program).GetMethod ("Echo");
				Console.WriteLine (echo.IsGenericMethodDefinition); // True
				echo.Invoke (null, new object[] { 123 } );          // **Exception**

需要一个额外的步骤,即在 MethodInfo 上调用 MakeGenericMethod,指定具体的泛型类型参数。这将返回另一个 MethodInfo,然后您可以调用它,如下所示:

MethodInfo echo = typeof (Program).GetMethod ("Echo");
MethodInfo intEcho = echo.MakeGenericMethod (typeof (int));
Console.WriteLine (intEcho.IsGenericMethodDefinition);          // False
Console.WriteLine (intEcho.Invoke (null, new object[] { 3 } )); // 3

匿名调用通用接口的成员 Anonymously Calling Members of a Generic Interface

当您需要调用泛型接口的成员并且直到运行时才知道类型参数时,反射很有用。从理论上讲,如果类型设计得很好,就很少需要这样做;当然,类型并不总是设计完美的。

例如,假设我们想要编写一个更强大的 ToString 版本,可以扩展 LINQ 查询的结果。我们可以这样开始:

public static string ToStringEx <T> (IEnumerable<T> sequence)

这已经相受限了。如果序列包含我们也想枚举的嵌套集合怎么办?我们需要重载方法来应对:

public static string ToStringEx <T> (IEnumerable<IEnumerable<T>> sequence)

然后如果序列包含分组或嵌套序列的投影怎么办?方法重载的静态解决方案变得不切实际——我们需要一种可以扩展以处理任意对象图的方法,例如:

public static string ToStringEx (object value)
		if (value == null) return "<null>";
		StringBuilder sb = new StringBuilder();
		if (value is List<>)                                            // Error
		    sb.Append ("List of " + ((List<>) value).Count + " items"); // Error
		if (value is IGrouping<,>)                                      // Error
		    sb.Append ("Group with key=" + ((IGrouping<,>) value).Key); // Error
		// Enumerate collection elements if this is a collection,
		// recursively calling ToStringEx()
		// ...
		return sb.ToString();

不幸的是,这不会编译:您不能调用未绑定泛型类型的成员,例如 List<>或 IGrouping<>。在 List<> 的情况下,我们可以通过使用非泛型 IList 接口来解决问题:

if (value is IList)
		sb.AppendLine ("A list with " + ((IList) value).Count + " items");

对于 IGrouping<,> 而言,解决方案并不那么简单。接口是这样定义的:

public interface IGrouping <TKey,TElement> : IEnumerable <TElement>, IEnumerable
		TKey Key { get; }

没有非泛型类型可以用来访问 Key 属性,所以这里我们必须使用反射。解决方案不是调用未绑定泛型类型的成员(这是不可能的),而是调用封闭泛型类型的成员,我们在运行时建立其类型参数。

第一步是确定值是否实现 IGrouping<,>,如果是,则获取其封闭的通用接口。我们可以通过执行 LINQ 查询最轻松地做到这一点。然后,我们检索并调用 Key 属性:

public static string ToStringEx (object value)
		if (value == null) return "<null>";
		if (value.GetType().IsPrimitive) return value.ToString();
		StringBuilder sb = new StringBuilder();
		if (value is IList)
				sb.Append ("List of " + ((IList)value).Count + " items: ");
		Type closedIGrouping = value.GetType().GetInterfaces()
				.Where (t => t.IsGenericType &&
					t.GetGenericTypeDefinition() == typeof (IGrouping<,>))
				.FirstOrDefault();
		if (closedIGrouping != null) // Call the Key property on IGrouping<,>
		    PropertyInfo pi = closedIGrouping.GetProperty ("Key");
		 object key = pi.GetValue (value, null);
		    sb.Append ("Group with key=" + key + ": ");
		if (value is IEnumerable)
		    foreach (object element in ((IEnumerable)value))
			sb.Append (ToStringEx (element) + " ");
		if (sb.Length == 0) sb.Append (value.ToString());
		return "\\r\\n" + sb.ToString();

这种方法很可靠:无论 IGrouping<,> 是隐式还是显式实现,它都有效。下面演示了这个方法:

Console.WriteLine (ToStringEx (new List<int> { 5, 6, 7 } ));
Console.WriteLine (ToStringEx ("xyyzzz".GroupBy (c => c) ));