工作五年的.NET程序员再谈应用反射,动态编译,代码生成器
工作多年,一直是.NET实践派的代表,没有过多的空洞理论,只谈实作技巧,把技术,知识,工具结合起来,给自己的生活多一点改变,提高产生效率。前面一个话题,《工作多年后才明白的.NET底层开发技术》都觉得反射的例子太过于肤浅,不过瘾,那么这个题材的反射例子,可能不会让你失望。
这个主题来源于我开发带智能提示的模板编辑器的过程,请看下面的例子程序。先说目的,我要达到在模板中敲入Math变量时,可以智能的显示它的成员列表,也就是做一个带智能提示的模板编辑器。
模板的语法定义如下
<%@ Property Name="Math" Type="MathProgram" Category="Text"Description="Namespace for this class" %>
<%@ Property Name="IncludeDelete" Type="System.Int32" Default="123" Category="Options" Description="If true delete statements will be generated." %>
<%@ Assembly Name="TestClassLibrary" %>
<%@ Import Namespace="EPN.Common" %>
如果你不熟悉这种语法,可以参考Code Smith的入门教程,也可以参考我写的文章《ORM框架-工具-产品开发之四 开发代码生成器 Template Studio Development 》,有二篇。
用简洁的语言来解释这段代码的意思,定义一个变量MathProgram类型的变量Math,它定义于TestClassLibrary程序集的EPN.Common命名空间中。类型MathProgram的代码定义如下
[TypeConverter(typeof(ExpandConverter))]
public class MathProgram
{
public MathProgram(string system,string application)
{
_SystemName = system;
_ApplictionName = application;
}
public MathProgram(){}
public int Add(int a,int b)
{
return a + b;
}
public int Substract(int a, int b)
{
return a - b;
}
public static string Product = "Template Studio";
private string _SystemName;
[Browsable(true)] [Category("Text")] [DefaultValue("")][Description("Namespace for this class")]
public string SystemName
{
get { return _SystemName; }
set { _SystemName = value; }
}
private string _ApplictionName;
[Browsable(true)][Category("Text")][DefaultValue("")] [Description("Namespace for this class")]
public string ApplictionName
{
get { return _ApplictionName; }
set { _ApplictionName = value; }
}
}
我要达到的效果是,在模板中敲入Math变量时,可以智能的显示它的成员列表。
为了表示这项技术的可行性,我来展示一下,在Code Smith中达到的效果
这项技术是Code Smith的卖点之一,带智能提示的好用的模板编辑器。
到此,你可能对这项技术开始有此好奇了,它是如何做到的,我是如何实现这个功能的,下面讲解它的原理和实现代码。
.NET编辑器,对点号.非常的在意,C++则同时有三个符号.,->,::都可以表示成员调用。当敲入点符号后,编辑器会启动分析程序,对点号前面的token进行分析,找到它的类型。如果是类型定义,则会列出它的成员,这是Visual Studio很平常但又很重要的功能。这个功能已经有很多编辑器实现了,直接调用即可。
但是,到了模板编辑器这里,又有些特殊,因为我们平时是这样来写代码的
MathProgram Math=new MathProgram();
Math.SystemName ; 这个SystemName变量可以根据智能提示显示出来
到了模板编辑器里面,代码定义是这样的
<%@ Property Name="Math" Type="MathProgram" Category="Text"Description="Namespace for this class" %>
这句话,也等同于代码片段MathProgram Math=new MathProgram();
当用户敲完了Property 的%>之后,分析器在后台应该加上了代码片段MathProgram Math=new MathProgram();
.NET 2.0后支持partial关键字,允许把类型的定义放在多个源代码文件中,编译时自动合并代码。我将模板中的模板内容,解析成一个C#代码文件,类型名字就取自模板名。这样,每次模板有变化,后台进程会重新分析模板的内容,重新生成C#代码文件,以用于成员的定义。似乎智能提示的问题到此应该可以解决了。
可是不行,达不到目的,C#中不允许有全局变量,也就是在一个地方定义,在另一个地方调用。我可以在后台C#代码中定义一个变量,但是在模板文件中取不到这个变量,.NET是OOP的语言,不允许全局有变量。
再来设想另一种情况,当我们敲入一个类型名,再敲点号,它会立即显示它的static成员,这个特性帮忙我解决了问题。
再来回顾一下模板的属性定义语法,类型MathProgram,变量名称是Math
<%@ Property Name="Math" Type="MathProgram" Category="Text"Description="Namespace for this class" %>
当输入完这段语法后,我在后台构造一个Math类型,而不是变量(因为C#不允许全局变量), 当用户在模板文件中敲入Math属性时,它会取自我在后台定义的类型,至于要显示它的成员,那很简单,把MathProgram中的成员,追加到我的后台定义类型Math中即可,并且加上static属性,这样,我在敲模板属性Math之后的点,就相当于在调用我后台生成的Math类型,它的方法做取自MathProgram的instance方法和属性。
问题到了这里,需要应用反射。我需要根据程序集TestClassLibrary中的类型EPN.Common.MathProgram来构造它的源码,以用于我的编辑器智能提示。根据程序集来构造它的源代码,代码看起来是这样的
string nameSpace = "EPN.Common";
string typeName = "MathProgram";
string fullName = nameSpace + "." + typeName;
string instanceName = "Math";
string assemblyName=”TestClassLibrary”;
string source = CodeDomHelper.BuildSpecifiedTypeSourceCode(assemblyName,fullName, instanceName);
读到这里,你可能会明白Code Smith的模板语法的Property 的含义了。我没有机会读Code Smith的源代码,但是我这几行代码足够帮忙你深刻领悟它的含义,知其所以然。
再来看如何根据程序集,产生它的源代码。我不是指用Reflector这样的工具来产生源代码,在我的Template Studio中,根本没有机会这样做,用户不可能帮忙你反编译程序集,给出源代码,你再来显示它的成员列表。
第一直觉印象就是command line调用Reflector程序,让它帮忙反编译成源代码,我想到可能是这样的
cmd->Reflector “TestLibrary” "EPN.Common.MathProgram” "C:\DNA\Math.cs”
用语言来表达是,我希望传入程序集,需要反编译的类型,然后到指定的文件中取它的源代码。
可惜,暂时Reflector做不到这样,官方已经把这作为一个feature,但是什么时候实现不得而知。
另一个很不错的反编译器ILSpy,开放源码,但是也没有看到有command line调用的例子,又失败。
第三个想法是,写Reflector的plug-in,已经有Reflector.FileDisassembler.dll,通过这个组件,你可以一次性的把一个程序集导出为.NET源代码,Reflector7内置了这个组件,请看图
于是,我想到查看Reflector的Export Assembly Source Code菜单命令的源代码,可是你知道的,用Reflector读它自己的源代码,这不可行。人家把工具卖给你是让你对付别人的程序集的,而不是用在它自己身上。
真到了这一步,你会发现互联网的资料真是不多,这些资料,永远都不会公开的,看来还是只有自己靠自己,要么变通一下,要么放弃这个feature的实现。
再来继续看问题,我的智能编辑器需要的是EPN.Common.MathProgram类型的源代码,但也不需要它的完整源代码,只拿到代码的签名就可以了,具体来说,属性定义,方法只需要签名,就达到目的了。因为我只用它来作为智能提示的内容,不需要执行它,也就不需要代码内容,body也可忽略。所以,我需要下面的这段代码
public class Math
{
public static int Add(int a,int b)
{
return 2012;
}
public static int Substract(int a, int b)
{
return 2012;
}
public static string SystemName
{
get { return “2012”; }
set { }
}
public static string ApplictionName
{
get { “2012”; }
set { }
}
}
看到在代码中,我把属性生成为static类型的,方法体都是空,当你在Visual Studio的代码编辑器中敲入Math类型时,它会智能的列出以上的成员,因为这里都是static型的。再对比一下模板的定义,我们把模板中的属性名称Math也当成了这里的Math类型,也就达到Code Smith中,敲入变量Math后,带有智能提示的效果。
再来分析代码生成技术,我想,你肯定对这段代码感兴趣了
string source = CodeDomHelper.BuildSpecifiedTypeSourceCode(assemblyName,fullName, instanceName);
是的,这就是代码生成技术,根据程序集生成它的源代码。看名字CodeDom,可能已经猜到了它的实现。
我确实是用CodeDom技术来实现的,因为要同时支持VB和C#两种代码,CodeDom是最好的技术选择。这样,我的模板才可以同时,支持两种语法C#和VB语法,后台的CodeDom帮忙我生成VB和C#两种语法的源代码文件,以用于智能提示。其实,我一开始没有考虑用CodeDom做代码生成的工具,因为它的语法有些难看,不容易理解,我一开始用C#拼凑字符串,生成C#的源代码文件,然后调用下面的工具,直接转化成VB的语法
Code Conversion是以前写的一个C#与VB代码相互转换的工具,因为有源代码,所以用代码转换C#到VB和用Code Conversion工具转换代码没有区别。辛苦的启动这个软件,单击了几个菜单,我用代码调用,一下子就把C#代码全部做转化成了VB代码,这就是代码的魔力。
回顾这个过程,C#代码,编译成程序集,添加到模板中应用;模板编辑器后台程序分析程序集的类型定义,生成它的源代码骨架,用于智能提示窗体的成员列表显示;为了支持VB和C#两种语法,生成代码的方式选择CodeDom技术。
总结:我承认我隐藏了很多实现的细节,既然我已经做出了这个效果,我可以自信的说,它应用了大量的反射,动态编译和CodeDom代码生技术。写文章的思路是断开的,要分重点的讲一下各个实现要点,但又要能合起来,看成一个整体,起承转合,上下衔接,这样才可以透彻的理解。我现在理解你迫切需要那几个C#文件,来把这篇文章的代码和思路连接成整体。等《ORM框架-工具-产品开发》系列的文章陆续发布后,可能有机会看到这些代码的实现。