运行时编译 C# 代码






4.96/5 (53投票s)
本文档展示了如何在运行时编译和使用 C# 代码,以及它对速度的影响。
引言
有时,在运行时编译代码非常有用。就个人而言,我主要在以下两种情况下使用此功能:
- 简单的 Web 教程 – 将一小段代码写入
TextBox
控件并执行它,而无需拥有或运行任何 IDE。 - 用户自定义函数 – 我编写了一个用于符号回归的应用程序,它具有简单的配置文件,用户可以选择我预定义的一些函数(sin、cos 等)。用户还可以使用基本的 C# 语言知识编写自己的数学表达式。
如果您想使用此功能,无需安装任何第三方库。所有功能均由 .NET Framework 在 Microsoft.CSharp
和 System.CodeDom.Compiler
命名空间中提供。
Hello World 程序
要进行运行时代码编译,您需要遵循以下几个步骤:
- 编写代码 – 最重要的一步。您可以在
string
变量中编写类和方法。我们将只添加Main
方法。string code = @" using System; namespace First { public class Program { public static void Main() { " + "Console.WriteLine(\"Hello, world!\");" + @" } } } ";
- 创建编译器提供程序和参数
CSharpCodeProvider provider = new CSharpCodeProvider(); CompilerParameters parameters = new CompilerParameters();
- 定义编译器参数(可选) – 在这一点上,我们可以添加对外部库的引用。我们还可以定义编译后的代码是仅生成在内存中,还是生成到 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;
- 编译程序集
CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
- 检查错误
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()); }
- 获取程序集、类型和
Main
方法Assembly assembly = results.CompiledAssembly; Type program = assembly.GetType("First.Program"); MethodInfo main = program.GetMethod("Main");
- 运行它:
main.Invoke(null, null);
就这样。我们已经运行了我们第一个运行时编译的程序!
用户自定义函数
正如我在简介中提到的,我使用此功能来运行时定义数学表达式。让我们看看如何创建具有两个参数的简单函数。
这与前面的示例非常相似(假设函数的参数命名为 x
和 y
)。在此代码中,我们只需将 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 种类型的函数:
- 原始的、在编译时定义的函数
- 通过反射调用的运行时编译函数
- 从运行时编译函数创建的委托
- 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);
经过多次测试,我们将得到以下结果:
- 原始 – 时间:92 毫秒
- 反射 – 时间:3686 毫秒
- 委托 – 时间:64 毫秒
- Lambda – 时间:90 毫秒
结论
在运行时编译 C# 代码对于各种应用程序来说非常有用且容易的任务。我展示了如何创建您自己的简单编译器。从速度结果来看,很明显,运行时编译代码的速度与经典代码相当(除了通过反射调用的情况)。