.NET推崇这样一种思想:相对于框架而言,语言处于从属、次要的地位。CodeDom名称空间中包含的类是这一思想的集中体现。我们可以用CodeDom构造一个树或图,用System.CodeDom名称空间的类填充它,完成后,用对应各种.NET语言的CodeProvider对象将树结构转换成该种语言的代码。要更换一种语言,简单到只需更换一下最后用到的CodeProvider对象。
  
    设想一下,利用这一技术,我们至少能够:
  
    ·查询存储过程的元数据,构造出一个负责参数绑定的类。
  
    ·查询程序集的manifest,构造出一个对每个函数执行单元测试的类。
  
    ·为开发组用到的每一种语言生成样板代码。
  
    ·只需写一份范例代码,就可以让用户自由选择他们要查看哪一种语言的版本。
  
    ·自定义模板语法,经解析后生成任意语言的代码。
  
    ·如果要学习某种不熟悉的语言,可以生成该语言的代码,然后将它与熟悉的语言比较。
  
    一、基本操作
  
    System.CodeDom名称空间包含了许多以语言中立的形式描述常见程序结构的对象,每一种语言的细节则由与该种语言对应的CodeProvider对象负责处理。例如,CodeConditionStatement包含一个TrueStatements集合、一个FalseStatements集合和一个条件属性(Condition attribute),但不涉及条件语句块要用“end if”还是右花括号“}”结束,这部分细节由CodeProvider处理。有了这一层抽象,我们就可以描述待生成的代码结构,然后将它以任意语言的形式输出,却不必斤斤计较于各种与特定语言有关的细节问题。同时,这种抽象也为我们通过程序改变代码的结构带来了方便。例如,当我们发现某个方法需要增加一个参数时,就可以将参数加入到该方法的Parameters集合,根本无须改动已生成的代码逻辑。
  
    我们在本文中要用到的大部分对象来自System.CodeDom名称空间,其余对象主要来自各个与特定语言有关的名称空间,例如Microsoft.CSharp名称空间、Microsoft.VisualBasic名称空间、Microsoft.JScript名称空间和Microsoft.VJSharp名称空间。所有这些面向特定语言的名称空间都包含各自的CodeProvider对象。最后,System.CodeDom.Complier名称空间定义ICodeGenerator接口,后者用来把生成的代码输出到一个TextWriter对象。
  
    如果我们只要生成一些用于插件或宏的代码片断,可以利用CodeGenerator从Statement、Expression、Type等生成代码。反之,如果我们要生成的是一个完整的文件,则必须从CodeNameSpace对象入手。在本文的例子中,我们将从一个名称空间开始,示范如何加入import语句、声明类、声明方法、声明变量、实现一个循环结构、索引一个数组,最后,我们将这些技术结合起来,得到一个大家都熟悉的程序。
  
    1.1 初始化名称空间
  
    初始化名称空间的代码类似下面这种形式: 
   
 1  private CodeNameSpace InitializeNameSpace(string Name) 
 2  
 3   // 初始化CodeNameSpace变量,指定名称空间的名称 
 4   CodeNameSpace CurrentNameSpace = new CodeNamespace (Name); 
 5   // 将一些名称空间加入到要导入的名称空间集合。 
 6   // 各种语言如何导入名称空间的细节由每种语言对应 
 7   // 的CodeProvider分别处理。 
 8   CurrentNameSpace.Imports.Add (new CodeNamespaceImport("System")); 
 9   CurrentNameSpace.Imports.Add (new CodeNamespaceImport("System.Text")); 
10   return CurrentNameSpace; 
11  }
 
12   

   
   
    这段代码定义了一个新的名称空间,并导入System和System.Text名称空间。
  
    1.2 创建类
  
    声明一个新类的代码类似下面这种形式: 
 1   
 2  private CodeTypeDeclaration CreateClass (string Name) 
 3  
 4   // 新建一个CodeTypeDeclaration对象,指定要创建的类的名称 
 5   CodeTypeDeclaration ctd = new CodeTypeDeclaration (Name); 
 6   // 指定这个CodeType是一个类,而不是一个枚举变量或struct 
 7   ctd.IsClass = true
 8   // 这个类的访问类型是public 
 9   ctd.Attributes = MemberAttributes.Public; 
10   // 返回新创建的类 
11   return ctd; 
12  }
 
13   

   
   
    CreateClass函数新建一个指定名称的类,做好为该类植入方法、属性、事件的准备。
  
    1.3 创建方法
  
    声明一个新函数的代码类似下面这种形式: 
 1   
 2  private CodeEntryPointMethod CreateMethod() 
 3  
 4   // 创建一个方法 
 5   CodeEntryPointMethod method = new CodeEntryPointMethod(); 
 6   // 指定该方法的修饰符:public,static 
 7   method.Attributes = MemberAttributes.Public | 
 8   MemberAttributes.Static; 
 9   // 返回新创建的方法 
10   return method; 
11  }
 
12   

   
   
    本例创建了一个CodeEntryPointMethod对象。CodeEntryPointMethod对象类似于CodeMemberMethod对象,两者的不同之处在于,CodeProvider会将CodeEntryPointMethod代表的方法作为类的入口点调用,例如作为Sub Main或void main等。对于CodeEntryPointMethod对象,方法的名称默认为Main;对于CodeMemberMethod,方法的名称必须显式指定。
  
    1.4 声明变量
  
    声明一个变量的代码类似下面这种形式: 
   
 
 1 private CodeVariableDeclarationStatement 
 2   DeclareVariables(System.Type DataType, 
 3   string Name) 
 4  
 5   // 为将要创建的变量类型创建一个CodeTypeReference对象, 
 6   // 这使得我们不必去关注该类数据在特定语言环境中的 
 7   // 与数据类型有关的细节问题。 
 8   CodeTypeReference tr = new CodeTypeReference (DataType ); 
 9   // CodeVariableDeclarationStatement对象使得我们不必纠缠于 
10   // 与特定语言有关的下列细节:在该语言的变量声明语句中, 
11   // 应该是数据类型在前,还是变量名称在前;声明变量时是 
12   // 否要用到Dim之类的关键词. 
13   CodeVariableDeclarationStatement Declaration = 
14   new CodeVariableDeclarationStatement(tr, Name); 
15   // CodeObjectCreateExpression负责处理所有调用构造器的细节。 
16   // 大多数情况下应该是new,但有时要使用New。但不管怎样, 
17   // 我们不必去关注这些由语言类型决定的细节. 
18   CodeObjectCreateExpression newStatement = new 
19   CodeObjectCreateExpression (); 
20   // 指定我们要调用其构造器的对象. 
21   newStatement.CreateType = tr; 
22   // 变量将通过调用其构造器的方式初始化. 
23   Declaration.InitExpression = newStatement; 
24   return Declaration; 
25  }
 
26   

   
   
    每一种.NET语言都有其特定的数据类型名称,所有这些数据类型都被映射到公共的.NET语言类型。例如,对于C#中称为int的数据类型,在VB.NET中是Integer,公共的.NET类型是System.Int32。CodeTypeReference对象直接使用.NET公共数据类型,以后由每种语言的CodeProvider将它转换成符合各自语言规范的类型名称。
  
    1.5 初始化数组
  
    初始化一个数组的代码类似下面这种形式: 
   
 
 1 private void InitializeArray (string Name, 
 2   params char[] Characters ) 
 3  
 4   // 从参数中传入的字符数组获得一个CodeTypeReference 对象, 
 5   // 以便在生成的代码中复制该数据类型. 
 6   CodeTypeReference tr = new CodeTypeReference (Characters.GetType()); 
 7   // 声明一个匹配原始数组的数组 
 8   CodeVariableDeclarationStatement Declaration = 
 9   new CodeVariableDeclarationStatement (tr, Name); 
10   // CodePrimitiveExpression代表“基本”或值数据类型, 
11   // 例如char、int、double等等。 
12   // 我们将用这类基本数据类型构成的一个数组来 
13   // 初始化我们正在声明的数组。 
14   CodePrimitiveExpression[] cpe = new 
15   CodePrimitiveExpression[Characters.Length]; 
16   // 循环遍历原始字符数组, 
17   // 为CodePrimitiveExpression类型的数组创建对象。 
18   for (int i = 0; i < Name.Length ; i++
19   
20   // 每一个CodePrimitiveExpression将有一个字符的语言 
21   // 中立的表示。 
22   cpe[i] = new CodePrimitiveExpression (Characters[i]); 
23   }
 
24   // CodeArrayCreateExpression负责调用数组中数据类型的 
25   // 默认构造器。 
26   // 由于我们还传入了一个CodePrimitiveExpression的数组, 
27   // 所以不必指定数组的大小,且数组中的每一个元素都将有 
28   // 合适的初值。 
29   CodeArrayCreateExpression array = new 
30   CodeArrayCreateExpression(tr, cpe); 
31   // 指定:该CodeArrayCreateExpression将初始化数组变量声明。 
32   Declaration.InitExpression = array; 
33   return Declaration; 
34  }
 
35
   
   
   
    1.6 定义循环结构
  
    声明一个循环结构的代码类似下面这种形式: 
   
 
 1 private CodeIterationStatement CreateLoop(string LoopControlVariableName) 
 2  
 3   // 声明一个新的变量,该变量将作为 
 4   // 循环控制变量 
 5   CodeVariableDeclarationStatement Declaration; 
 6   // 声明一个管理所有循环逻辑的CodeIterationStatement 
 7   CodeIterationStatement forloop = new CodeIterationStatement(); 
 8   // 为动态声明的变量指定数据类型的另一种方法: 
 9   // 用typeof函数获得该数据类型的Type对象,不必 
10   // 用到该类数据的变量 
11   Declaration = new CodeVariableDeclarationStatement(typeof (int), 
12   LoopControlVariableName); 
13   // 指定一个简单的初始化表达式: 
14   // 将新变量设置为0 
15   Declaration.InitExpression = new CodeSnippetExpression ("0"); 
16   // 这个新声明的变量将用来初始化循环 
17   forloop.InitStatement = Declaration; 
18   // CodeAssignStatement用来处理赋值语句。 
19   // 这里使用的构造器要求提供两个表达式,第一个位于 
20   // 赋值语句的左边,第二个位于赋值语句的右边。 
21   // 另一种办法是:调用默认的构造器,然后分别显式设置 
22   // 左、右两个表达式。 
23   CodeAssignStatement assignment = new CodeAssignStatement( 
24   new CodeVariableReferenceExpression(LoopControlVariableName), 
25   new CodeSnippetExpression (LoopControlVariableName + " + 1" )); 
26   // 在循环迭代中使用赋值语句。 
27   forloop.IncrementStatement = assignment; 
28   // 当循环控制变量超出数组中的字符个数时, 
29   // 循环结束 
30   forloop.TestExpression = new CodeSnippetExpression 
31   (LoopControlVariableName + " < Characters.Length"); 
32   return forloop; 
33  }
 
34   
35
   
   
    注意,这里我们用typeof函数直接获得循环控制变量的数据类型的Type对象,而不是通过声明一个CodeTypeReference对象的方式。这是CodeVariableDeclartionStatement的又一个构造器,实际上其构造器的总数多达7种。
  
    1.7 索引数组
  
    索引一个数组的代码类似下面这种形式: 
 1   
 2  private CodeArrayIndexerExpression 
 3   CreateArrayIndex(string ArrayName, string IndexValue ) 
 4  
 5   // 新建一个CodeArrayIndexerExpression 
 6   CodeArrayIndexerExpression index = new CodeArrayIndexerExpression (); 
 7   // Indices属性是一个能够支持多维数组的集合。不过这里我们只需要 
 8   // 一个简单的一维数组。 
 9   index.Indices.Add ( new CodeVariableReferenceExpression (IndexValue)); 
10   // TargetObject指定了要索引的数组的名称。 
11   index.TargetObject = new CodeSnippetExpression (ArrayName); 
12   return index; 
13  }
 
14   

   
   
    CodeArrayIndexerExpression对象处理数组索引方式的种种差异。例如,在C#中数组以ArrayName[IndexValue]的方式索引;但在VB.NET中,数组以ArrayName(IndexValue)的方式索引。CodeArrayIndexerExpression允许我们忽略这种差异,将注意力集中到其他更重要的问题,例如要索引哪一个数组、要访问第几个数组元素。
  
    二、装配出树结构
  
    我们可以把前面定义的所有函数加入到一个类,通过构造器初始化,例如: 
   
 
 1 public CodeDomProvider() 
 2  
 3   CurrentNameSpace = InitializeNameSpace("TestSpace"); 
 4   CodeTypeDeclaration ctd = CreateClass ("HelloWorld"); 
 5   // 把类加入到名称空间 
 6   CurrentNameSpace.Types.Add (ctd); 
 7   CodeEntryPointMethod mtd = CreateMethod(); 
 8   // 把方法加入到类 
 9   ctd.Members.Add (mtd); 
10   CodeVariableDeclarationStatement VariableDeclaration = 
11   DeclareVariables (typeof (StringBuilder), "sbMessage"); 
12   // 把变量声明加入到方法 
13   mtd.Statements.Add (VariableDeclaration); 
14   CodeVariableDeclarationStatement array = InitializeArray 
15   ("Characters"'H''E''L''L''O'' '
16   'W''O''R''L''D'); 
17   // 把数组加入到方法 
18   mtd.Statements.Add (array); 
19   CodeIterationStatement loop = CreateLoop("intCharacterIndex"); 
20   // 把循环加入到方法 
21   mtd.Statements.Add (loop); 
22   // 数组索引 
23   CodeArrayIndexerExpression index = CreateArrayIndex("Characters"
24   "intCharacterIndex"); 
25   // 加入一个语句,它将调用sbMessage对象的“Append”方法 
26   loop.Statements.Add (new CodeMethodInvokeExpression ( 
27   new CodeSnippetExpression ("sbMessage"),"Append"
28   index)); 
29   // 循环结束后,输出所有字符追加到sbMessage对象 
30   // 后得到的结果 
31   mtd.Statements.Add (new CodeSnippetExpression 
32   ("Console.WriteLine (sbMessage.ToString())")); 
33  }
 
34
   
   
   
    构造器的运行结果是一个完整的CodeDom树结构。可以看到,至此为止我们的所有操作都独立于目标语言。最后生成的代码将以属性的形式导出。
  
    三、输出生成结果
  
    构造好CodeDom树之后,我们就可以较为方便地将代码以任意.NET语言的形式输出。每一种.NET语言都有相应的CodeProvider对象,CodeProvider对象的CreateGenerator方法能够返回一个实现了ICodeGenerator接口的对象。ICodeGenerator接口定义了用来生成代码的所有方法,而且允许我们定义一个用来简化属性输出的辅助方法。下面的辅助方法GenerateCode负责设置好合适的TextWriter以供输出代码,以字符串的形式返回结果文本。 
   
 
 1 private string GenerateCode (ICodeGenerator CodeGenerator) 
 2  
 3   // CodeGeneratorOptions允许我们指定各种供代码生成器 
 4   // 使用的格式化选项 
 5   CodeGeneratorOptions cop = new CodeGeneratorOptions(); 
 6   // 指定格式:花括号的位置 
 7   cop.BracingStyle = "C"
 8   // 指定格式:代码块的缩进方式 
 9   cop.IndentString = " "
10   // GenerateCodeFromNamespace要求传入一个TextWriter以 
11   // 容纳即将生成的代码。这个TextWriter可以是一个StreamWriter、 
12   // 一个StringWriter或一个IndentedTextWriter。 
13   // StreamWriter可用来将代码输出到文件。 
14   // StringWriter可绑定到StringBuilder,后者可作为一个变量引用。 
15   // 在这里,我们把一个StringWriter绑定到StringBuilder sbCode。 
16   StringBuilder sbCode = new StringBuilder(); 
17   StringWriter sw = new StringWriter(sbCode); 
18   
19   // 生成代码! 
20   CodeGenerator.GenerateCodeFromNamespace(CurrentNameSpace, sw,cop); 
21   return sbCode.ToString(); 
22  }
 
23
   
   
   
    有了这个辅助函数,要获取各种语言的代码就相当简单了: 
   
 
 1 public string VBCode 
 2  
 3   get 
 4   
 5   VBCodeProvider provider = new VBCodeProvider (); 
 6   ICodeGenerator codeGen = provider.CreateGenerator (); 
 7   return GenerateCode (codeGen); 
 8   }
 
 9   
10  }
 
11   
12  public string JScriptCode 
13  
14   get 
15   
16   JScriptCodeProvider provider = new JScriptCodeProvider (); 
17   ICodeGenerator codeGen = provider.CreateGenerator (); 
18   return GenerateCode(codeGen); 
19   }
 
20   
21  }
 
22   
23  public string JSharpCode 
24  
25   get 
26   
27   VJSharpCodeProvider provider = new VJSharpCodeProvider (); 
28   ICodeGenerator codeGen = provider.CreateGenerator (); 
29   return GenerateCode (codeGen); 
30   }
 
31   
32  }
 
33   
34  public string CSharpCode 
35  
36   get 
37   
38   CSharpCodeProvider provider = new CSharpCodeProvider(); 
39   ICodeGenerator codeGen = provider.CreateGenerator (); 
40   return GeneratorCode (codeGen); 
41   }
 
42   
43  }
 
44
   
   
   
    四、显示出生成的代码
  
    为输出代码,我们要用到一个简单的.aspx文件,它有四个标签,分别对应一种.NET语言: 
   
 
 1 <table width="800" border="1"> 
 2   <tr> 
 3   <th>VB.NET代码</th> 
 4   </tr> 
 5   <tr > 
 6   <td> 
 7   <asp:Label ID="vbCode" Runat="server" CssClass="code"> 
 8   </asp:Label> 
 9   </td> 
10   </tr> 
11   <tr> 
12   <th> 
13   C#代码</th></tr> 
14   <tr> 
15   <td><asp:Label ID="csharpcode" Runat="server" CssClass="code"> 
16   </asp:Label></td> 
17   </tr> 
18   <tr> 
19   <th>J#代码</th></tr> 
20   <tr > 
21   <td> 
22   <asp:Label ID="JSharpCode" Runat="server" CssClass="code"> 
23   </asp:Label> 
24   </td> 
25   </tr> 
26   <tr> 
27   <th>JScript.NET代码</th> 
28   </tr> 
29   <tr> 
30   <td><asp:Label ID="JScriptCode" Runat="server" CssClass="code"> 
31   </asp:Label></td> 
32   </tr> 
33  </table> 
34
   
   
   
    在后台执行的代码中,我们实例化一个前面创建的CodeDomProvider类的实例,把它生成的代码赋值给.aspx页面的相应标签的Text属性。为了使Web页面中显示的代码整齐美观,有必要做一些简单的格式化,替换换行符号、空格等,如下所示: 
1   
2  private string FormatCode (string CodeToFormat) 
3  
4   string FormattedCode = Regex.Replace (CodeToFormat, "\n""<br>"); 
5   FormattedCode = Regex.Replace (FormattedCode, " " , " "); 
6   FormattedCode = Regex.Replace (FormattedCode, ","""); 
7   return FormattedCode; 
8  }
 
9   

   
   
    下面把生成的代码显示到Web页面: 
   
  1
  2  private void Page_Load(object sender, System.EventArgs e) 
  3  
  4   
  5   HelloWorld.CodeDomProvider codegen = new HelloWorld.CodeDomProvider (); 
  6   vbCode.Text = FormatCode (codegen.VBCode); 
  7   csharpcode.Text = FormatCode (codegen.CSharpCode); 
  8   JScriptCode.Text = FormatCode (codegen.JScriptCode); 
  9   JSharpCode.Text = FormatCode (codegen.JSharpCode); 
 10   Page.EnableViewState = false
 11  }
 
 12   
 13   
 14   
 15    输出结果如下: 
 16   
 17  VB.NET代码 
 18   
 19  Imports System 
 20  Imports System.Text 
 21   
 22  Namespace HelloWorld 
 23   
 24   Public Class Hello_World 
 25   
 26   Public Shared Sub Main() 
 27   Dim sbMessage As System.Text.StringBuilder = _ 
 28   New System.Text.StringBuilder 
 29   Dim Characters() As Char = New Char() {_ 
 30   Microsoft.VisualBasic.ChrW(72), _ 
 31   Microsoft.VisualBasic.ChrW(69), _ 
 32   Microsoft.VisualBasic.ChrW(76), _ 
 33   Microsoft.VisualBasic.ChrW(76), _ 
 34   Microsoft.VisualBasic.ChrW(79), _ 
 35   Microsoft.VisualBasic.ChrW(32), _ 
 36   Microsoft.VisualBasic.ChrW(87), _ 
 37   Microsoft.VisualBasic.ChrW(79), _ 
 38   Microsoft.VisualBasic.ChrW(82), _ 
 39   Microsoft.VisualBasic.ChrW(76), _ 
 40   Microsoft.VisualBasic.ChrW(68)}
 
 41   Dim intCharacterIndex As Integer = 0 
 42   Do While intCharacterIndex < Characters.Length 
 43   sbMessage.Append(Characters(intCharacterIndex)) 
 44   intCharacterIndex = intCharacterIndex + 1 
 45   Loop 
 46   Console.WriteLine (sbMessage.ToString()) 
 47   End Sub 
 48   End Class 
 49  End Namespace 
 50   
 51  C#代码 
 52   
 53  namespace HelloWorld 
 54  
 55   using System; 
 56   using System.Text; 
 57   
 58   public class Hello_World 
 59   
 60   public static void Main() 
 61   
 62   System.Text.StringBuilder sbMessage = new 
 63   System.Text.StringBuilder(); 
 64   char[] Characters = new char[] 
 65   'H'
 66   'E'
 67   'L'
 68   'L'
 69   'O'
 70   ' '
 71   'W'
 72   'O'
 73   'R'
 74   'L'
 75   'D'}

 76   for (int intCharacterIndex = 0
 77   intCharacterIndex < Characters.Length; 
 78   intCharacterIndex = intCharacterIndex + 1
 79   
 80   sbMessage.Append(Characters[intCharacterIndex]); 
 81   }
 
 82   Console.WriteLine (sbMessage.ToString()); 
 83   }
 
 84   }
 
 85  }
 
 86   
 87  J#代码 
 88   
 89  package HelloWorld; 
 90  import System.*
 91  import System.Text.*
 92   
 93   
 94  public class Hello_World 
 95  
 96   public static void main(String[] args) 
 97   
 98   System.Text.StringBuilder sbMessage = new 
 99   System.Text.StringBuilder(); 
100   char[] Characters = new char[] 
101   
102   'H'
103   'E'
104   'L'
105   'L'
106   'O'
107   ' '
108   'W'
109   'O'
110   'R'
111   'L'
112   'D'}
 
113   ; 
114   for (int intCharacterIndex = 0
115   intCharacterIndex < Characters.Length; 
116   intCharacterIndex = intCharacterIndex + 1
117   
118   sbMessage.Append(Characters[intCharacterIndex]); 
119   }
 
120   Console.WriteLine (sbMessage.ToString()); 
121   }
 
122  }
 
123   
124   
125  JScript.NET代码 
126   
127   
128  //@cc_on 
129  //@set @debug(off) 
130   
131  import System; 
132  import System.Text; 
133   
134  package HelloWorld 
135  
136   
137   public class Hello_World 
138   
139   
140   public static function Main() 
141   
142   var sbMessage : System.Text.StringBuilder = 
143   new System.Text.StringBuilder(); 
144   var Characters : char[] = 
145   ['H''E''L''L''O'' ''W''O''R''L''D']; 
146   for (var intCharacterIndex : int = 0
147   ; intCharacterIndex < Characters.Length; 
148   intCharacterIndex = intCharacterIndex + 1
149   
150   sbMessage.Append(Characters[intCharacterIndex]); 
151   }
 
152   Console.WriteLine (sbMessage.ToString()); 
153   }
 
154   }
 
155  }
 
156  HelloWorld.Hello_World.Main(); 
157
   
   
   
    总结:CodeDom体现了.NET中语言的重要性不如框架的思想。本文示范了如何运用一些常用的CodeDom类,几乎每一个使用CodeDom技术的应用都要用到这些类。作为一种结构化的代码生成技术,CodeDom有着无限的潜能,唯一的约束恐怕在于人们的想象力。