您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 
 订阅
使用C# 2.0实现语言无关的代码生成器
 
 
  780  次浏览      16 次
 2023-2-3
 
编辑推荐:
本文主要讲解了如何使用C# 2.0实现语言无关的代码生成器。
本文来自于Mikel,由火龙果Linda编辑推荐。

一、什么是CodeDom?

现在的程序规模越来越大,虽然在计算发展的几十年间,产生了许多快捷、高效的编程语言和开发工具,如C#、Visual Studio、java等。也产生了许多用以辅助软件设计、开发的思想和方法,如UML、OOP、Agile等。尽管利用这些技术和方法可以大大提高程序 编写的效率,但是仍可能有重复的编码工作。因此,现在出现了许多可以自动产生源代码或者目标文件的软件,即Code Wizards。

一 般这些Code Wizards在生成源代码时都是通过设置模板文件,然后根据这些模板文件生成源代码。有很多Code Wizards只能生成固定的语言(如java、C#等)。虽然有一些Code Wizards可以生成多种语言,但也只是固定的几种。而且生成源代码部分都是显示地固定在程序中。这样非常不易扩展。如CodeSmith系统,这是一 个非常不错的Code Wizard。它使用一个扩展名为cst的文件来设置模板。这个模板文件的格式类似于ASP.NET。如果想生成C#源代码,必须要在其中显示地标明,并 且模板的固定部分要使用C#语言编写。如果这样的话,同样功能要生成不同语言的代码,如C#和VB.net。就要编写两个模板文件。这是非常不方便的。

从 以上的描述来看, Code Wizard所面临的一个重要问题就是如何使用一个模版文件来生成不同语言的源代码。幸好Microsoft提供了一种解决方案,这就是CodeDOM技 术。CodeDOM的全称是代码文档对象模型(Code Document Object Model)。整个CodeDOM就是一张对象图(object graph)。它用这张图中的所有对象描述了面向对象语言中的几乎所有的语法现象,如类、接口、方法、属性等。CodeDOM通过对象模型对语言进行了抽象,然后利用具体语言所提供的生成源代码的机制来生成源代码,并可调用相应的编译器将源码生成*.dll或*.exe。从而可以达到与语言无关的目的。图1描述了使用CodeDOM生成和编译源代码的过程。

图1 CodeDom生成和编译源代码的过程

从上图可以看出,CodeWizard只使用CodeDOM对语言进行抽象,然后通过CodeDomProvider生成源代码。最后通过编译器生成中间语言。下面将详细讨论如何利用CodeDOM来实现CodeWizard。

二、实现CodeWizard

下 面要实现的这个CodeWizard非常简单。其功能主要是将一个数据表映射成一个类。这个类提供了Add和Save方法以及和数据表的每个字段相对应的 属性。使用这个类可以向数据表添加记录。为了便于描述,将这个数据表保存成xml文件格式。每条记录为一个item结点,每一个字段为这个结点的一个属 性。表名为这个xml文件的根结点名称。这个xml文件的格式如下所示:

<MyTable>

<item id = "01" name = "Bill"/>

<item id = "02" name = "Mike" />

</MyTable>

这个CodeWizard通过一个模板文件来定义数据表的结构。模板文件的格式如下:

<MyTable>

<id type = "System.Int32"/>

<name type = "System.String"/>

</MyTable>

其 中type为字段的类型,它的值是在.net framework中的System中定义的简单类型,如System.Int32、System.String、System.Char等。下面就详细 讨论如何利用这个模板文件和CodeDOM技术来生成C#和VB.net的源代码。

CodeDOM的结构

CodeDOM由两部分组成:

1. 用 于描述抽象代码结构的一组类。其中CodeCompileUnit类是这些类的根。代表一个源码文件(如C#的*.cs和VB.net的*.vb)。在使 用CodeDOM时,必须先建立一个CodeCompileUnit类的对象,然后在这个对象中加入必要的namespace、class等面向对象元 素。

2. 用于生成和编译源代码的类。这个类必须从CodeDomProvider类继承。每种.net framework

所支持的语言都有自己的CodeDomProvider类。如在C#中的CodeDomProvider类叫CSharpCodeProvider,而在VB.net中叫VBCodeProvider。

三、数据表类的定义

要用CodeDOM定义一个类需要三步:

1. 建立一个CodeCompileUnit对象。这个类相当于一个源码文件。

2. 建立一个CodeNamespace对象。理论上在.net framework上运行的程序语言,如C#、VB.net

等,可以没有namespace。但在CodeDOM中必须使用这个类,如果不想要namespace,可以将namespace的名字设为 null或空串。

2. 建立一个CodeTypeDeclaration对象。这个类可以建立Class和Interface两种Type。在

这个例子中只建立Class。如果想建立Interface,只需将IsInterface属性设为true即可。

主要的实现代码如下:

  private CodeCompileUnit m_CodeCompileUnit; private CodeNamespace m_CodeNameSpace; private CodeTypeDeclaration m_Class; private void InitCodeDom() { m_CodeCompileUnit = new CodeCompileUnit(); m_CodeNameSpace = new CodeNamespace("xml.tables"); m_CodeCompileUnit.Namespaces.Add(m_CodeNameSpace); m_Class = new CodeTypeDeclaration(m_ClassName); m_CodeNameSpace.Types.Add(m_Class); }

其中namespace的名子是“xml.tables”。在建立完namespace后,将其加入到m_CodeCompileUnit的 Namespaces集合中。m_ClassName是一个String变量,它的值就是数据表的表名。最后将所建立的类加入到namespace的 Types集合中。在产生完类后。需要在这个类中加入四部分内容,它们分别是:全局变量、属性、构造函数和方法(Add和Save方法)。下面就分别讨论 它们的实现过程。

全局变量的生成

这个数据表类中有四种全局变量:用于操作xml文件的类型为XmlDocument的变量、用于保存数据表文件名的变量、用于确定是否为加入状态的Boolean型变量、以及用于保存每个字段值的变量组。具体实现代码如下:

  private void GenerateFields() { // 产生 "private XmlDocument m_xml
= new XmlDocument();"
CodeMemberField xml = new
CodeMemberField(
"System.Xml.XmlDocument
", "m_xml"
); CodeObjectCreateExpression createxml
= new CodeObjectCreateExpression
(
"System.Xml.XmlDocument"
); xml.InitExpression = createxml; m_Class.Members.Add(xml); // 产生 "private String m_XmlFile;" CodeMemberField xmlfile = new
CodeMemberField("System.String", "m_XmlFile"
); m_Class.Members.Add(xmlfile); // 根据模板文件产生保存字段值的变量 String fieldname = "", fieldtype = ""; foreach (XmlNode xn in
m_Xml.DocumentElement.ChildNodes) { fieldname
= "m_" +
xn.Name; fieldtype = xn.Attributes["type"].Value; CodeMemberField field = new
CodeMemberField(fieldtype, fieldname); m_Class.Members.Add(field); }
// 产生 "private bool m_AddFlag;"
CodeMemberField addflag = new
CodeMemberField("System.Boolean", "m_AddFlag"
); m_Class.Members.Add(addflag); }

在以上代码中每段程序上方的注释是它们所生成的C#源代码。在输入这段代码之前,需要引入两个namespace。

using System.CodeDom;

using System.CodeDom.Compiler;

四、属性的生成

在数据表类中每个属性代表数据表的一个字段,名子就是字段名。这些属性和保存字段的全局变量一一对应。下面是具体的实现代码:

private void GenerateProperties() { String fieldname = "", fieldtype = ""; foreach (XmlNode xn in m_Xml.DocumentElement.
ChildNodes) { fieldname
=
xn.Name; fieldtype = xn.Attributes["type"].Value; CodeMemberProperty property = new
CodeMemberProperty(); property.Attributes = MemberAttributes.
Public
|
MemberAttributes.Final; property.Name = fieldname; property.Type = new CodeTypeReference(fieldtype); property.HasGet = true; property.HasSet = true; CodeVariableReferenceExpression field = new
CodeVariableReferenceExpression("m_" +
fieldname); // 产生 return m_property CodeMethodReturnStatement propertyReturn
= new
CodeMethodReturnStatement(field); property.GetStatements.Add(propertyReturn); // 产生 m_property = value; CodeAssignStatement propertyAssignment =
new CodeAssignStatement(field, new
CodePropertySetValueReferenceExpression()); property.SetStatements.Add(propertyAssignment); m_Class.Members.Add(property); } }

这些生成的属性是可读写的。这就需要将HasGet和HasSet两个属性设为true,然后分别将get和set方法中的语句分别加到GetStatements和SetStatements中。

构造函数的生成

构造函数的主要工作是打开数据表。如果数据表不存在,就创建这个数据表文件。在编写代码之前,需要先定义三个全局变量。因为这三个全局变量在程序中会多次 用到。它们的类型都是CodeVariableReferenceExpression。这个类型变量其实在生成源码中的作用就是对某一个变量的引用。具 体的实现代码如下:

private CodeVariableReferenceExpression m_XmlFileExpression; private CodeVariableReferenceExpression m_XmlExpression; private CodeVariableReferenceExpression m_AddFlagExpression; private void InitVariants() { m_XmlFileExpression = new
CodeVariableReferenceExpression(
"
m_XmlFile"
); m_XmlExpression = new
CodeVariableReferenceExpression(
"m_xml"
); m_AddFlagExpression = new
CodeVariableReferenceExpression("m_AddFlag"
); }

下面是生成构造函数的源代码:

Add和Save方法生成

Add方法只有一条语句,功能是将m_AddFlag设为true,以使数据表类处于加入状态。Save方法比较复杂。它的功能是当m_AddFlag为true时在数据表文件的最后加入一条记录,并保存。具体实现代码如下:

private void GenerateMethods() { CodeTypeReference voidReference = new
CodeTypeReference(
"System.void"
); //产生Add方法 CodeMemberMethod add = new CodeMemberMethod(); add.ReturnType = voidReference; add.Name = "add"; add.Attributes = MemberAttributes.Public |
MemberAttributes.Final; CodeAssignStatement assignAddTrue = new
CodeAssignStatement
(m_AddFlagExpression,
new
CodePrimitiveExpression(true
)); add.Statements.Add(assignAddTrue); m_Class.Members.Add(add); //产生Save方法 CodeMemberMethod save = new
CodeMemberMethod(); save.ReturnType = voidReference; save.Name = "save"; save.Attributes = MemberAttributes.Public |
MemberAttributes.Final; System.Collections.Generic.List
<CodeStatement> ifStatements
=
new System.Collections.Generic.List
<CodeStatement>
(); //产生 "XmlNode xn = m_xml.CreateNode
(XmlNodeType.Element, "item", "");"
CodeVariableDeclarationStatement xmlNode = new
CodeVariableDeclarationStatement(
"
System.Xml.XmlNode", "xn"
); CodeMethodInvokeExpression createNode = new
CodeMethodInvokeExpression(m_XmlExpression,
"CreateNode
", new CodeExpression[] {new
CodeVariableReferenceExpression(
"System.Xml.XmlNodeType.Element"
), new CodePrimitiveExpression("item"),
new CodePrimitiveExpression(""
) }); xmlNode.InitExpression = createNode; ifStatements.Add(xmlNode); //产生 "XmlAttribute xa = null; " CodeVariableDeclarationStatement
xmlAttr
= new
CodeVariableDeclarationStatement
(
"System.Xml.XmlAttribute", "xa"
); xmlAttr.InitExpression = new
CodePrimitiveExpression(
null
); ifStatements.Add(xmlAttr); //产生字段属性 CodeStatementCollection statements
= new
CodeStatementCollection(); foreach (XmlNode xn in
m_Xml.DocumentElement.ChildNodes) { CodeMethodInvokeExpression createAttribute
= new
CodeMethodInvokeExpression(m_XmlExpression, "CreateAttribute", new
CodePrimitiveExpression(xn.Name)); CodeAssignStatement assignxa
= new
CodeAssignStatement
(
new CodeVariableReferenceExpression("xa"
),
createAttribute); CodeMethodInvokeExpression invokeToString
= new
CodeMethodInvokeExpression(new
CodeVariableReferenceExpression
(
"m_" + xn.Name), "ToString"
); CodeAssignStatement assignValue = new
CodeAssignStatement
(
new CodeVariableReferenceExpression("xa.Value"
),
invokeToString); CodeMethodInvokeExpression invokeAppend
= new
CodeMethodInvokeExpression(new
CodeVariableReferenceExpression
(
"xn.Attributes"), "Append", new
CodeVariableReferenceExpression("xa")); statements.Add(invokeAppend); ifStatements.Add(assignxa); ifStatements.Add(assignValue); ifStatements.Add(statements[0]); } // 产生 "m_xml.DocumentElement.AppendChild(xn);" CodeMethodInvokeExpression invokeAppendChild
= new CodeMethodInvokeExpression(
new
CodeVariableReferenceExpression
(
"m_xml.DocumentElement"), "AppendChild",
new CodeVariableReferenceExpression("xn"
)); statements.Clear(); statements.Add(invokeAppendChild); ifStatements.Add(statements[0]); // 产生 "m_xml.Save(m_XmlFile);" CodeMethodInvokeExpression invokeSave = new
CodeMethodInvokeExpression(m_XmlExpression,
"Save"
, m_XmlFileExpression); statements.Clear(); statements.Add(invokeSave); ifStatements.Add(statements[0]); // 产生 "m_AddFlag = false;" CodeAssignStatement assignAddFalse = new
CodeAssignStatement(m_AddFlagExpression,
new CodePrimitiveExpression(false
)); ifStatements.Add(assignAddFalse); // 产生if语句: "if (m_AddFlag)" CodeConditionStatement ifStatement = new
CodeConditionStatement(m_AddFlagExpression,
ifStatements.ToArray()); save.Statements.Add(ifStatement); m_Class.Members.Add(save); }

 

五、生成源代码

生成具体语言的源代码需要一个从CodeDomProvider继承的类。对于C#而言是CSharpCodeProvider类。实现代码如下:

using Microsoft.CSharp; public void SaveCSharp(String filename) { IndentedTextWriter tw = new IndentedTextWriter
(
new StreamWriter(filename, false), " "
); CodeDomProvider provide = new CSharpCodeProvider(); provide.GenerateCodeFromCompileUnit(m_CodeCompileUnit,
tw,
new
CodeGeneratorOptions()); tw.Close(); }

在使用CSharpCodeProvider类时需要用到m_CodeCompileUnit这个全局变量。这样可产生一个*.cs文件。以上代码中的 IndentedTextWriter类是建立一个文件的Writer,用于向这个文件中输出源代码。但和其它的Writer不同的是它的输出是缩进的 (以四个空格进行缩进)。如是想生成VB.net的代码,只需将CSharpCodeProvider改为VBCodeProvider即可。

编译源代码

到 现在为止,这个数据表类的源代码已经全部生成了。你可以将这个源文件直接加入到自己的工程中。或者直接将其编译成*.dll文件,然后在程序中调用。如果 想编译,可以直接调用指定语言的编译器(如C#中的csc.exe)。但这样不是太方便。在CodeDOM中提供了一种机制,可以在程序中通过 CodeDomProvider直接调用指定语言的编译器。下面是编译C#源程序的一个例子。

public void CompileCSharp(String sourcefile,
String targetFile) { CompilerParameters cp
= new CompilerParameters
(
new String[] { "System.Xml.dll" }, targetFile, false
); CodeDomProvider provider = new CSharpCodeProvider(); cp.GenerateExecutable = false; // 调用编译器 CompilerResults cr = provider.
CompileAssemblyFromFile(cp, sourcefile);
if (cr.Errors.Count > 0
) { // 显示编译错误 foreach (CompilerError ce in cr.Errors) System.Windows.Forms.MessageBox.Show(ce.ToString()); } }

 

对于以上代码有两点说明:

使用CodeDomProvider调用编译器时也需要传递相应的参数,如在本例中将System.Xml.dll

作为一个参数,表示目标文件需要调用这个dll中的资源。

在调用编译器后,如果出现错误,可使用cr.Errors获得错误信息。

结束语

我花了一个晚上的时间实现了这个简单的例子,并用C#2.0调试通过,只是为了抛砖引玉。自动生成源代码有很多的方法,但使用CodeDom生成源代码会有更大的灵活性,主要表现在以下三个方面:

1. 语言无关。即只要是.net framework所支持的语言,并且这种语言提供了CodeDomProvider。

就可以生成这种语言的源代码。

2. 如果所生成的语言是测试版或要将这种语言升级到下一个版本,也可以考虑使用CodeDOM。

因为当这种语言的语法有所变化时,CodeDomProvider也会随之升级。因此,使用CodeDOM的Code Wizards也会随着CodeDOM而升级,这样就不必修改Code Wizards的源代码了。

3. 如果所生成的一种语言是你所不熟悉的,如果不使用CodeDOM,必须要熟悉这种语言的语

法,才能生成它的源代码。而使用CodeDOM却可以避免这一点。因为CodeDOM是使用抽象的object graph来描述语言的。而语言的具体语法是由CodeDomProvider所决定的。

其实CodeDOM不仅可以用在Code Wizards上,也可以用在许多其它地方,如可以生成Web

Services的客户端代理(Client Proxies),或根据UML图生成类的构架代码。总之,使用CodeDom可以大大降低和语言的偶合度,并且很容易维护和升级系统。

   
780 次浏览       16
 
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
 
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
 
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程

最新活动计划
MBSE(基于模型的系统工程)4-18[北京]
自然语言处理(NLP) 4-25[北京]
基于 UML 和EA进行分析设计 4-29[北京]
以用户为中心的软件界面设计 5-16[北京]
DoDAF规范、模型与实例 5-23[北京]
信息架构建模(基于UML+EA)5-29[北京]
 
 
最新文章
.NET Core 3.0 正式公布:新特性详细解读
.NET Core部署中你不了解的框架依赖与独立部署
C# event线程安全
简析 .NET Core 构成体系
C#技术漫谈之垃圾回收机制(GC)
最新课程
.Net应用开发
C#高级开发技术
.NET 架构设计与调试优化
ASP.NET Core Web 开发
ASP.Net MVC框架原理与应用开发
成功案例
航天科工集团子公司 DotNet企业级应用设计与开发
日照港集 .NET Framewor
神华信 .NET单元测试
台达电子 .NET程序设计与开发
神华信息 .NET单元测试