CLR自帶了各種語言的編譯器,例如C#、VB等。通過這些編譯器以及反射,可以實現以前在其它環境中做不到的事情:運行時代碼生成和編譯。
作為一個應用,我們以對象工廠作為示例。對象工廠是通過一些標識符,在運行時生成不同對象的一種設計模式,通常的代碼形式為:
public class ObjectFactory
{
public static object CreateInstance(string id)
{
switch (id)
{
case "A":
return new A();
case "B":
return new B();
…
default:
return null;
}
}
這段代碼非常好,但是存在一個問題是,這是一段源代碼,要想在運行時動態增加可以創建的對象就做不到。一種場景就是需要創建的對象是通過配置文件來確定的,那麼就不能使用這種方式了。
CLR環境下反射的存在可以非常方便地實現動態工廠,例如:
public class ObjectFactory2
{
public static object CreateInstance(string id)
{
Type type = Type.GetType(id);
return Activator.CreateInstance(type);
}
}
這裡要求id是一個CLR類型名稱。看上去很漂亮的代碼,問題是這段代碼非常慢,比前一段代碼慢一萬倍以上--原因在於在.Net中,幾乎所有的反射都會涉及到對虛擬機本身的調用,而這種調用是通過COM進行的。此外還涉及到對整個類型系統的搜索等開銷也是很龐大的。
第三種方案,使用一個Hash表以及配合委托進行。在系統初始化的時候創建一個Hash表,表的鍵為對象標識符,表的值為對象的類型,這樣代碼就會變成:
public class ObjectFactory3
{
public static Hashtable m_objectTypes;
public static object CreateInstance(string id)
{
Type type = m_objectTypes(id) as Type;
return Activator.CreateInstance(type);
}
}
在這段代碼中,節省了Type.GetType的時間,但是並沒有回避Activator.CreateInstance的代價,並且在大系統中,使用Hash表的效率也不是很高。
那麼有沒有更快、但是更加靈活的方式呢?回答是使用動態代碼生成和編譯技術。
想法是很簡單的,我們還是回到第一種方式,switch語句,只要我們在運行時讀入需要創建的對象標識符和對象類型,然而按照switch語句的語法創建一個C#源代碼文件,然後編譯,就可以了。這裡涉及到幾個問題:
1、 如何書寫源代碼:這實際上是很簡單的,創建一個StringBuilder,然後往裡面寫字符串就可以了。然而為了提高代碼的可讀性和方便性,我們可以對其進行一些封裝,例如下面這個接口可以完成大部分代碼書寫的工作。
public interface ICodeWriter
{
void WriteLine();
void WriteLine(int count);
void WriteLine(string s);
void WriteLine(string s, params object[] args);
void Write(string s);
void Write(string s, params object[] args);
void CommentLine();
void CommentLine(int count);
void CommentLine(string s);
void CommentLine(string s, params object[] args);
void Indent();
void Indent(int count);
void UnIndent();
void UnIndent(int count);
void WriteIndents();
}
2、 如何組織源代碼:基本上,我們需要下面這些信息:
a) 生成的工廠名稱,以及工廠所需要實現的接口;
b) 需要生成的最終對象的類型,在第一個示例中,最終對象的類型是object,然而我們也可以用其它類型來代替,一個比較好的方式是使用一個所有要創建對象的公共基類類型;
c) 對象標識符:這是一個字符串數組,包含所有對象的標識符;
d) 對象類型:這是一個字符串數組或者類型數組,包含所有要創建的對象類型名稱或者類型。
有了這些信息以後,我們就可以編寫這個工廠的創建程序了:
public class ObjectFactoryBuilder
{
public static string CreateObjectFactorySource(string factoryName, string factoryBaseName, string baseProductName, string [] productIds, string [] productTypes)
{
ICodeWriter writer = new CSharpWriter();
writer.WriteLine("public classs {0}: {1}", factoryName, factoryBaseName); writer.Indent();
writer.WriteLine("public {0} CreateInstance(string id)", baseProductName); writer.Indent();
writer.WriteLine("switch (name)");
writer.Indent();
for (int k = 0; k< productsIds.Length; ++k)
{
Writer. WriteLine("case \"{0}\": return new {1}();", productsIds[k], productTypes[k]); }
Writer.WriteLine("default: return null;");
Writer.Unindent(3);
return Writer.ToString();
}
}
上面這段代碼就可以根據傳入的信息,自動生成符合C#語法的源代碼。
3、 如何編譯:在 System.CodeDom.Compiler名字空間中包含了基本的編譯器支持,Microsoft.CSharp名字空間中提供了C#編譯器的實際對象。首先創建一個CompilerParameter對象,設置編譯選項,然後用下面語句創建編譯器並且編譯代碼:
CompilerParameters cp = new CompilerParameters();
// 設置 cp.ReferencedAssemblIEs CodeDomProvider provider = new CSharpCodeProvider();
CompileResult cr = provider.CompileAssemblyFromSource(cp, source);
其中,source是一個字符串,包含從CreateObjectFactorySource得到的工廠源代碼。如果編譯成功,那麼cr.Errors.Count == 0,編譯生成的配件就是cr.CompiledAssembly。假設我們的工廠名字是ObjectFactory4,實現的接口是IObjectFactory,那麼得到配件後,可以寫:
Assembly asm = cr.CompiledAssembly;
IObjectFactory factory = asm.CreateInstance("ObjectFactory4") as IObjectFactory;
基於這種工作模式,我們需要在我們自己的配件中定義一個基類或者接口IObjectFactory,然後讓動態生成的工廠繼承基類或者實現接口,這樣我們就可以調用這個工廠來創建對象了。
本文出自:http://www.cnblogs.com/xlshcn/archive/2007/11/21/runtimecodegen.Html