反射类所使用的设计模式
System.Reflection 类中最常用的方法都使用统一的模式。Module、Type 和 MemberInfo 类的成员使用下表中所示的设计模式。
成员签名
说明
MyInstance[] FindXxx(filter, filterCriteria)
查找并返回经过筛选的类型列表,或者在当前类型没有实现任何匹配筛选器的类型的情况下返回空数组。
例如:Type.FindInterfaces
MyInstance GetXxx(<parameters>)
返回由 <parameters> 唯一指定的类型。如果不存在这样的类型,成员将返回 null(在 Microsoft Visual Basic .NET 中为 Nothing)。请注意,<parameters> 唯一地指定一个实例。
例如:Type.GetInterface
MyInstance[] GetXxxs()
返回所有公共类型。如果不存在公共类型,成员将返回空数组。
例如:Type.GetFields
MyInstance[] GetXxxs(<parameters>)
返回由 <parameters> 指定的所有类型。如果不存在这样的类型,成员将返回空数组。请注意,<parameters> 并不一定指定唯一的实例。
另一个常用的设计模式是使用委托。它们通常在反射中用来支持对返回对象数组的方法的结果集进行筛选。
反射的安全注意事项
如果提供对非公共信息的访问,将带来安全风险。如果允许代码了解某类型的非公共信息,那么该代码就有可能会访问您需要保密的代码、数据和其他信息。因此,.NET 框架安全性强制实施了一些规则,以确定可以使用哪种程度的反射来了解类型信息和访问类型。根据所执行的操作,可能会需要用于序列化的 ReflectionPermission 或 SecurityPermission。
在未经许可的情况下,所有代码都可以使用反射来执行以下任务:
获取有关公共类型及其公共成员的信息。
了解代码所在的模块和程序集。
枚举公共类型。
枚举与使用反射的代码位于同一程序集中的非公共类型。
枚举程序集和模块。
调用公共成员。
对调用代码基类的族访问成员进行调用。
对调用代码程序集的程序集访问成员进行调用。
若要了解有关非公共成员的信息,调用方必须具有 ReflectionPermission,此权限表示可以获取类型信息。如果不具有此权限,代码将无法使用反射通过 Type、Assembly 和 Module 上的 Get 方法来获取有关非公共成员(即使属于它自己的类)的信息。
要使用反射来调用无法按照通用类型系统可访问性规则访问的方法或访问这样的字段,必须向代码赋予成员访问的 ReflectionPermission。
注意 建议使安全策略拒绝向源自 Internet 的代码授予 ReflectionPermission。
序列化的 SecurityPermission 提供了获取和设置可序列化类型的任何非瞬态数据字段(即不仅存在于内存中的成员)的能力,而不论是否可以访问这些字段。此权限使代码能够了解并更改实例的私有状态。(除了授予正确的权限之外,还必须在元数据中将类型标记为可序列化。)
链接请求检查
如果方法或委托对某一 P 权限具有 LinkDemand,运行库将对该方法或委托的调用方执行链接请求检查,以验证已经向调用方授予 P 权限。在了解类型信息和进行调用时,都会执行此链接请求检查。
应避免编写采用 MethodInfo 参数的公共 API,尤其是对于高度信任的代码。否则,调用权限可能会很容易被恶意代码绕过。例如,设想在高度信任的代码中有一个采用 MethodInfo 参数的公共 API。假设此公共 API 对所提供的参数间接地调用 MethodInfo.Invoke。如果公共 API 没有执行必要的权限检查,由于安全性系统断定调用方受到高度信任,对 Invoke 方法的调用将始终会成功。即使恶意代码无权直接调用该方法,它仍然能够通过调用公共 API 来间接地调用该方法。
动态加载和使用类型
反射提供了由语言编译器(例如 Microsoft Visual Basic .NET 和 JScript)用来实现隐式晚期绑定的基础结构。绑定是查找与唯一指定的类型相对应的声明(即实现)的过程。由于此过程在运行时而不是在编译时发生,所以称作晚期绑定。Visual Basic .NET 允许您在代码中使用隐式的晚期绑定;Visual Basic 编译器将调用一个帮助器方法,该方法使用反射来获取对象类型。传递给帮助器方法的参数有助于在运行时调用正确的方法。这些参数包括对其调用方法的实例(对象)、被调用方法的名称(字符串)和传递给被调用方法的参数(对象数组)。
在以下代码示例中,Visual Basic 编译器使用反射隐式地对其类型在编译时未知的对象调用方法。HelloWorld 类具有一个 PrintHello 方法,它输出与传递给 PrintHello 方法的某些文本串联的“Hello World”。在该示例中调用的 PrintHello 方法实际上是 Type.InvokeMember;Visual Basic 代码允许按照对象 (helloObj) 的类型在编译时已知(早期绑定)而不是在运行时已知(晚期绑定)的方式来调用 PrintHello 方法。
自定义绑定
除了由编译器隐式地用来进行晚期绑定之外,反射还可以在代码中显式地用来完成晚期绑定。
公共语言运行库支持多种编程语言,但这些语言的绑定规则各不相同。在早期绑定的情况下,代码生成器可以完全控制此绑定。但是,当通过反射进行晚期绑定时,必须用自定义绑定来控制绑定。Binder 类提供了对成员选择和调用的自定义控制。
利用自定义绑定,您可以在运行时加载程序集,获取有关该程序集中类型的信息,然后对该类型调用方法或访问该类型的字段或属性。如果您在编译时(例如当对象类型依赖于用户输入时)不知道对象的类型,就可以使用这种方法。以下代码示例显示在 HelloWorld.dll 程序集中使用反射动态调用的方法(首先在 Visual Basic .NET 中,然后在 C# 中)。
[C#]
// This class is deployed as an assembly consisting of one DLL,
// called HelloWorld.dll.
using System;
public class HelloWorld {
// Constant Hello World string.
private const String m_helloWorld = "Hello World";
// Default public constructor.
public HelloWorld() {
}
// Print "Hello World" plus the passed text.
public void PrintHello(String txt) {
// Output to the Console.
Console.WriteLine(m_helloWorld + " " + txt);
}
}
// Illustrates reflection's late binding functionality.
// Calls the PrintHello method on a dynamically loaded
// and created instance of the HelloWorld class.
using System;
using System.Reflection;
public class CSharpLateHello {
public static void Main() {
// Load the assembly to use.
Assembly assem = Assembly.Load("HelloWorld");
// Get the type to use from the assembly.
Type helloType = assem.GetType("HelloWorld");
// Get the method to call from the type.
MethodInfo printMethod = helloType.GetMethod("PrintHello");
// Create an instance of the HelloWorld class.
Object obj = Activator.CreateInstance(helloType);
// Create the args array.
Object[] args = new Object[1];
// Set the arguments.
args[0] = "From CSharp Late Bound";
// Invoke the PrintHello method.
printMethod.Invoke(obj, args);
}
}
InvokeMember 和 CreateInstance
使用 Type.InvokeMember 可调用类型的成员。各个类(如 System.Activator 和 System.Reflection.Assembly)的 CreateInstance 方法是特殊形式的 InvokeMember,它们可新建特定类型的实例。Binder 类用于在这些方法中进行重载决策和参数强制。
以下代码示例显示参数强制(类型强制)和成员选择三种可能的组合。在第 1 种情况中,不需要任何参数强制或成员选择。在第 2 种情况中,只需要成员选择。在第 3 种情况中,只需要参数强制。
[C#]
public class CustomBinderDriver
{
public static void Main (string[] arguments)
{
Type t = typeof (CustomBinderDriver);
CustomBinder binder = new CustomBinder();
BindingFlags flags = BindingFlags.InvokeMethod|BindingFlags.Instance|
BindingFlags.Public|BindingFlags.Static;
//Case 1. Neither argument coercion nor member selection is needed.
args = new Object[] {};
t.InvokeMember ("PrintBob", flags, binder, null, args);
//Case 2. Only member selection is needed.
args = new Object[] {42};
t.InvokeMember ("PrintValue", flags, binder, null, args);
//Case 3. Only argument coercion is needed.
args = new Object[] {"5.5"};
t.InvokeMember ("PrintNumber", flags, binder, null, args);
}
public static void PrintBob ()
{
Console.WriteLine ("PrintBob");
}
public static void PrintValue (long value)
{
Console.WriteLine ("PrintValue ({0})", value);
}
public static void PrintValue (String value)
{
Console.WriteLine ("PrintValue\"{0}\")", value);
}
public static void PrintNumber (double value)
{
Console.WriteLine ("PrintNumber ({0})", value);
}
}
当多个成员具有相同的名称时,将需要重载决策。Binder.BindToMethod 和 Binder.BindToField 方法用于解析与单个成员的绑定。Binder.BindToMethod 还通过 get 和 set 属性访问器提供了属性解析。
BindToMethod 返回要调用的 MethodBase,如果无法进行这样的调用,则返回 null。虽然 MethodBase 返回值通常是 match 参数中所包含的值之一,但它并不必如此。
当存在 ByRef 参数时,调用方可能需要取回这些参数。因此,如果 BindToMethod 已经操作参数数组,Binder 会允许将参数数组映射回它的初始形式。为了实现这一目的,必须向调用方保证参数的顺序不会改变。当按名称传递参数时,联编程序将重新排列参数数组,这就是调用方所见的参数。
可用成员集包括在类型和任何基类型中定义的成员。如果指定 BindingFlags.NonPublic,将返回该成员集中具有任何可访问性的成员。如果未指定 BindingFlags.NonPublic,联编程序就必须强制可访问性规则。当指定 Public 或 NonPublic 绑定标志后,还必须指定 Instance 或 Static 绑定标志,否则不会返回任何成员。
如果只有一个成员具有给定名称,则不必进行回调,而在该方法上进行绑定。代码示例的第 1 种情况说明了这一点:只有一个 PrintBob 方法可用,因此不需要进行回调。
如果可用集中有多个成员,所有这些方法都将传递给 BindToMethod,它将选择正确的方法并将其返回。在代码示例的第 2 种情况下,有两个名为 PrintValue 的方法。对 BindToMethod 的调用将选择正确的方法。
ChangeType 执行参数强制(类型强制),以便将实参转换为选定方法的形参的类型。即使类型精确匹配,仍会为每个参数调用 ChangeType。
在代码示例的第 3 种情况下,类型为 String 值为“5.5”的实参传递给 Double 类型的形参的方法。要使调用成功,必须将字符串值“5.5”转换为 double 值。ChangeType 会执行此转换。
ChangeType 仅执行无损或扩大转换,如下表所示。
源类型
目标类型
任何类型
它的基类型
任何类型
它所实现的接口
Char
UInt16、UInt32、Int32、UInt64、Int64、Single、Double
Byte
Char、UInt16、Int16、UInt32、Int32、UInt64、Int64、Single、Double
SByte
Int16、Int32、Int64、Single、Double
UInt16
UInt32、Int32、UInt64、Int64、Single、Double
Int16
Int32、Int64、Single、Double
UInt32
UInt64、Int64、Single、Double
Int32
Int64、Single、Double
UInt64
Single、Double
Int64
Single、Double
Single
Double
非引用类型
引用类型
Type 类具有 Get 方法,这些方法使用 Binder 类型的参数来解析对特定成员的引用。Type.GetConstructor、Type.GetMethod 和 Type.GetProperty 通过提供成员的签名信息来搜索当前类型的特定成员。Binder.SelectMethod 和 Binder.SelectProperty 则被回调来选择相应方法的给定签名信息。
访问默认成员
任何类型都可以具有默认成员,即在未给定任何成员名称时调用的成员。以下代码示例调用 Class1 的默认成员,并将它返回的值赋给 i。
默认成员用 System.Reflection.DefaultMemberAttribute 进行标记。以下代码示例显示如何通过检索默认成员的自定义属性来检索默认成员。
[C#]
Type t = typeof(DefaultMemberAttribute);
DefaultMemberAttribute defMem = (DefaultMemberAttribute)Attribute.GetCustomAttribute(Assembly.GetAssembly(t), t);
MemberInfo[] memInfo = t.GetMember(defMem.MemberName);
使用 Type.GetDefaultMembers 可能会简单一些,并且会生成完全相同的结果。但是,如果在类型上定义了多个默认成员,GetDefaultMembers 就会引发 InvalidOperationException。以下代码示例显示 GetDefaultMembers 的语法。
[C#]
MemberInfo[] memInfo = t.GetDefaultMembers();
通过以 String.Empty ("") 为成员名称来调用 Type.InvokeMember,就可以调用默认成员。InvokeMember 将从类型中检索 DefaultMemberAttribute,然后调用它。
访问默认参数值
某些语言(如 C++ 托管扩展和 Microsoft Visual Basic .NET)支持将默认值赋给参数。例如,以下代码示例是一个合法的 Visual Basic .NET 声明,此声明将默认值赋给两个参数。
您可以使用参数属性来分配默认的参数值。
通过确切指定哪些参数是默认值或略去尾部的默认参数,可以声明参数的默认值。例如,以下所有代码示例都是对 MyMethod 的有效调用。
[C#]
MyMethod (10, 55.3, 12);
MyMethod (10, 1.3); // c == 1
MyMethod (11); // b == 1.2, c == 1
要使用反射检索参数的默认值,请获取该参数的 ParameterInfo 对象,然后使用 ParameterInfo.DefaultValue 属性检索默认值。如果不存在默认值,该属性将返回 Value.DBNull。
以下代码示例向控制台显示 MyMethod 的默认值。
[C#]
MethodInfo m = t.GetMethod ("MyMethod");
ParameterInfo[] ps = m.GetParameters();
for (int i = 0; i < ps.Length; i++) {
Console.WriteLine("Default Value == {0}", ps[i].DefaultValue);
}
要调用包含具有默认值的参数的方法,请使用 Type.Missing 作为 InvokeMember 方法的参数值。这样,晚期绑定服务就能够为指定的参数值使用默认值。如果为不带默认值的参数传递 Type.Missing,则将引发 ArgumentException。有一点务必要注意,并非所有编译器的绑定机制都会遵守 Type.Missing 的这些规则。有些联编程序可能不支持此功能,或者可能以不同的方式来处理 Type.Missing。当使用 Type.Missing 时,默认值不必是结尾的参数值。
C# 语言不支持默认参数。
以下 Visual Basic .NET 代码示例显示如何调用具有默认参数的方法。
[Visual Basic]
Option Strict Off
Imports System
Imports System.Reflection
Public Class OptionalArg
Public Sub MyMethod (a As Integer, Optional b As Double = 1.2, Optional c As Integer=1)
Console.WriteLine("a = " & a & " b = " & b & " c = " & c)
End Sub
End Class
Module Module1
Sub Main()
Dim o As New OptionalArg
Dim t As Type
t = GetType(OptionalArg)
Dim Param As Object()= {10, 20, 30}
t.InvokeMember("MyMethod", _
BindingFlags.Public Or _
BindingFlags.Instance Or _
BindingFlags.InvokeMethod Or _
BindingFlags.OptionalParamBinding, _
Nothing, _
o, _
New Object() {10, 55.3, 12})
t.InvokeMember("MyMethod", _
BindingFlags.Public Or _
BindingFlags.Instance Or _
BindingFlags.InvokeMethod Or _
BindingFlags.OptionalParamBinding, _
Nothing, _
o, _
New Object() {10, 1.3, Type.Missing})
t.InvokeMember("MyMethod", _
BindingFlags.Public Or _
BindingFlags.Instance Or _
BindingFlags.InvokeMethod Or _
BindingFlags.OptionalParamBinding, _
Nothing, _
o, _
New Object() {10, Type.Missing, Type.Missing})
End Sub
End Module
当使用上述方法时,即使调用方未指定任何值,仍会考虑尾部的默认参数。这是调用具有默认参数的方法时最常用的方式。
如果是使用 MethodBase.Invoke 来调用方法,则需要显式指定哪些参数是默认值,指定的方法是为所有没有值的参数传递一个包含 Type.Missing 的对象数组。