.Net 动态方法表达式求值器






4.97/5 (21投票s)
2007 年 3 月 13 日
6分钟阅读

123271

2095
通过将 C# 代码编译为 IL,然后从 IL 创建 DynamicMethod 来评估动态表达式。
引言
关于使用 .NET 进行动态表达式求值,已经有很多文章了。我想在这里展示的是一个新颖的思路,即使用 System.Reflection.Emit
命名空间中的 DynamicMethod
类。在我的研究中,我发现了两种主要的方法来处理运行时表达式求值:一种是使用 System.CodeDom
命名空间编译 .NET 代码,另一种是编写解析器。每种方法都有其优缺点,但我知道我不想编写解析器,因为 .NET 已经可以解析 C#,而且做得比我好得多。
编译代码的好处是速度快。然而,它也有几个问题。相比于解析,编译似乎需要相当长的时间。所以如果表达式不断变化,那么它的性能就不会很好。但如果表达式是只编译一次并多次调用的,那么它的性能是无与伦比的。所以我要解决的问题是:给定一组表达式,我需要为给定的值集或数据行快速执行它们。然后能够通过对数据集中的每一行重复执行来处理数据集。
背景
找到的一些有用文章/网站
解析方法体的 IL
将 MethodInfo 转换为 DynamicMethod
再次审视表达式求值器(100% 托管 .net 中的 Eval 函数)
通过在运行时编译 C# 代码来评估数学表达式
数学表达式求值器
最好看一下其中的一些文章,以便了解本文有何不同或其基于哪些概念。
所以 CodeDom 的一个问题是,编译会导致生成的程序集被加载到当前的 AppDomain 中。由于在 .NET 中无法卸载程序集,所以你会得到大量的程序集,因为每个新表达式都会创建一个新的程序集。为了防止生成的程序集立即加载,可以创建一个新的 AppDomain 来生成代码。但是当我们想要执行表达式时,程序集仍然必须被加载。那么我们该如何解决这个问题呢? System.Reflection.Emit.DynamicMethod
类来拯救你。
此时,我确信您已经查阅了 MSDN 以了解更多关于它的信息。DynamicMethod
允许在运行时创建一个方法,并具有编译代码的速度。这很棒,但只有一个问题:你需要为它编写 IL 代码。
我知道我不想编写解析器,更不想花很多时间编写 IL 代码。一个绝妙的主意来自于上面链接的 将 MethodInfo 转换为 DynamicMethod 文章。基本上,反射可以用来获取已编译方法的 IL 代码,并从中生成一个 DynamicMethod
。太完美了!现在我们可以编写 .NET 代码,编译它,获取 IL 代码,然后创建我们需要的 DynamicMethod
。
使用代码
要将 MethodInfo
转换为 DynamicMethod
,我需要知道正在使用哪个方法。我知道我想要访问同一个类中的函数,所以不需要加上类名前缀。我想向用户提供一组文档化的函数,而无需他们了解太多 .NET 知识。所以我创建了一个带有某些函数的对象,并称之为 FunctionClass
。
public class FunctionClass
{
//some instance data, could be another object
//maybe a datarow or custom data object
public double X;
public double Y;
public double Z;
//method that can be called
public double Round(double number)
{
return Math.Round(number);
}
}
CodeDom 编译器
所以,为了编译一个可以访问此类方法的 C# 表达式,我将把编译后的方法放在一个继承自 FunctionClass
的类上。这样,它就可以访问基类的所有受保护和公共方法,并在没有类型前缀的情况下调用它们。我的假设是,一个表达式是一行代码,它看起来像 return [user code line];
。为了保持类型安全,我还将要求用户为表达式选择一个返回类型;例如 double
、string
、DateTime
等。所以下面的代码是编译器类的代码。
/// <summary>
/// Expression compiler using the C# language
/// </summary>
public class CSharpExpressionCompiler : BaseExpressionCompiler,
IExpressionCompiler
{
/// <summary>
/// Compiles the expression into an assembly and returns the method code
/// for it.
/// It should compile the method into a class that inherits from the
/// functionType.
/// </summary>
/// <param name="expression">expression to be
/// compiled</param>
/// <param name="functionType">Type of the function class
/// to use</param>
/// <param name="returnType">Return type of the method to
/// create</param>
/// <returns>DynamicMethodState - A serialized version of the
/// method code</returns>
public DynamicMethodState CompileExpression(string expression,
Type functionType, Type returnType)
{
DynamicMethodState methodState;
//use CodeDom to compile using C#
CodeDomProvider codeProvider =
CodeDomProvider.CreateProvider("CSharp");
CompilerParameters loParameters = new CompilerParameters();
//add assemblies
loParameters.ReferencedAssemblies.Add("System.dll");
loParameters.ReferencedAssemblies.Add(functionType.Assembly.Location);
//don't generate assembly on disk and treat warnings as errors
loParameters.GenerateInMemory = true;
loParameters.TreatWarningsAsErrors = true;
//set namespace of dynamic class
string dynamicNamespace = "ExpressionEval.Functions.Dynamic";
//set source for inherited class - need to change to use CodeDom
//objects instead
string source = @"
using System;
using {5};
namespace {6}
{{
public class {0} : {1}
{{
public {2} {3}()
{{
return {4};
}}
}}
}}
";
//set source code replacements
string className = "Class_" + Guid.NewGuid().ToString("N");
string methodName = "Method_" + Guid.NewGuid().ToString("N");
string returnTypeName = returnType.FullName;
//check for generic type for return
....
//format codestring with replacements
string codeString = string.Format(source, className,
functionType.FullName, returnTypeName,
methodName, expression, functionType.Namespace, dynamicNamespace);
//compile the code
CompilerResults results =
codeProvider.CompileAssemblyFromSource(loParameters, codeString);
if (results.Errors.Count > 0)
{
//throw an exception for any errors
throw new CompileException(results.Errors);
}
else
{
//get the type that was compiled
Type dynamicType = results.CompiledAssembly.GetType(
dynamicNamespace + "." + className);
//get the MethodInfo for the compiled expression
MethodInfo dynamicMethod = dynamicType.GetMethod(methodName);
//get the compiled expression as serializable object
methodState = GetMethodState(dynamicMethod);
}
return methodState;
}
}
BaseExpressionCompiler
提供了 GetMethodState
方法,该方法将 MethodInfo
转换为 DynamicMethodState
。DynamicMethodState
是一个类,它提供了方法 IL 和元数据令牌偏移量的可序列化形式。一旦我有了这个,我就可以丢弃创建的程序集。
/// <summary>
/// A base expression compiler. MarshalByRef so it can be used across
/// AppDomains.
/// </summary>
public abstract class BaseExpressionCompiler : MarshalByRefObject
{
/// <summary>
/// Converts a MethodInfo into a serialized version of it.
/// </summary>
/// <param name="dynamicMethod">The method for which to
/// create a DynamicMethod for</param>
/// <returns>DynamicMethodState - serialized version of a
/// method.</returns>
protected DynamicMethodState GetMethodState(MethodInfo dynamicMethod)
{
DynamicMethodState methodState = new DynamicMethodState();
//IL info from method
MethodBody methodIlCode = dynamicMethod.GetMethodBody();
//get code bytes and other method properties
methodState.CodeBytes = methodIlCode.GetILAsByteArray();
methodState.InitLocals = methodIlCode.InitLocals;
methodState.MaxStackSize = methodIlCode.MaxStackSize;
//get any local variable information
IDictionary<int, LocalVariable> locals = new SortedList<int,
LocalVariable>();
foreach (LocalVariableInfo localInfo in methodIlCode.LocalVariables)
{
locals.Add(localInfo.LocalIndex, new
LocalVariable(localInfo.IsPinned,
localInfo.LocalType.TypeHandle));
}
methodState.LocalVariables = locals;
TokenOffset tokenOffset = new TokenOffset();
//get metadata token offsets
IlReader reader = new IlReader(methodState.CodeBytes,
dynamicMethod.Module);
tokenOffset.Fields = reader.Fields;
tokenOffset.Methods = reader.Methods;
tokenOffset.Types = reader.Types;
tokenOffset.LiteralStrings = reader.LiteralStrings;
methodState.TokenOffset = tokenOffset;
return methodState;
}
}
编译器类继承自 MarshalByRefObject
,因为它需要跨 AppDomain 使用,如前所述。现在有了获取 DynamicMethodState
的编译器,我们就需要从中创建 DynamicMethod
对象。
DynamicMethod 委托
ExpressionDelegateFactory
调用编译器跨越它创建的一个 AppDomain 来编译表达式。然后,它以 R ExecuteExpression<R,C>(C functionClass)
的形式从编译器返回的 DynamicMethodState
创建一个委托,其中 R 是返回类型,C 是函数类类型。
/// <summary>
/// Implements a Delegate Factory for compiled expressions
/// </summary>
public class ExpressionDelegateFactory : IExpressionDelegateFactory
{
private ExpressionLanguage m_language;
/// <summary>
/// Delegate Factory for a Language
/// </summary>
/// <param name="language"></param>
public ExpressionDelegateFactory(ExpressionLanguage language)
{
m_language = language;
}
/// <summary>
/// Compiles an expression and returns a delegate to the compiled code.
/// </summary>
/// <typeparam name="R">The return type of the
/// expression</typeparam>
/// <typeparam name="C">The type of the function
/// class</typeparam>
/// <param name="expression">Expression to
/// evaluate</param>
/// <returns>ExecuteExpression<R, C> - a delegate that calls
/// the compiled expression</returns>
public ExecuteExpression<R, C> CreateExpressionDelegate<R,
C>(string expression)
{
ExecuteExpression<R, C> expressionDelegate = null;
DynamicMethodState methodState;
//create the compiled expression
methodState = CreateExpressionMethodState<R, C>(expression);
if (methodState != null && methodState.CodeBytes != null)
{
//get a dynamic method delegate from the method state
expressionDelegate = CreateExpressionDelegate<R,
C>(methodState);
}
return expressionDelegate;
}
/// <summary>
/// Compiles an expression and returns a DynamicMethodState
/// </summary>
/// <typeparam name="R">The return type of the
/// expression</typeparam>
/// <typeparam name="C">The type of the function
/// class</typeparam>
/// <param name="expression">Expression to
/// evaluate</param>
/// <returns>DynamicMethodState - serialized version of the
/// compiled expression</returns>
public DynamicMethodState CreateExpressionMethodState<R,
C>(string expression)
{
DynamicMethodState methodState;
IExpressionCompiler compiler;
//create an AppDomain
AppDomainSetup loSetup = new AppDomainSetup();
loSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
AppDomain loAppDomain =
AppDomain.CreateDomain("CompilerDomain", null, loSetup);
//get the compiler to use based on the language
string className = null;
switch(m_language)
{
case ExpressionLanguage.CSharp:
className = "CSharpExpressionCompiler";
break;
case ExpressionLanguage.VisualBasic:
className = "VisualBasicExpressionCompiler";
break;
case ExpressionLanguage.JScript:
className = "JScriptExpressionCompiler";
break;
}
//create an instance of a compiler
compiler=(IExpressionCompiler)loAppDomain.CreateInstanceFromAndUnwrap(
"ExpressionEval.ExpressionCompiler.dll",
"ExpressionEval.ExpressionCompiler." + className);
try
{
//compile the expression
methodState = compiler.CompileExpression(expression, typeof(C),
typeof(R));
}
catch (CompileException e)
{
//catch any compile errors and throw an overall exception
StringBuilder exceptionMessage = new StringBuilder();
foreach (CompilerError error in e.CompileErrors)
{
exceptionMessage.Append("Error# ").Append(error.ErrorNumber);
exceptionMessage.Append(", column ").Append(error.Column);
exceptionMessage.Append(", ").Append(error.ErrorText);
exceptionMessage.Append(Environment.NewLine);
}
throw new ApplicationException(exceptionMessage.ToString());
}
finally
{
//unload the AppDomain
AppDomain.Unload(loAppDomain);
}
//if for some reason the code byte were not sent then return null
if (methodState != null && methodState.CodeBytes == null)
{
methodState = null;
}
return methodState;
}
/// <summary>
/// Compiles a DynamicMethodState and returns a delegate.
/// </summary>
/// <typeparam name="R">The return type of the
/// expression</typeparam>
/// <typeparam name="C">The type of the function
/// class</typeparam>
/// <param name="methodState">The serialized version of a
/// method on the functionClass</param>
/// <returns>ExecuteExpression<R, C> - a delegate that calls
/// the compiled expression</returns>
public ExecuteExpression<R, C> CreateExpressionDelegate<R,
C>(DynamicMethodState methodState)
{
ExecuteExpression<R, C> expressionDelegate;
//create a dynamic method
DynamicMethod dynamicMethod = new DynamicMethod(
"_" + Guid.NewGuid().ToString("N"), typeof(R),
new Type[] { typeof(C) }, typeof(C));
//get the IL writer for it
DynamicILInfo dynamicInfo = dynamicMethod.GetDynamicILInfo();
//set the properties gathered from the compiled expression
dynamicMethod.InitLocals = methodState.InitLocals;
//set local variables
SignatureHelper locals = SignatureHelper.GetLocalVarSigHelper();
foreach (int localIndex in methodState.LocalVariables.Keys)
{
LocalVariable localVar = methodState.LocalVariables[localIndex];
locals.AddArgument(Type.GetTypeFromHandle(localVar.LocalType),
localVar.IsPinned);
}
dynamicInfo.SetLocalSignature(locals.GetSignature());
//resolve any metadata tokens
IlTokenResolver tokenResolver = new IlTokenResolver(
methodState.TokenOffset.Fields,
methodState.TokenOffset.Methods,
methodState.TokenOffset.Types,
methodState.TokenOffset.LiteralStrings);
methodState.CodeBytes = tokenResolver.ResolveCodeTokens(
methodState.CodeBytes, dynamicInfo);
//set the IL code for the dynamic method
dynamicInfo.SetCode(methodState.CodeBytes, methodState.MaxStackSize);
//create a delegate for fast execution
expressionDelegate = (ExecuteExpression<R,
C>)dynamicMethod.CreateDelegate(
typeof(ExecuteExpression<R, C>));
return expressionDelegate;
}
}
DynamicMethod
的“魔力”在于它表现得像一个静态方法,但也可以像实例方法一样使用。在 IL 代码中,实例方法中的第一个参数始终是该方法所属类型的实例。所以要让这个静态方法表现得像一个实例,你需要确保该方法的第一个参数是该方法所属类型的实例。在 MSDN 上阅读有关这方面的内容,因为这是使这段代码生效的重要概念。
我还想隐藏创建 DynamicMethod
的实现,使其不被使用表达式的代码直接访问,所以我创建了一个 ExpressionEvaluator
来封装对 ExpressionDelegateFactory
的调用。代码分为 5 个不同的项目:MsilConversion(IL 读取和令牌解析)、MethodState(跨应用程序域共享的 dll,DynamicMethodState)、ExpressionCompiler(特定语言的编译器)、DynamicMethodGenerator(DynamicMethod 委托工厂)、ExpressionEvaluation(委托工厂的包装器)。
我创建了一个名为 TestApp 的 Windows 窗体测试应用程序来调用 ExpressionEvaluation 引擎。它有一个简单的“一次性求值”函数和一个“循环”函数,循环函数展示了“一次编译,多次运行”操作可以实现的性能。
可能的原因:
从这里开始,在实现增强或修改方面似乎有相当多的可能性。不同的 .NET 语言,将自定义语言翻译成 .NET 语言的翻译器,支持多行代码而不是单行表达式。甚至可以更雄心勃勃一些,创建一个解析器/IL 生成器来跳过 CodeDom 步骤,直接编写 IL。
反馈
我很想听听您对代码、概念或文章本身的任何反馈。此外,我也很好奇您对增强功能的想法,以及如果您实现了这个概念,结果如何。
历史
- 2007 年 3 月 13 日 - 首次发布