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) ));