前面一片随笔讲过用Delegate.CreateDelegate来提高多次反射效率的,使用代价较小的委托来代替反射的Invoke。
在Delegate.CreateDelegate方法对实例方法有一些默认的转换,例如:String.Trim()这个实例方法可以转换成下面两种委托:
string
delegate
TrimDelegate1();
string
delegate
TrimDelegate2(
string
str);
第一个委托是绑定到一个具体的字符串实例的,第二个委托是不绑定到一个具体的字符串实例的。
在CreateDelegate的时候也略有不同,创建第一个委托的实例需要用Delegate的public static Delegate CreateDelegate(Type type, object firstArgument, MethodInfo method);方法(或类似需要firstArgument的重载),而创建第二个委托的实例时,需要用Delegate的public static Delegate CreateDelegate(Type type, MethodInfo method);方法(或类似不需要firstArgument的重载)。
第二类委托比第一类委托更灵活,因为它们仅仅绑定到类,而不是实例,当需要变换实例时,仅仅只需要第一个参数传入另外一个实例就可以了,不需要重新绑定到对象(重新绑定需要比较高的开销,主要花费在BindToMethodInfo或BindToMethodName)。
换而言之,第二类委托反映了一个对象的实例方法的真实实现,将this当成函数的第一个参数传入函数体。(熟悉IL的应该可以立即想到实例方法的Ldarg_0就是this,而静态方法的Ldarg_0却是函数的第一个参数。)
但是,第二个委托却有一个问题存在,那就是对值类型无效。
可以做个试验,目标方法是Point的Offset(Point pt)方法,委托为下面两个委托:
void
delegate
OffsetDelegate1(Point pt);
void
delegate
OffsetDelegate2(Point @this, Point pt);
第一个委托可以创建出来,而第二个委托却创建不出来,无法正确的绑定到方法上。
可以发现一样的方式对值类型就无效了,为什么哪?
假想一下,JIT生成了一个本机代码的函数,这个函数的签名是第二个委托的签名,传入第一个Point,也就是this,再传入第二个Point也就是位移量,函数修改了第一个参数(也就是this)的值,然后返回。好,问题来了,第一个参数没有返回,对函数外而言,并不知道函数对第一个参数(也就是this)修改了,也不可能获得这个修改后的值,为什么,因为它是值类型,而且是参数是按值传递的,于是,函数对第一个参数的任何修改都仅仅是对函数体内那个参数的副本的修改,根本不会影响到外面的值。
如何来避免这个问题?很简单,值类型已经是一个无法修改的事实了,那么,就只能修改第一个参数的传递方式,也就是改成按引用传递。这样任何函数内部对值的修改都将是有效的。
换而言之,正确的第二类委托应该是:
void
delegate
OffsetDelegate3(
ref
Point @this, Point pt);
这个委托才可以被Delegate.CreateDelegate正确的绑定。
也就是说,值类型的实例方法也引用类型的实例方法的一个重要区别是:值类型的实例方法的this是按引用传递的,而引用类型的实例方法的this是按值传递的。
熟悉IL的话,可以想一下,Ldflda这个操作码,它获得的是一个字段的地址而不是一个字段的实际值,地址有什么用?如果这个字段是值类型,那就有用了,因为按引用传递传的是什么?不就是一个地址吗?完全没必要用Ldfld获得值,在变成引用,再调用函数,还要再用Stfld设置值。用一个Ldflda就把这些问题全搞定了,传过去就是本实例的某个字段的地址,被调用函数内部的修改也都是对这个字段的修改。