如何动态评估C#表达式?

64

我想要做相当于以下的事情:

object result = Eval("1 + 3");
string now    = Eval("System.DateTime.Now().ToString()") as string

在参考Biri的链接后,我得到了以下代码片段(已修改以删除过时的方法ICodeCompiler.CreateCompiler()):

private object Eval(string sExpression)
{
    CSharpCodeProvider c = new CSharpCodeProvider();
    CompilerParameters cp = new CompilerParameters();

    cp.ReferencedAssemblies.Add("system.dll");

    cp.CompilerOptions = "/t:library";
    cp.GenerateInMemory = true;

    StringBuilder sb = new StringBuilder("");
    sb.Append("using System;\n");

    sb.Append("namespace CSCodeEvaler{ \n");
    sb.Append("public class CSCodeEvaler{ \n");
    sb.Append("public object EvalCode(){\n");
    sb.Append("return " + sExpression + "; \n");
    sb.Append("} \n");
    sb.Append("} \n");
    sb.Append("}\n");

    CompilerResults cr = c.CompileAssemblyFromSource(cp, sb.ToString());
    if (cr.Errors.Count > 0)
    {
        throw new InvalidExpressionException(
            string.Format("Error ({0}) evaluating: {1}", 
            cr.Errors[0].ErrorText, sExpression));
    }

    System.Reflection.Assembly a = cr.CompiledAssembly;
    object o = a.CreateInstance("CSCodeEvaler.CSCodeEvaler");

    Type t = o.GetType();
    MethodInfo mi = t.GetMethod("EvalCode");

    object s = mi.Invoke(o, null);
    return s;

}  

3
请记住,除非您在一个独立的AppDomain中进行编译和执行,否则可能会遇到内存问题。以这种方式生成的程序集无法卸载,但如果您在单独的AppDomain中创建程序集,就可以卸载该AppDomain,并因此卸载所生成的程序集。 - JJJ
为什么需要这个?我非常怀疑这是您打算使用的任何目的的最佳设计。很可能有其他机制可以产生您想要的结果,而不必在运行时评估代码片段这样做可能会带来巨大的安全和可维护性问题。 - Wedge
我想要一个快速、简单的方法来为我编写的DSL添加表达式验证。我控制被提供给计算器的文件,所以这个麻烦的问题永远不会出现;此外,我只允许一个表达式,不添加任何命名空间/引用任何程序集。这样就可以防止我自己造成伤害! - Daren Thomas
1
我在测试场景中这样做。我有一个程序集,随着更新而更改版本号。我希望我的测试代码对程序集进行“延迟绑定”,能够动态加载程序集并调用它。这只是一个简单的反射问题。但是,当我想要实现程序集中指定的接口时,需要动态编译代码,因为接口本身使用强名称(带有版本号)。我不能有实现IFoo v1.2的代码,而要调用v1.3。动态编译可以解决这个问题。 - Cheeso
1
我应该说,还有很多其他情况。这不是主流的做法,但在许多情况下,动态代码编译是有意义的。 - Cheeso
10个回答

49

虽然这是一个老话题,但考虑到这是谷歌搜索结果中最早的帖子之一,这里提供一个更新的解决方案。

您可以使用Roslyn的新脚本API来评估表达式

如果您正在使用NuGet,则只需添加对Microsoft.CodeAnalysis.CSharp.Scripting的依赖项即可。 要评估您提供的示例,只需简单地执行以下操作:

var result = CSharpScript.EvaluateAsync("1 + 3").Result;

这显然没有利用脚本引擎的异步能力。

您还可以按照您的意图指定计算结果类型:

var now = CSharpScript.EvaluateAsync<string>("System.DateTime.Now.ToString()").Result;

如果要评估更高级的代码片段,传递参数,提供引用、命名空间等等,请查看上面链接的维基。


1
我喜欢这个功能,它运行得非常好,唯一的抱怨是它的速度太慢了。 - JMIII
2
非常慢。添加30秒来评估简单的三元40表达式。 - hungryMind
2
@hungryMind,你不要只是表面上使用EvaluateAsync。在使用这种方法时需要考虑三个方面:
  1. Roslyn预热。
  2. 表达式编译。
  3. 表达式评估。
前两个方面占用了你看到的40秒中的99%。如果你缓存“CSharpScrip.Create”的结果并使用它,你将获得所请求字符串的本地性能。
- Tomasz Juszczak

33

我已经写了一个开源项目,Dynamic Expresso,可以将使用C#语法编写的文本表达式转换为委托(或表达式树)。 文本表达式会被解析和转换为表达式树,而不需要使用编译或反射。

你可以这样写:

var interpreter = new Interpreter();
var result = interpreter.Eval("8 / 2 + 2");
或者
var interpreter = new Interpreter()
                .SetVariable("service", new ServiceExample());

string expression = "x > 4 ? service.aMethod() : service.AnotherMethod()";

Lambda parsedExpression = interpreter.Parse(expression, 
                        new Parameter("x", typeof(int)));

parsedExpression.Invoke(5);

我的工作基于Scott Gu的文章http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx


+1 @Davide lcardi 这是一个非常好的库,但是它有一些限制: https://stackoverflow.com/questions/46581067/how-to-make-dynamicexpresso-recognizes-members-using-new-modifier - Dabbas

24
如果您想在自己的项目中特定调用代码和程序集,我建议使用C# CodeDom CodeProvider
以下是我了解的动态评估C#字符串表达式最流行的方法列表。

Microsoft解决方案

非微软解决方案(并不是说微软的不好)


看起来你忘了一些选项(有些在其他答案中提到),比如DataColumn.ExpressionDataTable.Compute,以及已经废弃的JScript。 - NetMage

2

我刚刚用纯C#编写了一个类似的库(Matheval),它可以像Excel公式一样计算字符串和数字表达式。

using System;
using org.matheval;
                    
public class Program
{
    public static void Main()
    {
        Expression expression = new Expression("IF(time>8, (HOUR_SALARY*8) + (HOUR_SALARY*1.25*(time-8)), HOUR_SALARY*time)");
        //bind variable
        expression.Bind("HOUR_SALARY", 10);
        expression.Bind("time", 9);
        //eval
        Decimal salary = expression.Eval<Decimal>();    
        Console.WriteLine(salary);
    }
}

2
using System;
using Microsoft.JScript;
using Microsoft.JScript.Vsa;
using Convert = Microsoft.JScript.Convert;

namespace System
{
    public class MathEvaluator : INeedEngine
    {
        private VsaEngine vsaEngine;

        public virtual String Evaluate(string expr)
        {
            var engine = (INeedEngine)this;
            var result = Eval.JScriptEvaluate(expr, engine.GetEngine());

            return Convert.ToString(result, true);
        }

        VsaEngine INeedEngine.GetEngine()
        {
            vsaEngine = vsaEngine ?? VsaEngine.CreateEngineWithType(this.GetType().TypeHandle);
            return vsaEngine;
        }

        void INeedEngine.SetEngine(VsaEngine engine)
        {
            vsaEngine = engine;
        }
    }
}

有一个更好的JavaScript实现叫做Iron JS。它使用类似于Iron Python等的DLR。NuGet上的Iron Js。据我所知,Vsa已经过时了。 - Jeno Laszlo

1
这样做会有什么性能影响吗?
我们使用类似上述的系统,每个C#脚本都编译成内存中的程序集,并在单独的AppDomain中执行。目前还没有缓存系统,因此每次运行时都需要重新编译脚本。我进行了一些简单的测试,一个非常简单的“Hello World”脚本在我的机器上编译大约需要0.7秒,包括从磁盘加载脚本。对于脚本系统来说,0.7秒是可以接受的,但对于响应用户输入可能太慢了,在这种情况下,专用的解析器/编译器如Flee可能更好。
using System;
public class Test
{
    static public void DoStuff( Scripting.IJob Job)
    {
        Console.WriteLine( "Heps" );
    }
}

0

虽然C#本身没有原生支持Eval方法,但我有一个C# eval程序,可以允许评估C#代码。它提供了在运行时评估C#代码的功能,并支持许多C#语句。实际上,这段代码可用于任何.NET项目中,但仅限使用C#语法。请查看我的网站http://csharp-eval.com以获取更多详细信息。


0

0

0

只需使用连接字符串将您的算术表达式传递给 SQL

// INPUT
string eval = "SELECT ((1+2)-3*4)/5.0";

string strCon = $"Server=.\sqlexpress; Database=master; integrated security = true";

SqlConnection sqlCon = new SqlConnection(strCon);
sqlCon.Open();

SqlCommand cmd = new SqlCommand(eval, sqlCon);

SqlDataAdapter da = new SqlDataAdapter(cmd);
string result = da.SelectCommand.ExecuteScalar().ToString();

// OUTPUT
Console.WriteLine(result);

你必须添加 using System.Data.SqlClient;


嗯...我可以看到这种方法可行,但是...这样做会有很多开销。为了评估一个表达式而拉入整个关系型数据库管理系统(RDBMS)是过度杀伤力的。如果没有现成的解决方案,你最好托管(嵌入)像JavaScript、Lua或Python这样的脚本语言。或者PowerShell呢? - Daren Thomas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接