.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 初始化名稱空間
初始化名稱空間的代碼類似下面這種形式:
private codenamespace initializenamespace(string name)
{
// 初始化codenamespace變量,指定名稱空間的名稱
codenamespace currentnamespace = new codenamespace (name);
// 將一些名稱空間加入到要導入的名稱空間集合。
// 各種語言如何導入名稱空間的細節由每種語言對應
// 的codeprovider分別處理。
currentnamespace.imports.add (new codenamespaceimport("system"));
currentnamespace.imports.add (new codenamespaceimport("system.text"));
return currentnamespace;
}
這段代碼定義了一個新的名稱空間,并導入system和system.text名稱空間。
1.2 創建類
聲明一個新類的代碼類似下面這種形式:
private codetypedeclaration createclass (string name)
{
// 新建一個codetypedeclaration對象,指定要創建的類的名稱
codetypedeclaration ctd = new codetypedeclaration (name);
// 指定這個codetype是一個類,而不是一個枚舉變量或struct
ctd.isclass = true;
// 這個類的訪問類型是public
ctd.attributes = memberattributes.public;
// 返回新創建的類
return ctd;
}
createclass函數新建一個指定名稱的類,做好為該類植入方法、屬性、事件的準備。
1.3 創建方法
聲明一個新函數的代碼類似下面這種形式:
private codeentrypointmethod createmethod()
{
// 創建一個方法
codeentrypointmethod method = new codeentrypointmethod();
// 指定該方法的修飾符:public,static
method.attributes = memberattributes.public |
memberattributes.static;
// 返回新創建的方法
return method;
}
本例創建了一個codeentrypointmethod對象。codeentrypointmethod對象類似于codemembermethod對象,兩者的不同之處在于,codeprovider會將codeentrypointmethod代表的方法作為類的入口點調用,例如作為sub main或void main等。對于codeentrypointmethod對象,方法的名稱默認為main;對于codemembermethod,方法的名稱必須顯式指定。
1.4 聲明變量
聲明一個變量的代碼類似下面這種形式:
private codevariabledeclarationstatement
declarevariables(system.type datatype,
string name)
{
// 為將要創建的變量類型創建一個codetypereference對象,
// 這使得我們不必去關注該類數據在特定語言環境中的
// 與數據類型有關的細節問題。
codetypereference tr = new codetypereference (datatype );
// codevariabledeclarationstatement對象使得我們不必糾纏于
// 與特定語言有關的下列細節:在該語言的變量聲明語句中,
// 應該是數據類型在前,還是變量名稱在前;聲明變量時是
// 否要用到dim之類的關鍵詞.
codevariabledeclarationstatement declaration =
new codevariabledeclarationstatement(tr, name);
// codeobjectcreateexpression負責處理所有調用構造器的細節。
// 大多數情況下應該是new,但有時要使用new。但不管怎樣,
// 我們不必去關注這些由語言類型決定的細節.
codeobjectcreateexpression newstatement = new
codeobjectcreateexpression ();
// 指定我們要調用其構造器的對象.
newstatement.createtype = tr;
// 變量將通過調用其構造器的方式初始化.
declaration.initexpression = newstatement;
return declaration;
}
每一種.net語言都有其特定的數據類型名稱,所有這些數據類型都被映射到公共的.net語言類型。例如,對于c#中稱為int的數據類型,在vb.net中是integer,公共的.net類型是system.int32。codetypereference對象直接使用.net公共數據類型,以后由每種語言的codeprovider將它轉換成符合各自語言規范的類型名稱。
1.5 初始化數組
初始化一個數組的代碼類似下面這種形式:
private void initializearray (string name,
params char[] characters )
{
// 從參數中傳入的字符數組獲得一個codetypereference 對象,
// 以便在生成的代碼中復制該數據類型.
codetypereference tr = new codetypereference (characters.gettype());
// 聲明一個匹配原始數組的數組
codevariabledeclarationstatement declaration =
new codevariabledeclarationstatement (tr, name);
// codeprimitiveexpression代表“基本”或值數據類型,
// 例如char、int、double等等。
// 我們將用這類基本數據類型構成的一個數組來
// 初始化我們正在聲明的數組。
codeprimitiveexpression[] cpe = new
codeprimitiveexpression[characters.length];
// 循環遍歷原始字符數組,
// 為codeprimitiveexpression類型的數組創建對象。
for (int i = 0; i < name.length ; i++)
{
// 每一個codeprimitiveexpression將有一個字符的語言
// 中立的表示。
cpe[i] = new codeprimitiveexpression (characters[i]);
}
// codearraycreateexpression負責調用數組中數據類型的
// 默認構造器。
// 由于我們還傳入了一個codeprimitiveexpression的數組,
// 所以不必指定數組的大小,且數組中的每一個元素都將有
// 合適的初值。
codearraycreateexpression array = new
codearraycreateexpression(tr, cpe);
// 指定:該codearraycreateexpression將初始化數組變量聲明。
declaration.initexpression = array;
return declaration;
}
1.6 定義循環結構
聲明一個循環結構的代碼類似下面這種形式:
private codeiterationstatement createloop(string loopcontrolvariablename)
{
// 聲明一個新的變量,該變量將作為
// 循環控制變量
codevariabledeclarationstatement declaration;
// 聲明一個管理所有循環邏輯的codeiterationstatement
codeiterationstatement forloop = new codeiterationstatement();
// 為動態聲明的變量指定數據類型的另一種方法:
// 用typeof函數獲得該數據類型的type對象,不必
// 用到該類數據的變量
declaration = new codevariabledeclarationstatement(typeof (int),
loopcontrolvariablename);
// 指定一個簡單的初始化表達式:
// 將新變量設置為0
declaration.initexpression = new codesnippetexpression ("0");
// 這個新聲明的變量將用來初始化循環
forloop.initstatement = declaration;
// codeassignstatement用來處理賦值語句。
// 這里使用的構造器要求提供兩個表達式,第一個位于
// 賦值語句的左邊,第二個位于賦值語句的右邊。
// 另一種辦法是:調用默認的構造器,然后分別顯式設置
// 左、右兩個表達式。
codeassignstatement assignment = new codeassignstatement(
new codevariablereferenceexpression(loopcontrolvariablename),
new codesnippetexpression (loopcontrolvariablename + " + 1" ));
// 在循環迭代中使用賦值語句。
forloop.incrementstatement = assignment;
// 當循環控制變量超出數組中的字符個數時,
// 循環結束
forloop.testexpression = new codesnippetexpression
(loopcontrolvariablename + " < characters.length");
return forloop;
}
注意,這里我們用typeof函數直接獲得循環控制變量的數據類型的type對象,而不是通過聲明一個codetypereference對象的方式。這是codevariabledeclartionstatement的又一個構造器,實際上其構造器的總數多達7種。
1.7 索引數組
索引一個數組的代碼類似下面這種形式:
private codearrayindexerexpression
createarrayindex(string arrayname, string indexvalue )
{
// 新建一個codearrayindexerexpression
codearrayindexerexpression index = new codearrayindexerexpression ();
// indices屬性是一個能夠支持多維數組的集合。不過這里我們只需要
// 一個簡單的一維數組。
index.indices.add ( new codevariablereferenceexpression (indexvalue));
// targetobject指定了要索引的數組的名稱。
index.targetobject = new codesnippetexpression (arrayname);
return index;
}
codearrayindexerexpression對象處理數組索引方式的種種差異。例如,在c#中數組以arrayname[indexvalue]的方式索引;但在vb.net中,數組以arrayname(indexvalue)的方式索引。
codearrayindexerexpression允許我們忽略這種差異,將注意力集中到其他更重要的問題,例如要索引哪一個數組、要訪問第幾個數組元素。
二、裝配出樹結構
我們可以把前面定義的所有函數加入到一個類,通過構造器初始化,例如:
public codedomprovider()
{
currentnamespace = initializenamespace("testspace");
codetypedeclaration ctd = createclass ("helloworld");
// 把類加入到名稱空間
currentnamespace.types.add (ctd);
codeentrypointmethod mtd = createmethod();
// 把方法加入到類
ctd.members.add (mtd);
codevariabledeclarationstatement variabledeclaration =
declarevariables (typeof (stringbuilder), "sbmessage");
// 把變量聲明加入到方法
mtd.statements.add (variabledeclaration);
codevariabledeclarationstatement array = initializearray
("characters", 'h', 'e', 'l', 'l', 'o', ' ',
'w', 'o', 'r', 'l', 'd');
// 把數組加入到方法
mtd.statements.add (array);
codeiterationstatement loop = createloop("intcharacterindex");
// 把循環加入到方法
mtd.statements.add (loop);
// 數組索引
codearrayindexerexpression index = createarrayindex("characters",
"intcharacterindex");
// 加入一個語句,它將調用sbmessage對象的“append”方法
loop.statements.add (new codemethodinvokeexpression (
new codesnippetexpression ("sbmessage"),"append",
index));
// 循環結束后,輸出所有字符追加到sbmessage對象
// 后得到的結果
mtd.statements.add (new codesnippetexpression
("console.writeline (sbmessage.tostring())"));
}
構造器的運行結果是一個完整的codedom樹結構。可以看到,至此為止我們的所有操作都獨立于目標語言。最后生成的代碼將以屬性的形式導出。
三、輸出生成結果
構造好codedom樹之后,我們就可以較為方便地將代碼以任意.net語言的形式輸出。每一種.net語言都有相應的codeprovider對象,codeprovider對象的creategenerator方法能夠返回一個實現了icodegenerator接口的對象。icodegenerator接口定義了用來生成代碼的所有方法,而且允許我們定義一個用來簡化屬性輸出的輔助方法。下面的輔助方法generatecode負責設置好合適的textwriter以供輸出代碼,以字符串的形式返回結果文本。
private string generatecode (icodegenerator codegenerator)
{
// codegeneratoroptions允許我們指定各種供代碼生成器
// 使用的格式化選項
codegeneratoroptions cop = new codegeneratoroptions();
// 指定格式:花括號的位置
cop.bracingstyle = "c";
// 指定格式:代碼塊的縮進方式
cop.indentstring = " ";
// generatecodefromnamespace要求傳入一個textwriter以
// 容納即將生成的代碼。這個textwriter可以是一個streamwriter、
// 一個stringwriter或一個indentedtextwriter。
// streamwriter可用來將代碼輸出到文件。
// stringwriter可綁定到stringbuilder,后者可作為一個變量引用。
// 在這里,我們把一個stringwriter綁定到stringbuilder sbcode。
stringbuilder sbcode = new stringbuilder();
stringwriter sw = new stringwriter(sbcode);
// 生成代碼!
codegenerator.generatecodefromnamespace(currentnamespace, sw,cop);
return sbcode.tostring();
}
有了這個輔助函數,要獲取各種語言的代碼就相當簡單了:
public string vbcode
{
get
{
vbcodeprovider provider = new vbcodeprovider ();
icodegenerator codegen = provider.creategenerator ();
return generatecode (codegen);
}
}
public string jscriptcode
{
get
{
jscriptcodeprovider provider = new jscriptcodeprovider ();
icodegenerator codegen = provider.creategenerator ();
return generatecode(codegen);
}
}
public string jsharpcode
{
get
{
vjsharpcodeprovider provider = new vjsharpcodeprovider ();
icodegenerator codegen = provider.creategenerator ();
return generatecode (codegen);
}
}
public string csharpcode
{
get
{
csharpcodeprovider provider = new csharpcodeprovider();
icodegenerator codegen = provider.creategenerator ();
return generatorcode (codegen);
}
}
四、顯示出生成的代碼
為輸出代碼,我們要用到一個簡單的.aspx文件,它有四個標簽,分別對應一種.net語言:
<table width="800" border="1">
<tr>
<th>vb.net代碼</th>
</tr>
<tr >
<td>
<asp:label id="vbcode" runat="server" cssclass="code">
</asp:label>
</td>
</tr>
<tr>
<th>
c#代碼</th></tr>
<tr>
<td><asp:label id="csharpcode" runat="server" cssclass="code">
</asp:label></td>
</tr>
<tr>
<th>j#代碼</th></tr>
<tr >
<td>
<asp:label id="jsharpcode" runat="server" cssclass="code">
</asp:label>
</td>
</tr>
<tr>
<th>jscript.net代碼</th>
</tr>
<tr>
<td><asp:label id="jscriptcode" runat="server" cssclass="code">
</asp:label></td>
</tr>
</table>
在后臺執行的代碼中,我們實例化一個前面創建的codedomprovider類的實例,把它生成的代碼賦值給.aspx頁面的相應標簽的text屬性。為了使web頁面中顯示的代碼整齊美觀,有必要做一些簡單的格式化,替換換行符號、空格等,如下所示:
private string formatcode (string codetoformat)
{
string formattedcode = regex.replace (codetoformat, "/n", "<br>");
formattedcode = regex.replace (formattedcode, " " , " ");
formattedcode = regex.replace (formattedcode, ",", ", ");
return formattedcode;
}
下面把生成的代碼顯示到web頁面:
private void page_load(object sender, system.eventargs e)
{
helloworld.codedomprovider codegen = new helloworld.codedomprovider ();
vbcode.text = formatcode (codegen.vbcode);
csharpcode.text = formatcode (codegen.csharpcode);
jscriptcode.text = formatcode (codegen.jscriptcode);
jsharpcode.text = formatcode (codegen.jsharpcode);
page.enableviewstate = false;
}
輸出結果如下:
vb.net代碼
imports system
imports system.text
namespace helloworld
public class hello_world
public shared sub main()
dim sbmessage as system.text.stringbuilder = _
new system.text.stringbuilder
dim characters() as char = new char() {_
microsoft.visualbasic.chrw(72), _
microsoft.visualbasic.chrw(69), _
microsoft.visualbasic.chrw(76), _
microsoft.visualbasic.chrw(76), _
microsoft.visualbasic.chrw(79), _
microsoft.visualbasic.chrw(32), _
microsoft.visualbasic.chrw(87), _
microsoft.visualbasic.chrw(79), _
microsoft.visualbasic.chrw(82), _
microsoft.visualbasic.chrw(76), _
microsoft.visualbasic.chrw(68)}
dim intcharacterindex as integer = 0
do while intcharacterindex < characters.length
sbmessage.append(characters(intcharacterindex))
intcharacterindex = intcharacterindex + 1
loop
console.writeline (sbmessage.tostring())
end sub
end class
end namespace
c#代碼
namespace helloworld
{
using system;
using system.text;
public class hello_world
{
public static void main()
{
system.text.stringbuilder sbmessage = new
system.text.stringbuilder();
char[] characters = new char[] {
'h',
'e',
'l',
'l',
'o',
' ',
'w',
'o',
'r',
'l',
'd'};
for (int intcharacterindex = 0;
intcharacterindex < characters.length;
intcharacterindex = intcharacterindex + 1)
{
sbmessage.append(characters[intcharacterindex]);
}
console.writeline (sbmessage.tostring());
}
}
}
j#代碼
package helloworld;
import system.*;
import system.text.*;
public class hello_world
{
public static void main(string[] args)
{
system.text.stringbuilder sbmessage = new
system.text.stringbuilder();
char[] characters = new char[]
{
'h',
'e',
'l',
'l',
'o',
' ',
'w',
'o',
'r',
'l',
'd'}
;
for (int intcharacterindex = 0;
intcharacterindex < characters.length;
intcharacterindex = intcharacterindex + 1)
{
sbmessage.append(characters[intcharacterindex]);
}
console.writeline (sbmessage.tostring());
}
}
jscript.net代碼
//@cc_on
//@set @debug(off)
import system;
import system.text;
package helloworld
{
public class hello_world
{
public static function main()
{
var sbmessage : system.text.stringbuilder =
new system.text.stringbuilder();
var characters : char[] =
['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'];
for (var intcharacterindex : int = 0;
; intcharacterindex < characters.length;
intcharacterindex = intcharacterindex + 1)
{
sbmessage.append(characters[intcharacterindex]);
}
console.writeline (sbmessage.tostring());
}
}
}
helloworld.hello_world.main();
總結:codedom體現了.net中語言的重要性不如框架的思想。本文示范了如何運用一些常用的codedom類,幾乎每一個使用codedom技術的應用都要用到這些類。作為一種結構化的代碼生成技術,codedom有著無限的潛能,唯一的約束恐怕在于人們的想象力。