一、麻烦前的宁静:

“老赵,嗯,帮忙测试一下这个方法。”唉,同伴传过来一个托管dll文件。唉,真麻烦啊,为什么不用CVS呢?用个VSS也好啊。老赵一边抱怨着一边打开了VS.Net 2003。

测试嘛,总免不了使用NUnit。想到NUnit的艺术性,啧啧,老赵总要赞叹一番,看着“环保”的绿色就让人有一种心情愉快的感觉。

需要测试的代码如下,需要测试TestMethod( )方法 :
namespace TestAssembly
{
public class TestClass
{
public TestClass(){}
public double TestMethod( double param )
{
return param * 0.75;
}
}
}

在项目中引入dll文件,三下两下就写完了,测试代码如下:
[Test]
public void Test()
{
TestAssembly.TestClass tc = new TestAssembly.TestClass();
Assert.AreEqual( 0.75, tc.TestMethod(1.0) );
Assert.AreEqual( 7.5, tc.TestMethod(10.0) );
}

绿灯,通过,任务完成。

二、麻烦的开始:

“老赵,程序集更新了,再测试一下”。将旧的文件替换成新的,运行Unit,红灯。但是一看错误:“找不到方法:Double TestAssembly.TestClass.TestMethod(Double)”。怎么会这样?回到VS.Net 2003,Ctrl+Alt+B,果然,编译不通过:TestAssembly.TestClass”并不包含对“TestMethod”的定义。

忽然QQ上头像闪动:“差点忘了告诉你,方法名改成NewTestMethod了。” 那么还有什么好说的呢?修改测试代码呗。

QQ上又来消息了:“对了,方法名我随时会改,‘测试框架’留大一些啊。”老赵顿时感到一阵胸闷。这个所谓的“测试框架”是什么东西,怎么做?算了,老赵是个体谅同伴的好同志:“唉唉,那么你总要告诉我测试哪个方法吧。”

“以后我附带一个文本文件给你,里面写着测试哪个方法。” 不一会儿,又传来一个txt文件,打开一看,里面写着:“TestAssembly.TestClass.NewTestMethod”。老赵长叹一声:“没想到在测试时居然也会用到反射……”

不过还好,也不复杂,老赵开始动手修改代码: 
[Test]
public void Test()
{
Assembly asm = Assembly.LoadFrom("TestAssembly.dll");
//省略类名和方法名的获得代码
string className = "TestAssembly.TestClass";
string methodName = "NewTestMethod";
Type classType = asm.GetType(className, true);
object obj = classType.InvokeMember(null, 
BindingFlags.CreateInstance, null, null, null);
object result = classType.InvokeMember( methodName, 
BindingFlags.InvokeMethod, null, obj, new object[]{1} );
Assert.AreEqual( 0.75, (double)result );
}

运行NUnit,绿灯,放行。

三、麻烦无极限:

“这是我新写的一个方法,测试n遍吧,看看返回值是否在这个范围内。对了,方法名还是写在文本文
“n大概为多少?”

“越多越好,就几十万遍吧,嘿嘿。”

老赵根本没有多想,C&P,再随手改写了一点代码就完成了:
[Test]
public void Test()
{
//……
for (int i = 0; i < 500; i++)
{
for (int j = 0; j < 500; j++)
{
object result = classType.InvokeMember( methodName, 
BindingFlags.InvokeMethod, null, obj, new object[]{1} );
//……
}
}
}

打开NUnit,运行。嗯?怎么没反应?机器也处于濒死状态。打开“任务管理器”,“nunit-gui”占用了几乎所有的CPU。忽然想到了Reflection对于性能的影响。

老赵连忙在QQ上呼叫:“在吗?商量一下事情。”“什么事情?”

“我给你个类你派生一下,override它的virtual方法,用Reflection太慢”。

“什么Reflection……不懂。而且我这个类已经有超类了。”

“汗……那么我给你一个接口你实现一下如何?”

“那么我不是无法改方法名了?”

老赵忽然也意识到了这一点,匆匆告别同伴,寻找其它解决办法。

“唉,想偷懒一下的,还是没有办法,算了,只能这样了。”老赵自言自语。

方法其实也不难,只是需要写个delegate,然后把要运行的方法绑定上去即可。这么做和派生一个现成的类和实现一个现成接口相比要略微麻烦一些,而且执行速度也略慢一些。

“不管怎么样,动手吧。”老赵心想。

先定义一个委托,然后开始写代码:
delegate double MyDelegate(double d);
[Test]
public void Test()
{
Assembly asm = Assembly.LoadFrom("TestAssembly.dll");
//省略类名和方法名的获得代码
string className = "TestAssembly.TestClass";
string methodName = "NewTestMethod";
Type classType = asm.GetType(className, true);
object target = classType.InvokeMember(null, 
BindingFlags.CreateInstance, null, null, null);
object target = classType.InvokeMember( methodName, 
BindingFlags.InvokeMethod, null, obj, new object[]{1} );
MyDelegate run = (MyDelegate)Delegate.CreateDelegate( 
typeof(MyDelegate), target, methodName);
for (int i = 0; i < 500; i++)
{
for (int j = 0; j < 500; j++)
{
object result = run(1.0);
//……
}
}
}

结果如何?几乎是瞬间运行就结束了,其中性能差距何等明显!

老赵长吁一口气,真的没有料想到会遇到如此莫名其妙的测试方式。不知道以后还会怎么样,老赵的麻烦看来是少不了了……

四、麻烦后的总结:

.Net的反射机制相当强,虽然还不能完全得到所有的程序集信息,但是对于几乎所有的应用都足够了。反射机制的原理是从程序集的元数据中获取各种信息,元数据保存在程序集的各种“表格”里。反射机制对于编写动态加载的程序非常重要,比如插件程序。

在编译时就确定的成员访问来自然效率最高,通过反射机制来访问一个成员效率就低下了。就拿InvokeMember方法来讲,要确定访问的是哪个成员,首先要遍历整张表,并且要做大量的字符串匹配;其次,再传递参数时,我们会首先构造一个数组,初始化其中的元素,然后再交给反射方法调用。在调用时,反射机制还用从数组中提取参数压入堆栈。相反,在编译时就被调用的代码,在call一个方法时,会将实例的引用和第一个参数放在寄存器中,这样又减少的访存操作,运行效率自然就提高了。再次,反射机制在执行方法前必须还要检测参数的数量和类型是否正确。最后还有比较关键一点,并不是程序集中所有的成员都能被访问,CLR要通过检查安全许可来确定这次访问是否合法。以上四点导致了在反射机制中的效率低下。

因此,InvokeMember方法虽然强大,但是如果要频繁使用一个方法,就应该使用别的方法来访问成员。

要提高反射机制的效率,很自然想到的方法就是减少上述四项的使用。事实上,一些提高反射机制运行效率的技巧,就是在这一点上做文章。

下面就会介绍一写提高反射机制效率的方法,并将它们与直接运行和InvokeMember的效率进行量化的比较,直观地反映出这些技巧的重要性。

还是以原来的例子进行改编,添加一个接口和委托供使用,原来的例子代码如下:

namespace TestAssembly
{
public class TestClass: ITestInterface
{
public TestClass(){}
public double TestMethod( double param )
{
return param * 0.75;
}
}

public static void Main(string[] args)
{
int n = 1000;
Test1(n);//直接调用
Test2(n);//通过InvokeMember调用
Test3(n);//通过接口调用
Test4(n);//绑定至delegate
Console.In.ReadLine();
}

public interface ITestInterface
{
double TestMethod( double param );
}

public delegate double TestDelegate( double param );
}
首先,写代码测量直接运行的效率,代码如下:
static void Test1(int n)
{
TestClass tc = new TestClass();
DateTime startTime = DateTime.Now;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
tc.TestMethod( 1.0 );
TimeSpan ts = DateTime.Now - startTime;
Console.Out.WriteLine( "Test1: " + ts );
}
接着是通过InvokeMember调用,代码如下:
static void Test2(int n)
{
Type testType = typeof(TestClass);
object obj = testType.InvokeMember(null, 
BindingFlags.CreateInstance, null, null, null);
DateTime startTime = DateTime.Now;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
testType.InvokeMember( "TestMethod", 
BindingFlags.InvokeMethod, null, 
obj, new object[]{1.0} );
TimeSpan ts = DateTime.Now - startTime;
Console.Out.WriteLine( "Test2: " + ts );
然后,是将获得的object用接口来引用,然后调用方法,代码如下:
static void Test3(int n)
{
Type testType = typeof(TestClass);
object obj = testType.InvokeMember(null, 
BindingFlags.CreateInstance, null, null, null);
ITestInterface instance = (ITestInterface)obj;
DateTime startTime = DateTime.Now;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
instance.TestMethod(1.0);
TimeSpan ts = DateTime.Now - startTime;
Console.Out.WriteLine( "Test3: " + ts );
}
最后,绑定至一个delegate,代码如下:
static void Test4(int n)
{
Type testType = typeof(TestClass);
object obj = testType.InvokeMember(null, 
BindingFlags.CreateInstance, null, null, null);
TestDelegate testMethod = (TestDelegate)Delegate.CreateDelegate( 
typeof(TestDelegate), obj, "TestMethod" );
DateTime startTime = DateTime.Now;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
testMethod(1.0);
TimeSpan ts = DateTime.Now - startTime;
Console.Out.WriteLine( "Test4: " + ts );
}
以下是输出结果:
Test1: 00:00:00.0200288
Test2: 00:00:01.6824192
Test3: 00:00:00.0200288
Test4: 00:00:00.0300432
其时间的绝对值因为和机器性能相关,因此没有任何意义。

Test1和Test3的测试结果相同,可见这是使用反射机制的最好方式。另外,文章一开始提到的,让程序集派生于一个类,覆盖其虚方法,然后在程序中通过超类的引用来调用子类的方法。但是由于.Net不支持多继承,因此使用这个方法就带有一定的局限性。使用派生与接口和类的方法都是编写插件程序的常用方法,尤其是接口方式。当我们创建可扩展的应用程序时,接口应该处于中心位置。一般来说,应该创建一个程序集,在其中定义接口,接口即作为应用程序和插件之间的通信。然后保持这个程序集不变,而在自己的应用程序中则可以任意修改,因为根本不会影响到插件程序。

Test2的运行时间令人不敢恭维,在多次测试中均为直接运行的几十倍,可见InvokeMember方法并不适合大规模使用,而且由于其参数繁多,对于方法的使用也非常复杂。在n越大时,差距愈发明显。

Test4把方法绑定到了一个签名相同的委托上,这也是个比较出色的方法。虽然理论上这样的方法性能不如Test1,但是在测试中发现,n等于500时其效率还无法和Test1区分开来。事实上这个方法被广泛使用,往往配合定制属性会有非常令人信服的表现。

其实,Test4就使用了“一次绑定,多次执行”的方法。其实,如果要访问的并不是方法而是其余类型的成员,使用各Info类也是比较常用的办法。不过,绑定到这些类,更多的可能并不是为了执行,而是为了查看其元数据属性等其它目的。因为这样的绑定,和绑定到delegate相比起来,不是很彻底,在运行的效率上也不如delegate。