65.9K
CodeProject 正在变化。 阅读更多。
Home

运行时编译 C# 代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (53投票s)

2014 年 1 月 25 日

CPOL

3分钟阅读

viewsIcon

249043

downloadIcon

6145

本文档展示了如何在运行时编译和使用 C# 代码,以及它对速度的影响。

引言

有时,在运行时编译代码非常有用。就个人而言,我主要在以下两种情况下使用此功能:

  • 简单的 Web 教程 – 将一小段代码写入 TextBox 控件并执行它,而无需拥有或运行任何 IDE。
  • 用户自定义函数 – 我编写了一个用于符号回归的应用程序,它具有简单的配置文件,用户可以选择我预定义的一些函数(sin、cos 等)。用户还可以使用基本的 C# 语言知识编写自己的数学表达式。

如果您想使用此功能,无需安装任何第三方库。所有功能均由 .NET Framework 在 Microsoft.CSharpSystem.CodeDom.Compiler 命名空间中提供。

Hello World 程序

要进行运行时代码编译,您需要遵循以下几个步骤:

  1. 编写代码 – 最重要的一步。您可以在 string 变量中编写类和方法。我们将只添加 Main 方法。
    string code = @"
        using System;
    
        namespace First
        {
            public class Program
            {
                public static void Main()
                {
                " +
                    "Console.WriteLine(\"Hello, world!\");"
                    + @"
                }
            }
        }
    ";
  2. 创建编译器提供程序和参数
    CSharpCodeProvider provider = new CSharpCodeProvider();
    CompilerParameters parameters = new CompilerParameters();
    
  3. 定义编译器参数(可选) – 在这一点上,我们可以添加对外部库的引用。我们还可以定义编译后的代码是仅生成在内存中,还是生成到 DLL 或 EXE 文件中。
    // Reference to System.Drawing library
    parameters.ReferencedAssemblies.Add("System.Drawing.dll");
    // True - memory generation, false - external file generation
    parameters.GenerateInMemory = true;
    // True - exe file generation, false - dll file generation
    parameters.GenerateExecutable = true;
  4. 编译程序集
    CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
  5. 检查错误
    if (results.Errors.HasErrors)
    {
        StringBuilder sb = new StringBuilder();
    
        foreach (CompilerError error in results.Errors)
        {
            sb.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText));
        }
    
        throw new InvalidOperationException(sb.ToString());
    }
  6. 获取程序集、类型和 Main 方法
    Assembly assembly = results.CompiledAssembly;
    Type program = assembly.GetType("First.Program");
    MethodInfo main = program.GetMethod("Main");
  7. 运行它:
    main.Invoke(null, null);

就这样。我们已经运行了我们第一个运行时编译的程序!

用户自定义函数

正如我在简介中提到的,我使用此功能来运行时定义数学表达式。让我们看看如何创建具有两个参数的简单函数。

这与前面的示例非常相似(假设函数的参数命名为 xy)。在此代码中,我们只需将 string 的一部分替换为我们想要的功能并对其进行编译即可。

public static MethodInfo CreateFunction(string function)
{
    string code = @"
        using System;
            
        namespace UserFunctions
        {                
            public class BinaryFunction
            {                
                public static double Function(double x, double y)
                {
                    return func_xy;
                }
            }
        }
    ";

    string finalCode = code.Replace("func_xy", function);

    CSharpCodeProvider provider = new CSharpCodeProvider();
    CompilerResults results = provider.CompileAssemblyFromSource(new CompilerParameters(), finalCode);

    Type binaryFunction = results.CompiledAssembly.GetType("UserFunctions.BinaryFunction");
    return binaryFunction.GetMethod("Function");
}

此时,我们已经编写了用于编译用户自定义函数的代码,因此我们通过创建一些函数并调用它来使用它。

MethodInfo function = CreateFunction("x + 2 * y");
object result = function.Invoke(null, new object[] { 2, 3 });
Console.WriteLine(result);

在这种情况下,结果等于 8,因此我们的编译器工作正常,但我们得到了 Object 类型的结果,并且我们还必须提供 Object 类型的参数。有一个更好的方法 – 创建委托。

var betterFunction = (Func<double, double, double>)Delegate.CreateDelegate
(typeof(Func<double, double, double>), function);

并且有一个非常简单的方法来调用它

double result = betterFunction(2, 3);
Console.WriteLine(result);

速度比较

我们已经编写了一个简单的函数编译器,所以现在我们有 4 种类型的函数:

  1. 原始的、在编译时定义的函数
  2. 通过反射调用的运行时编译函数
  3. 从运行时编译函数创建的委托
  4. Lambda 表达式委托 ((x, y) => x + 2 * y)

让我们编写一个简单的程序来比较它们的速度

DateTime start;
DateTime stop;
double result;
int repetitions = 5000000;

start = DateTime.Now;
for (int i = 0; i < repetitions; i++)
{
    result = OriginalFunction(2, 3);
}
stop = DateTime.Now;
Console.WriteLine("Original - time: {0} ms", (stop - start).TotalMilliseconds);

start = DateTime.Now;
for (int i = 0; i < repetitions; i++)
{
    result = (double)function.Invoke(null, new object[] { 2, 3 });
}
stop = DateTime.Now;
Console.WriteLine("Reflection - time: {0} ms", (stop - start).TotalMilliseconds);

start = DateTime.Now;
for (int i = 0; i < repetitions; i++)
{
    result = betterFunction(2, 3);
}
stop = DateTime.Now;
Console.WriteLine("Delegate - time: {0} ms", (stop - start).TotalMilliseconds); 

start = DateTime.Now;
for (int i = 0; i < repetitions; i++)
{
    result = lambda(2, 3);
}
stop = DateTime.Now;
Console.WriteLine("Lambda - time: {0} ms", (stop - start).TotalMilliseconds);

经过多次测试,我们将得到以下结果:

  1. 原始 – 时间:92 毫秒
  2. 反射 – 时间:3686 毫秒
  3. 委托 – 时间:64 毫秒
  4. Lambda – 时间:90 毫秒

结论

在运行时编译 C# 代码对于各种应用程序来说非常有用且容易的任务。我展示了如何创建您自己的简单编译器。从速度结果来看,很明显,运行时编译代码的速度与经典代码相当(除了通过反射调用的情况)。 

© . All rights reserved.