【.net 深呼吸】細說CodeDom(5):類型成員


 

前文中,老周已經厚着臉皮介紹了類型的聲明,類型里面包含的自然就是類型成員了,故,順着這個思路,今天咱們就了解一下如何向類型添加成員。

咱們都知道,常見的類型成員,比如字段、屬性、方法、事件。表示代碼成員的類型與 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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM