代碼生成:CodeDom分析(一)

自動代碼生成

曾經有很多同僚發表過類似的觀點,程序員的本質,就是懶。

 

這點我是相當的贊同,因爲我也曾幻想過,寫個替我寫代碼的程序,然後每天上班把程序打開,下班把程序關掉。其他時間打打遊戲,看看動畫。

 

當然現實是殘酷的,一來我沒有那個水平寫那麼智能的程序,二來真寫出來估計也會被同僚打死,當然別人寫出這種程序,我也肯定在他發表之前,打死他。

 

我最開始寫的自動代碼生成工具,是爲CSV表格生成自動類,一張表對應一個類,有獲取行列的方法。後來才知道這玩意和protobuf思想差不多,然而那時候我還沒有用到protobuf。後來又寫了一個網絡協議處理的半自動工具,只要在Unity的inspector配置好指定的協議,就可以自動生成一部分框架代碼。

 

那時候的自動代碼生成工具,使用的是最簡單的類模板+關鍵字替換,以及函數模板+註釋標籤+代碼插入的方式實現的。

 

雖然簡單粗暴,但是確實避免了自己寫一些繁瑣的重複代碼。

 

隨後,在最近的項目中是用到了XLuaFramework,這個框架使用pureMVC架構組織lua代碼,這就需要在拼好UI的Prefab之後,手動的獲取每個控件的路徑,手動創建Mode,Ctrl和View頁面,手動填寫響應函數,手動……

 

這聽起來頭皮發麻,別說累不累,這麼多手動,我肯定會忘記其中一兩項……再加上某些缺心眼的策劃經常改需求,一旦prefab的頁面發生了變化,就又會引發一場新的災難。

 

於是我這次做的東西比第一次稍微複雜一點,構建了一個LuaCodeBuilder,利用代碼構建器+多類註釋標籤的方式,實現了通過拼好的UGUI的prefab自動生成MVC架構框架頁面,並把所有的初始化,路徑查詢,回調函數注入,以及其他一些可以自動化的代碼都寫好。並且確定,在//AFX_XXX_BEGIN和//AFX_XXX_END之內的代碼爲自動生成代碼,其他程序員不允許修改這裏面的代碼。

 

這麼做還是有兩個小小的缺點:

 

一是出現一大堆標籤,看起來美感稍微差一些,更要命的是程序員一旦把代碼寫到兩個標籤中間,下次再生成的時候,就有可能抹掉這些代碼。

 

二是很多代碼,如自動生成的回調函數函數體,在不用的時候,只能手動刪除,不能自動刪除。

 

於是我想要做這樣一件事情,解決上面兩個問題。一個是通過解析器解析整個工程的代碼,估計現有的IDE都具備這樣的能力,然後根據UGUI的Prafab生成一個描述文件,描述哪些東西是自動生成的。這樣可以更加精確的定位自動代碼和手動修改的代碼。

 

二是可以使用更少的調用Builder的代碼,來生成更復雜的程序。

 

此外還有一個好處是,使得生成更復雜的代碼成爲可能。

 

如果想要實現這個目的,單純的使用CodeBuilder是不夠的。於是我發現了微軟已經提供好了CodeDom,也就是代碼對象模型這個好東西,雖然他現在不支持Lua代碼,但是我可以學習一下他的思想——在內存中,用樹狀圖,表示程序。

 

這就使得對程序的自動修改,不再是文本和文本替換級別的,而是對象級別的。

 

同時,我還要去尋找一個開源的,或者自己寫一個代碼解析工具,把現有Lua代碼解析成Dom,並構建起不同源文件之間的聯繫——這樣我可以更靈活的,自動重構代碼,比如即使用戶(指其他程序員)修改了一個變量,比如一個label,lbl_user_name的名字,並且自己編寫了一些代碼,比如lbl_user_nane:SetText("xxxx"),當策劃將這個lbl改名的時候,我也能夠自動重構代碼,將其所有引用的地方改成新的變量名。

 

爲了實現這個目標,我需要先學習一下微軟的CodeDom的原理。

CodeDom簡介

CodeDom就是文檔代碼對象模型。

 

 

這張圖是一個不太負責任的代碼內存分析模型,不夠詳細,也不夠全面,但是它大概的展示了一個源代碼文件,其大概結構。

 

要在內存中表示一個源代碼文件,首先應該將其分解成各種元組件,然後在將他們組成一個樹圖。

 

微軟的CodeDom更多是用來生成代碼而不是解析用的,用來生成的原材料可能是一張CSV表,一個protobuf源文件,或者是一個可視化代碼工具——或許這個也是個不錯的入手點:)

CodeDom

使用CodeDom自動生成代碼,分三部分操作。

1是利用CodeDom構建代碼。

2是利用IndentedTextWriter輸入帶有縮進的代碼文件。

3是利用GenerateCodeFromCompileUnit編譯代碼。

 

這裏我主要涉及的是1和2。

 

1 CodeCompileUnit

 

在我之前構建代碼的工具中,採用的是文本內存模型的方式,也就是用一個類對象,裏面包含一個List<String>數據結構,來記錄代碼的每一行。然後通過字符串匹配以及正則表達式匹配,子串查找等方式,找到目標代碼,目標註釋標籤等,在進行代碼刪除,插入——基本上不存在修改的操作,只是刪除和插入新代碼。

 

單純對於CodeDom而言,微軟只提供了生成整片代碼的機能——看註釋就能知道:

 

 

大意是這代碼是自動生成的,重生成代碼會抹掉你的代碼,亂改代碼可能會引發不可預知的錯誤。

 

用它來實現我的最終目標似乎還遠遠不夠,但是分析代碼DOM模型還是很有價值的一步。在CodeDom模型中,CodeCompileUnit和我用來組織源代碼的類作用幾乎是一致的。

 

最簡單的理解,一個CodeComplieUnit可以代表一個.cs文件。他是CodeDom在內存中表示一段代碼的根節點。也就是代碼樹的根節點。

 

2 CodeNamespace

CodeNamespace在CodeDom中代表着一個命名空間。

 

這裏需要注意幾個地方:

 

  1. 一個.cs文件裏面有多個命名空間是完全OK,並且合法的。

 

  1. CodeNamespaceImport可以用來增加using xxxx的代碼。

 

CodeNamespace codeNamespace = new CodeNamespace(ns_name);        
List<CodeNamespaceImport> cni_list = new List<CodeNamespaceImport>();
cni_list.Add(new CodeNamespaceImport("UnityEngine"));
cni_list.Add(new CodeNamespaceImport("System.Collections.Generic"));
foreach (var cni in cni_list)
{
    codeNamespace.Imports.Add(cni);
}

 

  1. using代碼塊是屬於namespace的

而不像很多Unity或者其他IDE,默認把using放到了namespace外面,網上有很多關於using到底應該在namespace裏面還是外面的爭論,按照微軟官方的做法,只能在CodeNamespace中添加命名空間引用,而不能在CodeCompileUnit裏面添加namespace引用來看,微軟官方是支持吧using代碼扔到namespace裏面的。

 

     2.向CodeCompileUnit添加Namespace

 

CodeCompileUnit codeUnit = new CodeCompileUnit();    
CodeNamespace codeNamespace = new CodeNamespace(ns_name);  
...
codeUnit.Namespaces.Add(codeNamespace);

 

3 輸出代碼文件

這用到兩個核心類:IndentedTextWriter和CodeDomProvider

 

如果直接用TextWriter輸出代碼,將會非常難看,因爲沒有縮進。IndentedTextWriter對輸出代碼進行了一層包裝,使得生成的代碼帶有縮進。

 

實例代碼如下:

其中codeUnit是包含了兩個空的namespace的CodeCompileUnit對象。

 

string tabString = "    ";
string outputPath = "Assets/SharpCodeGen/outputs/test.cs";
IndentedTextWriter itw = new IndentedTextWriter(new StreamWriter(outputPath, false), tabString);
CodeDomProvider provide = new CSharpCodeProvider();
provide.GenerateCodeFromCompileUnit(codeUnit, itw, new CodeGeneratorOptions());
itw.Flush();
itw.Close();

 

輸出結果如下:

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章