前文中,老周已經厚着臉皮介紹了類型的聲明,類型里面包含的自然就是類型成員了,故,順着這個思路,今天咱們就了解一下如何向類型添加成員。
咱們都知道,常見的類型成員,比如字段、屬性、方法、事件。表示代碼成員的類型與 CodeTypeDeclaration 類有着共同的基類—— CodeTypeMember。畢竟類型也好,類型成員也好,都有共同特征。
下面,簡單認識一下相關的幾個類型,心里也有個譜。
CodeMemberField:字段
CodeMemberProperty:屬性
CodeMemberMethod:方法
CodeConstructor:構造函數
CodeMemberEvent:事件
下面來個例子,聲明一個Dog類,並且為它添加一個帶有單個 string 類型參數的構造函數,以及一個名為Age的屬性。
/* * 從外向里,層層創建 */ // 編譯單元 CodeCompileUnit unit = new CodeCompileUnit(); // 命名空間 CodeNamespace ns = new CodeNamespace("Sample"); // 引入一個System命名空間 ns.Imports.Add(new CodeNamespaceImport(nameof(System))); unit.Namespaces.Add(ns); // 聲明類型 CodeTypeDeclaration t1 = new CodeTypeDeclaration("Dog"); ns.Types.Add(t1); // 准備用於構造函數的參數 CodeParameterDeclarationExpression p = new CodeParameterDeclarationExpression(typeof(string), "dogName"); // 構造函數 CodeConstructor ctr = new CodeConstructor(); ctr.Parameters.Add(p); t1.Members.Add(ctr); // 屬性 CodeMemberProperty prty = new CodeMemberProperty(); prty.Name = "Age"; // 屬性值的類型 prty.Type = new CodeTypeReference(typeof(int)); // 公共屬性 prty.Attributes = MemberAttributes.Public; prty.HasGet = true; //可讀 prty.HasSet = true; //可寫 t1.Members.Add(prty);
在使用 CodeMemberProperty 定義屬性時,HasGet和HasSet屬性用於指定屬性是否可讀或可寫。
上面例子生成的代碼如下:
這里生成的屬性是帶有 virtual 關鍵字,即虛成員,將來可以被派生類 override 的。關於如何去掉這個 virtual 關鍵字,可以把它變成 Finnal 成員。 關於這個 Attributes 屬性,老周向大伙伴們表示歉意。
因為 MemberAttributes 枚舉沒有附加 Flags 特性說明,老周當初以為它不可以進行組合使用,不過,后來老周經過試驗,發現這個渾蛋是可以進行位運算的。老周計划后面再弄一篇文章來說這個事情。
比如,上面的 Age 屬性,如果想去掉 virtual 關鍵字,可以這樣寫:
prty.Attributes = MemberAttributes.Public | MemberAttributes.Final;
再舉一個例子,聲明一個名為Demo的類,里面有個私有字段 ma, 通過公共的構造函數中的參數把數據傳遞給 ma 字段。
CodeCompileUnit unit = new CodeCompileUnit(); CodeNamespace ns = new CodeNamespace("Data"); unit.Namespaces.Add(ns); ns.Imports.Add(new CodeNamespaceImport(nameof(System))); // 聲明類型 CodeTypeDeclaration type1 = new CodeTypeDeclaration("Demo"); ns.Types.Add(type1); // 字段 CodeMemberField fd = new CodeMemberField(typeof(string), "ma"); fd.Attributes = MemberAttributes.Private; //私有 type1.Members.Add(fd); // 構造函數 // 參數 CodeParameterDeclarationExpression pc = new CodeParameterDeclarationExpression(typeof(string), "a"); CodeConstructor ctor = new CodeConstructor(); type1.Members.Add(ctor); ctor.Parameters.Add(pc); // 公共 ctor.Attributes = MemberAttributes.Public; // 賦值語句 CodeAssignStatement ass = new CodeAssignStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), fd.Name), new CodeVariableReferenceExpression(pc.Name)); // 將賦值語句加入到方法體中 ctor.Statements.Add(ass);
用 CodeMemberField 聲明字段很好辦,只要指定字段類型和名字就行了。 構造函數是一個特殊的方法,CodeConstructor也繼承了一個Statements 集合,表示的是方法體中的語句。在上面例子中,構造函數內只要一個賦值語句就夠了。
生成的C#代碼如下圖所示。
-------------------------------------------------------------------------------------
下面看看方法的生成,先舉個簡單點的例子,生成一個叫PlayMusic,無參數,返回值類型為void的方法。
CodeMemberMethod m = new CodeMemberMethod(); m.Name = "PlayMusic"; CodeDomProvider prvd = CodeDomProvider.CreateProvider("cs"); prvd.GenerateCodeFromMember(m, Console.Out, null);
生成結果如下圖所示。
再復雜一點,下面咱們生成一個類,再於類中定義一個帶兩個參數,且返回非void類型的方法。
CodeTypeDeclaration tp = new CodeTypeDeclaration("Calculator"); tp.Attributes = MemberAttributes.Public; CodeMemberMethod m = new CodeMemberMethod(); tp.Members.Add(m); // 方法名稱 m.Name = "Add"; // 公共方法 m.Attributes = MemberAttributes.Public | MemberAttributes.Final; // 返回值 m.ReturnType = new CodeTypeReference(typeof(int)); // 兩個參數 CodeParameterDeclarationExpression p1 = new CodeParameterDeclarationExpression(typeof(int), "m"); CodeParameterDeclarationExpression p2 = new CodeParameterDeclarationExpression(typeof(int), "n"); m.Parameters.Add(p1); m.Parameters.Add(p2); /* * 在方法中生成語句 return m + n */ // 運算表達式 m+n CodeBinaryOperatorExpression addexp = new CodeBinaryOperatorExpression(); addexp.Operator = CodeBinaryOperatorType.Add; addexp.Left = new CodeVariableReferenceExpression(p1.Name); addexp.Right = new CodeVariableReferenceExpression(p2.Name); // return 語句 CodeMethodReturnStatement retstm = new CodeMethodReturnStatement(addexp); m.Statements.Add(retstm); CodeDomProvider provider = CodeDomProvider.CreateProvider("cs"); CodeGeneratorOptions opt = new CodeGeneratorOptions { BracingStyle = "C" }; provider.GenerateCodeFromType(tp, Console.Out, opt);
ReturnType用於設置方法的返回值類型,方法參數使用 CodeParameterDeclarationExpression 來定義,屬性於表達式。在方法的代碼塊中,如果要進行返回,就要使用 return 語句,CodeMethodReturnStatement類表示的就是return語句。使用時要指定一個用計算返回值的表達式,如果不提供表達式,就直接返回(跳出),好比如這樣:
return;
結果會生成這樣的代碼:
方法成員里面,有一種方法比較特殊,它就是入口點,即Main方法,聲明入口點方法,可以用專門的類—— CodeEntryPointMethod。初始化時無需指定名字,因為它的名字是固定的。入口點的返回值類型通常是 void 或者 int,參數一般是一個字符串數組,表示傳入的命令行參數,當然也可以是無參的。
下面代碼定義一個Program類,然后在類中加一個入口點方法,在入口點方法中調用Console.WriteLine輸出字符串。
CodeTypeDeclaration t = new CodeTypeDeclaration("Program"); // 入口點方法 CodeEntryPointMethod main = new CodeEntryPointMethod(); t.Members.Add(main); // 調用一個方法 CodeMethodReferenceExpression mref = new CodeMethodReferenceExpression(new CodeTypeReferenceExpression(typeof(Console)), nameof(Console.WriteLine)); CodeMethodInvokeExpression invoke = new CodeMethodInvokeExpression(mref, new CodePrimitiveExpression("hello pig")); main.Statements.Add(new CodeExpressionStatement(invoke));
生成的代碼如下圖所示。
如果要將生成的代碼編譯為.exe的話,入口點是必須的,否則會“呵呵”的。
-------------------------------------------------------------------------------------
好,喝了3杯稻花香,下面咱們繼續,現在該來生成一下事件成員了。
下面代碼單獨生成了一個 Click 事件。
CodeMemberEvent ev = new CodeMemberEvent(); ev.Attributes = MemberAttributes.Public; ev.Name = "Click"; ev.Type = new CodeTypeReference(typeof(System.EventHandler)); CodeDomProvider p = CodeDomProvider.CreateProvider("cs"); p.GenerateCodeFromMember(ev, Console.Out, null);
注意,事件的類型是委托,因此,Type屬性所指定的類型必須是委托類型,雖然在代碼生成階段你可以亂寫,但是,如果事件的類型不是委托,在編譯的時候,編譯器會給你臉色看的。所以,咱們還是別太任性了,該指定什么類型就什么類型。
生成的事件如下:
大家想必也知道,在.net類庫中有個事件委托叫 EventHandler<TEventArgs>,這個委托的好處使得我們不必要去為各種自定義事件聲明委托,一般來說,TEventArgs指代的是 EventArgs 類或者它的子類。
下面我們就用這帶有泛型參數的事件委托來生成事件定義代碼。
先上代碼,大伙看完代碼后我再講解一下。
CodeNamespace ns = new CodeNamespace("TestSomething"); ns.Imports.Add(new CodeNamespaceImport(nameof(System))); // 自定義事件參數類 CodeTypeDeclaration custEvarg = new CodeTypeDeclaration("SubmitEventArgs"); ns.Types.Add(custEvarg); // 基類 custEvarg.BaseTypes.Add(new CodeTypeReference(typeof(EventArgs))); // 字段 CodeMemberField dtf = new CodeMemberField(typeof(byte), "m_data"); custEvarg.Members.Add(dtf); dtf.Attributes = MemberAttributes.Private; // 屬性 CodeMemberProperty dtpry = new CodeMemberProperty(); custEvarg.Members.Add(dtpry); dtpry.Name = "Data"; dtpry.Attributes = MemberAttributes.Public | MemberAttributes.Final; dtpry.Type = new CodeTypeReference(typeof(byte)); // 只讀 dtpry.HasGet = true; dtpry.HasSet = false; // 返回屬性值 CodeMethodReturnStatement ret = new CodeMethodReturnStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), dtf.Name)); dtpry.GetStatements.Add(ret); // 構造函數 CodeConstructor ctor = new CodeConstructor(); custEvarg.Members.Add(ctor); ctor.Attributes = MemberAttributes.Public; ctor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(byte), "data")); ctor.Statements.Add(new CodeAssignStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), dtf.Name), new CodeVariableReferenceExpression("data"))); /**************************************************************/ CodeTypeDeclaration testtype = new CodeTypeDeclaration("Test"); ns.Types.Add(testtype); // 事件 CodeMemberEvent eve = new CodeMemberEvent(); testtype.Members.Add(eve); eve.Name = "OnSubmit"; eve.Attributes = MemberAttributes.Public; eve.Type = new CodeTypeReference($"EventHandler`1[{custEvarg.Name}]"); CodeDomProvider p = CodeDomProvider.CreateProvider("cs"); p.GenerateCodeFromNamespace(ns, Console.Out, null);
代碼是蠻長的,不過相信有一部分你是看得懂的。
首先,聲明了一個從EventArgs類派生的類,叫SubmitEventArgs。
CodeTypeDeclaration custEvarg = new CodeTypeDeclaration("SubmitEventArgs"); custEvarg.BaseTypes.Add(new CodeTypeReference(typeof(EventArgs)));
然后加了個私有字段,名為m_data,類型為byte,可以通過Data返回。這個字段的值可以通過傳給構造函數參數的值來設置。
第二個類是Test類,它有一個公共事件OnSubmit,這里才是代碼的重點,我再復制一遍。
CodeMemberEvent eve = new CodeMemberEvent(); testtype.Members.Add(eve); eve.Name = "OnSubmit"; eve.Attributes = MemberAttributes.Public; eve.Type = new CodeTypeReference($"EventHandler`1[{custEvarg.Name}]");
這里比較難處理的是事件的委托類型,因為我們用的是 EventHandler<TEventArgs>,它帶有一個泛型參數,並且泛型參數類型是我們前面要生成的 SubmitEventArgs 類。那么,帶泛型參數的類型的完整名字怎么輸呢,它的格式是這樣的:
<類型名字>`<泛型參數個數>[<類型列表>,……n]
如上面代碼中,EventHandler 類有1個泛型參數,所以后面的數字是1,即EventHandler`1,而泛型的類型就緊跟其后,用中括號包起來,表示是一個數組。
比如,某類名叫 Order,它有2個泛型參數,那么 Order<byte, int>的寫法是:
Order`2[System.Byte, System.Int32]
如果類型要寫上程序集、版本號之類的信息,就再套一層中括號,把類型括起來。比如
Order`2[[System.Byte,mscorelib,Version=4.0.0.0,PublicKeyToken=xxxxxx], [System.Int32,mscorelib,Version=4.0.0.0]]
如果類型有4個泛型參數,就寫成
Order`5[……]
總之類型名稱后的`之后的數字就是泛型參數的個數,中括號是類型列表。
如果你不知道怎么寫,告訴你一招,很簡單,不用記,獲取類型的Type,再看看它的FullName屬性就知道了。至於那個“`”字符,就在鍵盤左上角,數字1左邊那個。看圖
最終生成的代碼如下圖所示。
怎么樣,刺激吧。
--------------------------------------------------------------------------------------
下面說一個讓不少剛接觸 CodeDom 的朋友相當頭痛的東東——枚舉類型怎么加成員。
你就把枚舉的成員當成是字段來處理就OK了,看一個例子。
CodeTypeDeclaration entype = new CodeTypeDeclaration("CloseMode"); // 指定它是枚舉類型 entype.IsEnum = true; // 添加成員 CodeMemberField m1 = new CodeMemberField(); m1.Name = "Restart"; CodeMemberField m2 = new CodeMemberField(); m2.Name = "Shutdown"; CodeMemberField m3 = new CodeMemberField(); m3.Name = "None"; entype.Members.Add(m1); entype.Members.Add(m2); entype.Members.Add(m3); CodeDomProvider prd = CodeDomProvider.CreateProvider("cs"); prd.GenerateCodeFromType(entype, Console.Out, null);
這個枚舉有三個成員,對於枚舉,字段的類型可以不設置,只設置成員名字就行了,反正你想啊,枚舉的成員默認是int,頂多也就是byte、short這些,都是整型的,所以,只要有成員名字就可以了。
生成結果如下。
如果要想為某些成員賦特定數值,可以設置 InitExpression 屬性。比如這樣
CodeMemberField m1 = new CodeMemberField(); m1.Name = "Restart"; m1.InitExpression = new CodePrimitiveExpression(15);
這樣,生成的枚舉成員就有數值了。
好好,今天老周給大伙介紹了類型成員代碼的生成,基本覆蓋了屬性、字段、方法和事件。就講這么多吧,講太多了大家不好消化。
改天有空,老周繼續跟大伙聊 CodeDom。