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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (21投票s)

2007 年 3 月 13 日

6分钟阅读

viewsIcon

123271

downloadIcon

2095

通过将 C# 代码编译为 IL,然后从 IL 创建 DynamicMethod 来评估动态表达式。

.Net Expression Evaluator using 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];。为了保持类型安全,我还将要求用户为表达式选择一个返回类型;例如 doublestringDateTime 等。所以下面的代码是编译器类的代码。

/// <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 转换为 DynamicMethodStateDynamicMethodState 是一个类,它提供了方法 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 日 - 首次发布
© . All rights reserved.