前文中,老周已经厚着脸皮介绍了类型的声明,类型里面包含的自然就是类型成员了,故,顺着这个思路,今天咱们就了解一下如何向类型添加成员。
    咱们都知道,常见的类型成员,比如字段、属性、方法、事件。表示代码成员的类型与 CodeTypeDeclaration 类有着共同的基类—— CodeTypeMember。毕竟类型也好,类型成员也好,都有共同特征。
    下面,简单认识一下相关的几个类型,心里也有个谱。
    CodeMemberField:字段
    CodeMemberProperty:属性
    CodeMemberMethod:方法
    CodeConstructor:构造函数
    CodeMemberEvent:事件

    下面来个例子,声明一个Dog类,并且为它添加一个带有单个 string 类型参数的构造函数,以及一个名为Age的属性。

    1. /*
    2. * 从外向里,层层创建
    3. */
    4. // 编译单元
    5. CodeCompileUnit unit = new CodeCompileUnit();
    6. // 命名空间
    7. CodeNamespace ns = new CodeNamespace("Sample");
    8. // 引入一个System命名空间
    9. ns.Imports.Add(new CodeNamespaceImport(nameof(System)));
    10. unit.Namespaces.Add(ns);
    11. // 声明类型
    12. CodeTypeDeclaration t1 = new CodeTypeDeclaration("Dog");
    13. ns.Types.Add(t1);
    14. // 准备用于构造函数的参数
    15. CodeParameterDeclarationExpression p = new CodeParameterDeclarationExpression(typeof(string), "dogName");
    16. // 构造函数
    17. CodeConstructor ctr = new CodeConstructor();
    18. ctr.Parameters.Add(p);
    19. t1.Members.Add(ctr);
    20. // 属性
    21. CodeMemberProperty prty = new CodeMemberProperty();
    22. prty.Name = "Age";
    23. // 属性值的类型
    24. prty.Type = new CodeTypeReference(typeof(int));
    25. // 公共属性
    26. prty.Attributes = MemberAttributes.Public;
    27. prty.HasGet = true; //可读
    28. prty.HasSet = true; //可写
    29. t1.Members.Add(prty);

    在使用 CodeMemberProperty 定义属性时,HasGet和HasSet属性用于指定属性是否可读或可写。
    上面例子生成的代码如下:
    image.png
    这里生成的属性是带有 virtual 关键字,即虚成员,将来可以被派生类 override 的。关于如何去掉这个 virtual 关键字,可以把它变成 Finnal 成员。 关于这个 Attributes 属性,老周向大伙伴们表示歉意。
    因为 MemberAttributes 枚举没有附加 Flags 特性说明,老周当初以为它不可以进行组合使用,不过,后来老周经过试验,发现这个浑蛋是可以进行位运算的。老周计划后面再弄一篇文章来说这个事情。
    比如,上面的 Age 属性,如果想去掉 virtual 关键字,可以这样写:

    1. prty.Attributes = MemberAttributes.Public | MemberAttributes.Final;

    再举一个例子,声明一个名为Demo的类,里面有个私有字段 ma, 通过公共的构造函数中的参数把数据传递给 ma 字段。

    1. CodeCompileUnit unit = new CodeCompileUnit();
    2. CodeNamespace ns = new CodeNamespace("Data");
    3. unit.Namespaces.Add(ns);
    4. ns.Imports.Add(new CodeNamespaceImport(nameof(System)));
    5. // 声明类型
    6. CodeTypeDeclaration type1 = new CodeTypeDeclaration("Demo");
    7. ns.Types.Add(type1);
    8. // 字段
    9. CodeMemberField fd = new CodeMemberField(typeof(string), "ma");
    10. fd.Attributes = MemberAttributes.Private; //私有
    11. type1.Members.Add(fd);
    12. // 构造函数
    13. // 参数
    14. CodeParameterDeclarationExpression pc = new CodeParameterDeclarationExpression(typeof(string), "a");
    15. CodeConstructor ctor = new CodeConstructor();
    16. type1.Members.Add(ctor);
    17. ctor.Parameters.Add(pc);
    18. // 公共
    19. ctor.Attributes = MemberAttributes.Public;
    20. // 赋值语句
    21. CodeAssignStatement ass = new CodeAssignStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), fd.Name), new CodeVariableReferenceExpression(pc.Name));
    22. // 将赋值语句加入到方法体中
    23. ctor.Statements.Add(ass);

    用 CodeMemberField 声明字段很好办,只要指定字段类型和名字就行了。 构造函数是一个特殊的方法,CodeConstructor也继承了一个Statements 集合,表示的是方法体中的语句。在上面例子中,构造函数内只要一个赋值语句就够了。
    生成的C#代码如下图所示。
    image.png

    ——————————————————————————————————————————-
    下面看看方法的生成,先举个简单点的例子,生成一个叫PlayMusic,无参数,返回值类型为void的方法。

    1. CodeMemberMethod m = new CodeMemberMethod();
    2. m.Name = "PlayMusic";
    3. CodeDomProvider prvd = CodeDomProvider.CreateProvider("cs");
    4. prvd.GenerateCodeFromMember(m, Console.Out, null);

    生成结果如下图所示。
    image.png

    再复杂一点,下面咱们生成一个类,再于类中定义一个带两个参数,且返回非void类型的方法。

    1. CodeTypeDeclaration tp = new CodeTypeDeclaration("Calculator");
    2. tp.Attributes = MemberAttributes.Public;
    3. CodeMemberMethod m = new CodeMemberMethod();
    4. tp.Members.Add(m);
    5. // 方法名称
    6. m.Name = "Add";
    7. // 公共方法
    8. m.Attributes = MemberAttributes.Public | MemberAttributes.Final;
    9. // 返回值
    10. m.ReturnType = new CodeTypeReference(typeof(int));
    11. // 两个参数
    12. CodeParameterDeclarationExpression p1 = new CodeParameterDeclarationExpression(typeof(int), "m");
    13. CodeParameterDeclarationExpression p2 = new CodeParameterDeclarationExpression(typeof(int), "n");
    14. m.Parameters.Add(p1);
    15. m.Parameters.Add(p2);
    16. /*
    17. * 在方法中生成语句 return m + n
    18. */
    19. // 运算表达式 m+n
    20. CodeBinaryOperatorExpression addexp = new CodeBinaryOperatorExpression();
    21. addexp.Operator = CodeBinaryOperatorType.Add;
    22. addexp.Left = new CodeVariableReferenceExpression(p1.Name);
    23. addexp.Right = new CodeVariableReferenceExpression(p2.Name);
    24. // return 语句
    25. CodeMethodReturnStatement retstm = new CodeMethodReturnStatement(addexp);
    26. m.Statements.Add(retstm);
    27. CodeDomProvider provider = CodeDomProvider.CreateProvider("cs");
    28. CodeGeneratorOptions opt = new CodeGeneratorOptions
    29. {
    30. BracingStyle = "C"
    31. };
    32. provider.GenerateCodeFromType(tp, Console.Out, opt);

    ReturnType用于设置方法的返回值类型,方法参数使用 CodeParameterDeclarationExpression 来定义,属性于表达式。在方法的代码块中,如果要进行返回,就要使用 return 语句,CodeMethodReturnStatement类表示的就是return语句。使用时要指定一个用计算返回值的表达式,如果不提供表达式,就直接返回(跳出),好比如这样:
    return;
    结果会生成这样的代码:
    image.png

    方法成员里面,有一种方法比较特殊,它就是入口点,即Main方法,声明入口点方法,可以用专门的类—— CodeEntryPointMethod。初始化时无需指定名字,因为它的名字是固定的。入口点的返回值类型通常是 void 或者 int,参数一般是一个字符串数组,表示传入的命令行参数,当然也可以是无参的。
    下面代码定义一个Program类,然后在类中加一个入口点方法,在入口点方法中调用Console.WriteLine输出字符串。

    1. CodeTypeDeclaration t = new CodeTypeDeclaration("Program");
    2. // 入口点方法
    3. CodeEntryPointMethod main = new CodeEntryPointMethod();
    4. t.Members.Add(main);
    5. // 调用一个方法
    6. CodeMethodReferenceExpression mref = new CodeMethodReferenceExpression(new CodeTypeReferenceExpression(typeof(Console)), nameof(Console.WriteLine));
    7. CodeMethodInvokeExpression invoke = new CodeMethodInvokeExpression(mref, new CodePrimitiveExpression("hello pig"));
    8. main.Statements.Add(new CodeExpressionStatement(invoke));

    生成的代码如下图所示。
    image.png
    如果要将生成的代码编译为.exe的话,入口点是必须的,否则会“呵呵”的。

    ——————————————————————————————————————————-
    好,喝了3杯稻花香,下面咱们继续,现在该来生成一下事件成员了。
    下面代码单独生成了一个 Click 事件。

    1. CodeMemberEvent ev = new CodeMemberEvent();
    2. ev.Attributes = MemberAttributes.Public;
    3. ev.Name = "Click";
    4. ev.Type = new CodeTypeReference(typeof(System.EventHandler));
    5. CodeDomProvider p = CodeDomProvider.CreateProvider("cs");
    6. p.GenerateCodeFromMember(ev, Console.Out, null);

    注意,事件的类型是委托,因此,Type属性所指定的类型必须是委托类型,虽然在代码生成阶段你可以乱写,但是,如果事件的类型不是委托,在编译的时候,编译器会给你脸色看的。所以,咱们还是别太任性了,该指定什么类型就什么类型。
    生成的事件如下:
    image.png

    大家想必也知道,在.net类库中有个事件委托叫 EventHandler,这个委托的好处使得我们不必要去为各种自定义事件声明委托,一般来说,TEventArgs指代的是 EventArgs 类或者它的子类。
    下面我们就用这带有泛型参数的事件委托来生成事件定义代码。
    先上代码,大伙看完代码后我再讲解一下。

    1. CodeNamespace ns = new CodeNamespace("TestSomething");
    2. ns.Imports.Add(new CodeNamespaceImport(nameof(System)));
    3. // 自定义事件参数类
    4. CodeTypeDeclaration custEvarg = new CodeTypeDeclaration("SubmitEventArgs");
    5. ns.Types.Add(custEvarg);
    6. // 基类
    7. custEvarg.BaseTypes.Add(new CodeTypeReference(typeof(EventArgs)));
    8. // 字段
    9. CodeMemberField dtf = new CodeMemberField(typeof(byte), "m_data");
    10. custEvarg.Members.Add(dtf);
    11. dtf.Attributes = MemberAttributes.Private;
    12. // 属性
    13. CodeMemberProperty dtpry = new CodeMemberProperty();
    14. custEvarg.Members.Add(dtpry);
    15. dtpry.Name = "Data";
    16. dtpry.Attributes = MemberAttributes.Public | MemberAttributes.Final;
    17. dtpry.Type = new CodeTypeReference(typeof(byte));
    18. // 只读
    19. dtpry.HasGet = true;
    20. dtpry.HasSet = false;
    21. // 返回属性值
    22. CodeMethodReturnStatement ret = new CodeMethodReturnStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), dtf.Name));
    23. dtpry.GetStatements.Add(ret);
    24. // 构造函数
    25. CodeConstructor ctor = new CodeConstructor();
    26. custEvarg.Members.Add(ctor);
    27. ctor.Attributes = MemberAttributes.Public;
    28. ctor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(byte), "data"));
    29. ctor.Statements.Add(new CodeAssignStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), dtf.Name), new CodeVariableReferenceExpression("data")));
    30. /**************************************************************/
    31. CodeTypeDeclaration testtype = new CodeTypeDeclaration("Test");
    32. ns.Types.Add(testtype);
    33. // 事件
    34. CodeMemberEvent eve = new CodeMemberEvent();
    35. testtype.Members.Add(eve);
    36. eve.Name = "OnSubmit";
    37. eve.Attributes = MemberAttributes.Public;
    38. eve.Type = new CodeTypeReference($"EventHandler`1[{custEvarg.Name}]");
    39. CodeDomProvider p = CodeDomProvider.CreateProvider("cs");
    40. p.GenerateCodeFromNamespace(ns, Console.Out, null);

    代码是蛮长的,不过相信有一部分你是看得懂的。
    首先,声明了一个从EventArgs类派生的类,叫SubmitEventArgs。

    1. CodeTypeDeclaration custEvarg = new CodeTypeDeclaration("SubmitEventArgs");
    2. custEvarg.BaseTypes.Add(new CodeTypeReference(typeof(EventArgs)));

    然后加了个私有字段,名为m_data,类型为byte,可以通过Data返回。这个字段的值可以通过传给构造函数参数的值来设置。
    第二个类是Test类,它有一个公共事件OnSubmit,这里才是代码的重点,我再复制一遍。

    1. CodeMemberEvent eve = new CodeMemberEvent();
    2. testtype.Members.Add(eve);
    3. eve.Name = "OnSubmit";
    4. eve.Attributes = MemberAttributes.Public;
    5. eve.Type = new CodeTypeReference($"EventHandler`1[{custEvarg.Name}]");

    这里比较难处理的是事件的委托类型,因为我们用的是 EventHandler,它带有一个泛型参数,并且泛型参数类型是我们前面要生成的 SubmitEventArgs 类。那么,带泛型参数的类型的完整名字怎么输呢,它的格式是这样的:
    <类型名字><泛型参数个数>[<类型列表>,……n]<br />如上面代码中,EventHandler 类有1个泛型参数,所以后面的数字是1,即EventHandler1,而泛型的类型就紧跟其后,用中括号包起来,表示是一个数组。
    比如,某类名叫 Order,它有2个泛型参数,那么 Order的写法是:
    Order2[System.Byte, System.Int32]<br />如果类型要写上程序集、版本号之类的信息,就再套一层中括号,把类型括起来。比如<br />Order2[[System.Byte,mscorelib,Version=4.0.0.0,PublicKeyToken=xxxxxx], [System.Int32,mscorelib,Version=4.0.0.0]]
    如果类型有4个泛型参数,就写成
    Order5[……]<br />总之类型名称后的之后的数字就是泛型参数的个数,中括号是类型列表。
    如果你不知道怎么写,告诉你一招,很简单,不用记,获取类型的Type,再看看它的FullName属性就知道了。至于那个“`”字符,就在键盘左上角,数字1左边那个。看图
    image.png
    最终生成的代码如下图所示。
    image.png
    怎么样,刺激吧。

    ———————————————————————————————————————————
    下面说一个让不少刚接触 CodeDom 的朋友相当头痛的东东——枚举类型怎么加成员。
    你就把枚举的成员当成是字段来处理就OK了,看一个例子。

    1. CodeTypeDeclaration entype = new CodeTypeDeclaration("CloseMode");
    2. // 指定它是枚举类型
    3. entype.IsEnum = true;
    4. // 添加成员
    5. CodeMemberField m1 = new CodeMemberField();
    6. m1.Name = "Restart";
    7. CodeMemberField m2 = new CodeMemberField();
    8. m2.Name = "Shutdown";
    9. CodeMemberField m3 = new CodeMemberField();
    10. m3.Name = "None";
    11. entype.Members.Add(m1);
    12. entype.Members.Add(m2);
    13. entype.Members.Add(m3);
    14. CodeDomProvider prd = CodeDomProvider.CreateProvider("cs");
    15. prd.GenerateCodeFromType(entype, Console.Out, null);

    这个枚举有三个成员,对于枚举,字段的类型可以不设置,只设置成员名字就行了,反正你想啊,枚举的成员默认是int,顶多也就是byte、short这些,都是整型的,所以,只要有成员名字就可以了。
    生成结果如下。
    image.png
    如果要想为某些成员赋特定数值,可以设置 InitExpression 属性。比如这样

    1. CodeMemberField m1 = new CodeMemberField();
    2. m1.Name = "Restart";
    3. m1.InitExpression = new CodePrimitiveExpression(15);

    这样,生成的枚举成员就有数值了。
    image.png