在利用 Type 类进行反射时,经常用到 GetMethod 和 GetProperty 反射方法与属性,或者使用 InvokeMember 直接调用类型成员。这些方法都具有一个 System.Reflection.Binder 类型的 binder 参数,而这个参数一般都是设置为 null 的,很少使用。
事实上,这个 binder 参数是很强大的,它可以几乎完全控制反射的工作方式(这里用几乎,是因为它受到了 RuntimeType 实现时的一些限制),只不过默认情况下使用的 System.DefaultBinder 类已经足够的使用了,因此不用太过于在意这个参数。
下面将会以我实现的 PowerBinder 类作为例子,解释 Binder 类到底是做什么的,以及如何实现自己的 Binder 类。PowerBinder 的实现与 DefaultBinder 的逻辑是基本相同的,区别在于添加了对泛型方法和强制类型转换的支持,同时进行了部分改进,下面给出一个与 DefaultBinder 对比的例子:
1 class TestClass 2 { 3 public static void TestMethod(int value) { } 4 public static void TestMethod2<T>(T value) { } 5 } 6 Type type = typeof(TestClass); 7 Console.WriteLine(type.GetMethod("TestMethod", new Type[] { typeof(long) })); 8 Console.WriteLine(type.GetMethod("TestMethod", BindingFlags.Static | BindingFlags.Public, PowerBinder.CastBinder, 9 new Type[] { typeof(long) }, null)); 10 Console.WriteLine(type.GetMethod("TestMethod2", new Type[] { typeof(string) })); 11 Console.WriteLine(type.GetMethod("TestMethod2", BindingFlags.Static | BindingFlags.Public, PowerBinder.DefaultBinder, 12 new Type[] { typeof(string) }, null));
这个例子是分别用 DefaultBinder 和 PowerBinder 反射获取 TestClass 类的方法,得到的结果如下所示:
1 null 2 Void TestMethod(Int32) 3 null 4 Void TestMethod2[String](System.String)
可以看到,有了泛型方法和强制类型转换的支持,在反射调用方法时会更加灵活方便,而且自定义 Binder 类的好处是很容易重用,而且能够使用 .Net 提供的相关接口。
首先来看 Binder 类是如何控制反射的工作方式的。它在 MSDN 中的解释是“从候选者列表中选择一个成员,并执行实参类型到形参类型的类型转换。”,也就是说在执行反射时,会由 Type 类选出一组可能的 MethodBase、PropertyInfo 或 FieldInfo,然后由 Binder 类来决定到底要使用哪个方法、属性或字段,或者干脆哪个都不选;而且实参到形参的类型转换(会在 Invoke 时使用)也是由 Binder 类来控制的,所以说它可以几乎完全控制反射的工作方式——唯一的不足就是候选者列表是由 Type 类提供的。
这里列出了 Binder 类需要重写的方法和简要的说明,每个方法的参数的具体解释可以参考 MSDN。
接下来就是详细解释 PowerBinder 是如何实现每个方法的。
先再次列出方法签名,以方便参考: FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, Object value, CultureInfo culture)。
这个方法其实很少被使用,仅当父类和子类定义了同名字段时才可能使用(否则根本不能定义同名的字段)。下面是 BindToField 方法的实现流程图,在这个流程图中也显示出了 RuntimeType 类为我们做的一些工作。
这个方法的实现还是很简单的,不过有些地方需要详细解释一下。
至于如何选择定义在子类中的字段,可以简单的按照 FieldInfo 被定义的深度来选择,深度比较深的就意味着是在子类中定义的。
.Net 4.0 中的 RuntimeType 类的实现有个小小的问题,现在假设类型 C 具有一个 string[] 类型的字段 F,当想通过 InvokeMemver 将 F[1] 设置为 "b" 时(一种很少见的用法,可能很多人都不知道),可以使用下面的代码(更多信息请参见 MSDN):
typeof(C).InvokeMember("F", BindingFlags.SetField, null, c, new Object[] {1, "b"}, null, null, null);
但是,此时 BindToField 方法的 value 参数得到的不是要设置的值 "b",也不是 string[] 类型的值,而是那个索引 1,这就导致 BindToField 是不可能通过类型选择合适的字段的(甚至可能选择错误)。下面就是一个例子:
1 class TestClass 2 { 3 public string[] TestField = new string[] { "XXX" }; 4 } 5 class TestSubClass 6 { 7 public new int TestField; 8 }
当调用
typeof(TestSubClass).InvokeMember("TestField", BindingFlags.SetField, null, new TestSubClass(), new object[] { 0, "XXX2" }, null, null, null);
时,就不能正确的反射到 TestClass.TestField,会抛出 ArgumentException。不过还好,这个问题几乎不可能遇到,即使真的出现这种问题,先获取字段对应的数组,再获取或设置数组指定索引的值就可以完美解决了。
下面是实现的代码:
其中用到的 FindMostSpecificType(Type type1, Type type2, Type type) 方法,是在两个类型 type1 和 type2 中,选择与 type 最接近的类型。例如在类型 short 和 int 中,与 long 最接近的显然是 int 类型,而与 sbyte 最接近的则是 short 类型。 具体的做法,就是判断 type1 和 type2 中哪个可以从 type 类型隐式转换而来,没有数据的丢失显然是更好的;如果都可以从 type 类型隐式转换而来,那么就选择 type1 和 type2 中更窄的那个(更接近 type);如果都不可以从 type 类型隐式转换,那么就选择更宽的那个,以减少数据丢失。
方法的签名为 MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref Object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] names, out Object state)。
这个方法是重写 Binder 类时最复杂的方法,它需要处理的情况非常多,包括参数名映射,可选参数、params 参数和泛型方法。我将 BindToMethod 方法的实现分成了下面的五个步骤。为了简便起见,这里与 System.DefaultBinder 一样不对 modifiers 参数进行处理(它一般都是用于 COM 组件的)。下面就是 BindToMethod 方法的实现流程图,虽然看起来不是很复杂,但其中的每一个步骤都需要做很多的工作。
names 参数允许参数不按顺序传入,所以首先要对 names 参数进行检查,要求 names 中不能有同名参数。在 DefaultBinder 并没有做这个检查,所以当存在同名参数时,会出现诡异的 IndexOutOfRangeException。
接下来根据 names 参数调整参数的位置,就是在方法的参数列表中寻找与 names 中的名称相同的参数,直到所有参数名称都被匹配(如果有未被匹配的参数名称,那么认为这个方法就不是想要的),在这里定义映射 map 来保存参数与 names 的匹配关系:如果 names[i] == params[j].Name,则 map[j] = i。
以方法
void TestMethod(int value1 = 11, int value2 = 22, int value3 = 33)
举例来说:
具体的实现为:
接下来就是对泛型方法的支持了,如果函数签名是 TestMethod<T>,这里需要将开放的泛型参数 T 替换为合适的类型,以得到相应的封闭泛型方法(例如 TestMethod<int>)。如果泛型参数 T 只对应一个参数 p,把 p 的类型作为 T 的类型即可。如果对应多个参数 p1, p2 ... pn,则要选择 pi 的类型,使得其他参数的类型都可以隐式转换为 pi 的类型。如果没有对应任何参数,那么显然是不能推导出类型实参的,直接返回。
如果泛型参数 T 对应着两个参数 pi 和 pj,其中 p1, p2 ... pn 都可以隐式转换为 pi 和 pj 的类型,那么泛型参数 T 的类型既可以选择 pi 的类型,也可以选择 pj 的类型,但到底使用哪个,程序是不能确定的,因此要求类型实参的推导必须是唯一的。
需要注意的是,这里使用的都是隐式类型转换,而不是显式类型转换,这是由于显示类型转换很容易导致找不到唯一的类型实参的推导,因此只遵循通常的原则。
然后就是根据参数类型进行过滤,即依次比较 params[i].ParameterType 是否可以从 args[map[i]] 的类型转换而来。而具体的比较又要分为三种情况分别讨论,
具体的实现代码如下:
通过上面的参数类型匹配,可能找到多个合适的方法,那么现在就需要在这些方法中,找到最合适的那个,其基本思想就是看哪个函数的签名与 args 的类型最为接近,实现起来跟 FindMostSpecificType 接近,只不过需要同时考虑多个类型。
如果参数类型同样接近,那么类型特化的方法总是优于泛型方法,子类定义的方法总是优于父类定义的方法(通过比较层级深度)。
由于参数的顺序需要根据 names 或默认参数进行调整,所以需要更改参数数组以匹配方法的签名,在这之前则需要保存旧的参数顺序,以用于之后的 ReorderArgumentArray 方法还原参数数组。这里为了简便起见,直接将参数数组复制一份(浅复制)保存,这样还原的时候直接替换就可以了。
对参数数组的调整首先要根据 names 调整顺序,接下来对默认参数和 params 参数进行处理,方式则类似于 2.2.3 中匹配参数类型,只不过是需要将多的参数包装为数组,或者将缺少的参数使用默认值补齐。实现的代码如下所示:
这个方法用于实现类型转换,它的逻辑需要和 BindToField 和 BindToMethod 相匹配,即如果 BindToXXX 方法只选择可以隐式类型转换的类型,那么 ChangeType 同样只需要处理隐式类型转换;如果 BindToXXX 方法对现实类型转换提供支持,ChangeType 也必须提供同样的支持。
System.DefaultBinder 只支持内置的隐式类型转换,所以直接抛出 NotSupportedException 就完成工作了。而我的 PowerBinder 支持完整的隐式类型转换和显式类型转换(包括对 Nullable<T>,枚举和自定义类型转换)的支持,因此实现起来会复杂很多,但其原理已经在之前的 C# 判断类型间能否隐式或强制类型转换中阐述了,所以这里就不再详细说明,可以自行看源代码:
这个方法是最简单的,实现的方式也在 2.2 节实现 BindToMethod 方法中说明了,这里直接略过。
这个方法也没有必要详细说明,因为它的实现已经完整包含在 BindToMethod 中了,只不过不必考虑 names 参数而已,可以认为是“2.2.2 处理泛型方法”,“2.2.3 根据参数类型筛选”和“2.2.4 进一步匹配方法”这三节的内容的组合。
不过,这里还是有些细节问题。在 DefaultBinder 中,SelectMethod 是不会考虑可选参数、params 参数和泛型方法的,我的 PowerBinder 决定要加入对他们的支持。但是在 RuntimeType 中,会对方法的参数数量进行初步筛选,如果没有设置 BindingFlags.InvokeMember、BindingFlags.CreateInstance、BindingFlags.GetProperty 或 BindingFlags.SetProperty 之一的话,或过滤掉所有参数数量不相等的方法。因此,如果希望使用 PowerBinder 得到可选参数或 params 参数,需要设置以上的四个标志之一才可以,当然,BindingFlags.OptionalParamBinding 也是不能忘记的。
相对于方法的选择,属性的选择简单了很多,只要考虑属性的类型和索引参数就可以了,可选参数、params 参数和泛型等复杂的东西全部与属性无关。其流程图如下所示:
其中用到的属性类型匹配类似于 BindToField 中的实现,索引参数的匹配则类似于方法参数的匹配,这里就不详细解释了。
以上就是 PowerBinder 的实现原理,从 Binder 类的实现过程中,也可以看到反射的效率为什么这么低下——需要由 RuntimeType 类选出特定类型的所有字段、属性或方法,然后根据名称进行第一次过滤,再由 Binder 类通过各种复杂的判断才能够得到反射的结果。
PowerBinder 类包含了两个静态属性:DefaultBinder 和 CastBinder,其中 DefaultBinder 不支持强制类型转换,CastBinder 则提供了对强制类型转换的支持,泛型方法和用户自定义类型转换是两个类支持的,所有源代码可见 PowerBinder.cs。
作者:CYJB
出处:http://www.cnblogs.com/cyjb/
GitHub:https://github.com/CYJB/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。