C# 使用 Binder 类自定义反射

在利用 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 类介绍

首先来看 Binder 类是如何控制反射的工作方式的。它在 MSDN 中的解释是“从候选者列表中选择一个成员,并执行实参类型到形参类型的类型转换。”,也就是说在执行反射时,会由 Type 类选出一组可能的 MethodBase、PropertyInfo 或 FieldInfo,然后由 Binder 类来决定到底要使用哪个方法、属性或字段,或者干脆哪个都不选;而且实参到形参的类型转换(会在 Invoke 时使用)也是由 Binder 类来控制的,所以说它可以几乎完全控制反射的工作方式——唯一的不足就是候选者列表是由 Type 类提供的。

这里列出了 Binder 类需要重写的方法和简要的说明,每个方法的参数的具体解释可以参考 MSDN

  1. FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, Object value, CultureInfo culture) 方法:
    当利用 Type.InvokeMember 方法访问字段时,先由 InvokeMember 方法选出与 name 和 bindingFlags 匹配的字段,然后由 BindToField 选择与 value 最匹配的字段。
  2. MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref Object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] names, out Object state) 方法:
    当利用 Type.InvokeMember 方法调用方法或访问属性时,先由 InvokeMember 方法选出与 name 和 bindingFlags 匹配的方法(属性则使用相应的 SetMethod 或 GetMethod),然后由 BindToMethod 选择与 args 最匹配的方法。这个方法非常复杂,由于 InvokeMember 方法允许通过参数名称指定参数,因此参数的顺序和个数与方法的形参可能并不匹配,需要由 BindToMethod 方法将参数数组调整为正确的顺序,并且要求 ReorderArgumentArray 方法配合附加的 out state 参数,将被改变的参数数组顺序还原为被传入时的顺序。
  3. Object ChangeType(Object value, Type type, CultureInfo culture) 方法:
    这个方法用于在利用反射设置值时(例如 FieldInfo.SetValue 和 MethodBase.Invoke),对类型进行转换。MSDN 建议只进行扩宽强制,这样不会丢失数据,但也可以实现自己的逻辑。
  4. void ReorderArgumentArray(ref Object[] args, Object state) 方法:
    这个方法是与 BindToMethod 成对使用的,根据 BindToMethod 的 out state 参数,还原 args 的参数顺序。两个方法的实现也必须相对应。
  5. MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers) 方法:
    当利用 Type.GetMethod 反射方法时,先由 GetMethod 方法选出与 name、bindingFlags、callConvention 和参数数量匹配的方法,然后由 SelectMethod 选择与 types 最匹配的方法。
  6. PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers) 方法:
    当利用 Type.GetProperty 反射属性时,先由 GetProperty 方法选出与 name、bindingFlags 和参数数量匹配的属性,然后由 SelectProperty 选择与 returnType 和 indexes 最匹配的属性。

二、支持泛型方法和强制类型转换的 PowerBinder 类

接下来就是详细解释 PowerBinder 是如何实现每个方法的。

2.1 实现 BindToField 方法

先再次列出方法签名,以方便参考: FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, Object value, CultureInfo culture)。

这个方法其实很少被使用,仅当父类和子类定义了同名字段时才可能使用(否则根本不能定义同名的字段)。下面是 BindToField 方法的实现流程图,在这个流程图中也显示出了 RuntimeType 类为我们做的一些工作。

C# 使用 Binder 类自定义反射

这个方法的实现还是很简单的,不过有些地方需要详细解释一下。

  • 如果设置了 GetField 标志,RuntimeType 会将 value 设置为一个特殊的值 Empty.Value(这是一个内部类),所以这时候 value 是完全不可用的。
  • 如果设置了 SetField 标志而 value == null,这时只要求 FieldType 是引用类型即可,即任何可以接受 null 值的类型。
  • 如果按照 value 的类型筛选字段仍然得到多个可选字段,可以尝试从匹配的字段中找到最匹配的那个。例如有两个字段,其类型分别是 int 和 long,与 short 类型的值更匹配的显然是 int 而不是 long。这一匹配方式在后面也会多次用到。

至于如何选择定义在子类中的字段,可以简单的按照 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。不过还好,这个问题几乎不可能遇到,即使真的出现这种问题,先获取字段对应的数组,再获取或设置数组指定索引的值就可以完美解决了。

下面是实现的代码:

View Code

其中用到的 FindMostSpecificType(Type type1, Type type2, Type type) 方法,是在两个类型 type1 和 type2 中,选择与 type 最接近的类型。例如在类型 short 和 int 中,与 long 最接近的显然是 int 类型,而与 sbyte 最接近的则是 short 类型。 具体的做法,就是判断 type1 和 type2 中哪个可以从 type 类型隐式转换而来,没有数据的丢失显然是更好的;如果都可以从 type 类型隐式转换而来,那么就选择 type1 和 type2 中更窄的那个(更接近 type);如果都不可以从 type 类型隐式转换,那么就选择更宽的那个,以减少数据丢失。

2.2 实现 BindToMethod 方法

方法的签名为 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 方法的实现流程图,虽然看起来不是很复杂,但其中的每一个步骤都需要做很多的工作。

C# 使用 Binder 类自定义反射

2.2.1 处理 names 参数

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)

举例来说:

  1. 若 names = null, 则 map = {0, 1, 2},表示参数都是按顺序传递的。
  2. 若 names = {"value2", "value1", "value3"}, 则 map = {1, 0, 2}。
  3. 若 names = {"value3"}, 则 map = {1, 2, 0},即 names 的个数小于参数的个数时,剩余的参数会按顺序传递。

具体的实现为:

View Code

2.2.2 处理泛型方法

接下来就是对泛型方法的支持了,如果函数签名是 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 的类型,但到底使用哪个,程序是不能确定的,因此要求类型实参的推导必须是唯一的。

需要注意的是,这里使用的都是隐式类型转换,而不是显式类型转换,这是由于显示类型转换很容易导致找不到唯一的类型实参的推导,因此只遵循通常的原则。

2.2.3 根据参数类型筛选

然后就是根据参数类型进行过滤,即依次比较 params[i].ParameterType 是否可以从 args[map[i]] 的类型转换而来。而具体的比较又要分为三种情况分别讨论,

  1. params.Length > args.Length 这种情况意味着部分参数没有给出,因此要求没有给出值的参数(map[i] >= args.Length)具有默认值(DefaultValue != DBNull.Value),而且同时指定了 BindingFlags.OptionalParamBinding 标志。特别的,若最后一个参数是 params 参数,是没有默认值的,需要特殊处理一下。
    System.DefaultBinder 类在这里有个问题,就是要求默认参数总是在参数列表的末尾,即使是使用 names 更改参数顺序也不允许默认参数出现在参数列表的中间。拿之前定义的 TestMethod 举例来说,若传入 name = {"value2"}, args = {1},System.DefaultBinder 会抛出 IndexOutOfRangeException。在我实现的 PowerBinder 中,则允许默认参数出现在任意位置。
  2. params.Length < args.Length 这种情况意味着给出的参数多于方法的参数,即方法必须包含 params 参数,这时就需要检查 args 的额外参数值能否强制类型转换到 params 参数的基础类型。
  3. params.Length == args.Length 这就是最常见的情况,但是要根据最后一个参数是否是 params 参数进行额外的判断,因为参数值既可以是数组的一个元素,也可以是只包含一个元素的数组。

具体的实现代码如下:

View Code

2.2.4 进一步匹配方法

通过上面的参数类型匹配,可能找到多个合适的方法,那么现在就需要在这些方法中,找到最合适的那个,其基本思想就是看哪个函数的签名与 args 的类型最为接近,实现起来跟 FindMostSpecificType 接近,只不过需要同时考虑多个类型。

如果参数类型同样接近,那么类型特化的方法总是优于泛型方法,子类定义的方法总是优于父类定义的方法(通过比较层级深度)。

2.2.5 保存与调整参数顺序

由于参数的顺序需要根据 names 或默认参数进行调整,所以需要更改参数数组以匹配方法的签名,在这之前则需要保存旧的参数顺序,以用于之后的 ReorderArgumentArray 方法还原参数数组。这里为了简便起见,直接将参数数组复制一份(浅复制)保存,这样还原的时候直接替换就可以了。

对参数数组的调整首先要根据 names 调整顺序,接下来对默认参数和 params 参数进行处理,方式则类似于 2.2.3 中匹配参数类型,只不过是需要将多的参数包装为数组,或者将缺少的参数使用默认值补齐。实现的代码如下所示:

View Code

2.3 实现 ChangeType 方法

这个方法用于实现类型转换,它的逻辑需要和 BindToField 和 BindToMethod 相匹配,即如果 BindToXXX 方法只选择可以隐式类型转换的类型,那么 ChangeType 同样只需要处理隐式类型转换;如果 BindToXXX 方法对现实类型转换提供支持,ChangeType 也必须提供同样的支持。

System.DefaultBinder 只支持内置的隐式类型转换,所以直接抛出 NotSupportedException 就完成工作了。而我的 PowerBinder 支持完整的隐式类型转换和显式类型转换(包括对 Nullable<T>,枚举和自定义类型转换)的支持,因此实现起来会复杂很多,但其原理已经在之前的 C# 判断类型间能否隐式或强制类型转换中阐述了,所以这里就不再详细说明,可以自行看源代码:

View Code

2.4 实现 ReorderArgumentArray 方法

这个方法是最简单的,实现的方式也在 2.2 节实现 BindToMethod 方法中说明了,这里直接略过。

2.5 实现 SelectMethod 方法

这个方法也没有必要详细说明,因为它的实现已经完整包含在 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 也是不能忘记的。

2.6 实现 SelectProperty 方法

相对于方法的选择,属性的选择简单了很多,只要考虑属性的类型和索引参数就可以了,可选参数、params 参数和泛型等复杂的东西全部与属性无关。其流程图如下所示:

C# 使用 Binder 类自定义反射

其中用到的属性类型匹配类似于 BindToField 中的实现,索引参数的匹配则类似于方法参数的匹配,这里就不详细解释了。

以上就是 PowerBinder 的实现原理,从 Binder 类的实现过程中,也可以看到反射的效率为什么这么低下——需要由 RuntimeType 类选出特定类型的所有字段、属性或方法,然后根据名称进行第一次过滤,再由 Binder 类通过各种复杂的判断才能够得到反射的结果。

PowerBinder 类包含了两个静态属性:DefaultBinder 和 CastBinder,其中 DefaultBinder 不支持强制类型转换,CastBinder 则提供了对强制类型转换的支持,泛型方法和用户自定义类型转换是两个类支持的,所有源代码可见 PowerBinder.cs

作者:CYJB 
出处:http://www.cnblogs.com/cyjb/ 
GitHub:https://github.com/CYJB/ 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

 
分类:  .Net
标签:  类型转换Binder反射

你可能感兴趣的:(类型转换,反射,Binder)