知道了如果构建代码文档,知道了如何生成代码,那么编译程序集就很简单了。
    CodeDomProvider 类提供了三个可以执行编译的方法:
    1、CompileAssemblyFromSource——这个好懂,也好办,就是用字符串直接构建代码,然后传给这个方法,就可以把源代码编译了。
    2、CompileAssemblyFromFile——这个是把一个代码文件传给方法进行编译,文件中包含源代码。
    3、CompileAssemblyFromDom——这个重载版本跟我们之前所学的内容关联性最大,因为它是把 CodeCompileUnit 实例传进去来编译的。
    以上几个重载,尽管代码的来源不同,但都有一个共同点:支持多个源。
    咱们知道,代码文档结构的根是命名空间,然后是类型,类型下是成员。一个程序集是可以包括多个命名空间的,假设编译的代码源自文件,而每个文件的代码都包含一个命名空间,那么要将多个命名空间合到一个程序集中,就可以把多个文件同时进行编译。当然,你也可以把所有的代码都放到一个文件中,然后只编译这个文件就行了。随你怎么弄,这样做只是为了灵活。

    大伙应该也发现了,这些方法都有一个参数,是 CompilerParameters 类型的,它的作用是设置编译选项。老周大概总结这么几点,以供大家参考,其他的大家不妨自己摸索,放心,不会很复杂的。
    1、如果你要生成可直接运行的程序集,即.exe,那就得把GenerateExecutable属性设置为true,默认它是为false的,即生成dll文件。所有可执行文件,不管你用啥语言写,都必须有入口点的,所以,如果要生成exe,就必须设置MainClass属性,它指的是包含Main方法的类,类名必须完整,要写上命名空间的名字,如my.Program。
    2、设置OutputAssembly属性,指定输出文件名,可以是绝对路径,也可以是相对路径。如dddd.exe、kkkk.dll等。当然你可以用其他名字,如comm.ft,但是,如果要生成exe,后缀必须是.exe,这样才能双击运行。如果这个属性没有指定,它会生成一个随机的文件名,并且输出临时文件目录下。注意:输出文件是设置OutputAssembly属性,不是CoreAssemblyFileName属性,千万不要弄错,CoreAssemblyFileName是设置核心类库的位置,即常见的 mscorlib.dll,主要是包含.net基本类型的程序集,一般我们不用设置它,由编译器自行选择合适的版本。
    3、如果GenerateInMemory设置为true,则可以不设置OutputAssembly,因为GenerateInMemory属性表示把程序集生成到内存中,而不是文件中。
    4、TempFiles设置编译时所产生的临时文件的路径,默认是临时文件夹,这个一般不用改。
    5、编译过程实际上是调用.net的命令行工具的,对于VB.NET语言,调用vbc命令,对于C#语言,调用csc命令。如果要指定一些编译器选项,可以设置CompilerOptions属性,它是一个字符串。关于编译选项,可以在开发工具的命令行工具中输入csc /?或vbc /?查看。
    下面,先举一个最简单的例子,就直接用代码源文件来编译。
    假设我在【文档】下建了一个demo.cs文件,在里面输入了以下代码:

    1. using System;
    2. namespace Sample
    3. {
    4. public class Demo
    5. {
    6. // 成员列表
    7. }
    8. }

    然后保存。
    接下来咱们要在程序中动态编译这个代码文件。

    1. // 文件路径
    2. string doclib = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
    3. string srccodePath = Path.Combine(doclib, "demo.cs");
    4. CodeDomProvider provider = CodeDomProvider.CreateProvider("cs");
    5. // 编译参数
    6. CompilerParameters p = new CompilerParameters();
    7. // 输出文件
    8. p.OutputAssembly = "DemoLib.dll";
    9. // 添加引用的程序集
    10. // 其实我们这里用不上,只是作为演示
    11. // mscorLib.dll是不用添加的,它是默认库
    12. p.ReferencedAssemblies.Add("System.dll");
    13. // 编译
    14. CompilerResults res = provider.CompileAssemblyFromFile(p, srccodePath);
    15. // 检查编译结果
    16. if (res.Errors.Count == 0)
    17. {
    18. // 没有出错
    19. Console.WriteLine("编译成功。");
    20. // 获取刚刚编译的程序集信息
    21. Assembly outputAss = res.CompiledAssembly;
    22. // 全名
    23. Console.WriteLine($"程序集全名:{outputAss.FullName}");
    24. // 位置
    25. Console.WriteLine($"程序集位置:{outputAss.Location}");
    26. // 程序集中的类型
    27. Type[] types = outputAss.GetTypes();
    28. Console.WriteLine("----------------------\n类型列表:");
    29. foreach (Type t in types)
    30. {
    31. Console.WriteLine(t.FullName);
    32. }
    33. }
    34. else
    35. {
    36. // 如果编译出错
    37. Console.WriteLine("发生错误,详见以下内容:");
    38. foreach (CompilerError er in res.Errors)
    39. {
    40. Console.WriteLine($"行{er.Line},列{er.Column},错误号{er.ErrorNumber},错误信息:{er.ErrorText}");
    41. }
    42. }

    代码虽长,但不难懂。注意编译完成后,会返回一个表示编译结果的CompilerResults实例,如果其中的Errors集合中没有元素,说明编译成功,如果里面有东西,表明其间发生了错误。每个错误都用CompilerError类封装。
    如果成功编译,通过结果的CompiledAssembly属性就可以获取到刚刚编译的程序集信息。
    示例输出结果如下图。
    image.png
    下面提供一个用 CodeDom 来编译的例子。

    1. CodeCompileUnit unit = new CodeCompileUnit();
    2. // 命名空间
    3. CodeNamespace ns = new CodeNamespace("MyApp");
    4. unit.Namespaces.Add(ns);
    5. ns.Imports.Add(new CodeNamespaceImport(nameof(System)));
    6. ns.Imports.Add(new CodeNamespaceImport($"{nameof(System)}.{nameof(System.Windows)}.{nameof(System.Windows.Forms)}"));
    7. // 类型
    8. CodeTypeDeclaration typedec = new CodeTypeDeclaration("Program");
    9. ns.Types.Add(typedec);
    10. typedec.Attributes = MemberAttributes.Public | MemberAttributes.Final;
    11. // 入口点
    12. CodeEntryPointMethod main = new CodeEntryPointMethod();
    13. typedec.Members.Add(main);
    14. // 创建窗口实例
    15. CodeVariableDeclarationStatement newwindow = new CodeVariableDeclarationStatement();
    16. main.Statements.Add(newwindow);
    17. newwindow.Name = "mainWindow";
    18. newwindow.Type = new CodeTypeReference(nameof(System.Windows.Forms.Form));
    19. newwindow.InitExpression = new CodeObjectCreateExpression(nameof(System.Windows.Forms.Form));
    20. // 设置窗口标题栏
    21. CodeAssignStatement settitle = new CodeAssignStatement();
    22. main.Statements.Add(settitle);
    23. settitle.Left = new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(newwindow.Name), nameof(System.Windows.Forms.Form.Text));
    24. settitle.Right = new CodePrimitiveExpression("我的应用程序");
    25. // 调用 Application.Run 方法
    26. CodeMethodInvokeExpression invokeexp = new CodeMethodInvokeExpression(new CodeTypeReferenceExpression(nameof(System.Windows.Forms.Application)), nameof(System.Windows.Forms.Application.Run), new CodeVariableReferenceExpression(newwindow.Name));
    27. CodeExpressionStatement invrunstatem = new CodeExpressionStatement(invokeexp);
    28. main.Statements.Add(invrunstatem);
    29. // 生成代码
    30. CodeDomProvider provider = CodeDomProvider.CreateProvider("cs");
    31. provider.GenerateCodeFromCompileUnit(unit, Console.Out, null);
    32. // 编译
    33. CompilerParameters p = new CompilerParameters();
    34. p.GenerateExecutable = true; //生成exe
    35. p.CompilerOptions = "/t:winexe"; //非控制台应用程序
    36. p.OutputAssembly = "testapp.exe";
    37. // 包含入口点的类
    38. p.MainClass = $"{ns.Name}.{typedec.Name}";
    39. // 引用的程序集
    40. p.ReferencedAssemblies.Add("System.dll");
    41. p.ReferencedAssemblies.Add("System.Windows.Forms.dll");
    42. CompilerResults res = provider.CompileAssemblyFromDom(p, unit);
    43. if (res.Errors.Count == 0)
    44. {
    45. Console.WriteLine("编译成功。");
    46. // 启动它
    47. System.Diagnostics.Process.Start(res.CompiledAssembly.Location);
    48. }
    49. else
    50. {
    51. Console.WriteLine("错误信息:");
    52. foreach (CompilerError er in res.Errors)
    53. {
    54. Console.WriteLine(er.ErrorText);
    55. }
    56. }

    代码虽然很是TMD的长,但你别紧张,其实就做了三件事。
    1、构建代码逻辑。这个示例生成一个Windows Form程序,所以,需要定义一个类,在类中必须有Main方法,在Main方法中实例化窗口类,然后用Application.Run方法显示窗口。
    2、生成代码,这个大家已经熟悉,前面N篇文章中就用到多次。
    3、编译。
    我们重点放在编译上,请大家注意,尽管GenerateExecutable属性已经被设置为true,不过,你懂的,exe程序有两类,一类是控制台,一类是常见的win窗口,所以,这里还得借助编译器命令行选项,加一个/t:winexe,表示生成的是Windows标准窗口程序。而正因为生成的是exe文件,所以,不要忘了MainClass属性,指定我们刚刚用CodeDom构建的那个类,它包含了入口点方法。
    运行之后,会在当前程序的同一目录下,生成一个.exe文件,并且执行后,显示一个空白的窗口。如下图所示。
    image.png
    好了,说到了编译部分,CodeDom的这一系列文章也写得差不多了,不过后面还会加一篇补充的,把一些零碎的内容过一下。
    之所以前面的文章中,一些评论老周没有回复,是因为老周又发现,又有人把CodeDom和Emit搞混了,动态发出程序集是基于指令的,而CodeDom是基于代码文档,CodeDom既可用于生成代码源文件,也可用于动态编译。这个老周前面是强调过的,希望大家注意。