是否可以解释 C# 表达式树以发出 JavaScript

本文关键字:JavaScript 解释 是否 表达式 | 更新日期: 2023-09-27 17:56:38

例如,如果你有一个这样的表达式:

Expression<Func<int, int>> fn = x => x * x;

有什么东西可以遍历表达式树并生成这个吗?

"function(x) { return x * x; }"

是否可以解释 C# 表达式树以发出 JavaScript

这可能并不容易,但是是的,这是绝对可行的。像Entity Framework或Linq to SQL这样的ORM可以将Linq查询转换为SQL,但实际上你可以从表达式树中生成任何你想要的东西......

应实现一个ExpressionVisitor来分析和转换表达式。


编辑:这是一个非常基本的实现,适用于您的示例:

Expression<Func<int, int>> fn = x => x * x;
var visitor = new JsExpressionVisitor();
visitor.Visit(fn);
Console.WriteLine(visitor.JavaScriptCode);
...
class JsExpressionVisitor : ExpressionVisitor
{
    private readonly StringBuilder _builder;
    public JsExpressionVisitor()
    {
        _builder = new StringBuilder();
    }
    public string JavaScriptCode
    {
        get { return _builder.ToString(); }
    }
    public override Expression Visit(Expression node)
    {
        _builder.Clear();
        return base.Visit(node);
    }
    protected override Expression VisitParameter(ParameterExpression node)
    {
        _builder.Append(node.Name);
        base.VisitParameter(node);
        return node;
    }
    protected override Expression VisitBinary(BinaryExpression node)
    {
        base.Visit(node.Left);
        _builder.Append(GetOperator(node.NodeType));
        base.Visit(node.Right);
        return node;
    }
    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        _builder.Append("function(");
        for (int i = 0; i < node.Parameters.Count; i++)
        {
            if (i > 0)
                _builder.Append(", ");
            _builder.Append(node.Parameters[i].Name);
        }
        _builder.Append(") {");
        if (node.Body.Type != typeof(void))
        {
            _builder.Append("return ");
        }
        base.Visit(node.Body);
        _builder.Append("; }");
        return node;
    }
    private static string GetOperator(ExpressionType nodeType)
    {
        switch (nodeType)
        {
            case ExpressionType.Add:
                return " + ";
            case ExpressionType.Multiply:
                return " * ";
            case ExpressionType.Subtract:
                return " - ";
            case ExpressionType.Divide:
                return " / ";
            case ExpressionType.Assign:
                return " = ";
            case ExpressionType.Equal:
                return " == ";
            case ExpressionType.NotEqual:
                return " != ";
            // TODO: Add other operators...
        }
        throw new NotImplementedException("Operator not implemented");
    }
}

它只处理具有单个指令的 lambda,但无论如何,C# 编译器无法为块 lambda 生成表达式树。

当然还有很多工作要做,这是一个非常小的实现......您可能需要添加方法调用(VisitMethodCall),属性和字段访问(VisitMember)等。

Script# 被Microsoft内部开发人员用来做到这一点。

看看 Lambda2Js,一个由 Miguel Angelo 为此目的创建的库。

它将CompileToJavascript扩展方法添加到任何表达式。

示例 1:

Expression<Func<MyClass, object>> expr = x => x.PhonesByName["Miguel"].DDD == 32 | x.Phones.Length != 1;
var js = expr.CompileToJavascript();
Assert.AreEqual("PhonesByName['"Miguel'"].DDD==32|Phones.length!=1", js);

示例 2:

Expression<Func<MyClass, object>> expr = x => x.Phones.FirstOrDefault(p => p.DDD > 10);
var js = expr.CompileToJavascript();
Assert.AreEqual("System.Linq.Enumerable.FirstOrDefault(Phones,function(p){return p.DDD>10;})", js);

更多例子在这里。

C# 编译器已为您分析了表达式;剩下的就是遍历表达式树并生成代码。遍历树可以递归完成,每个节点都可以通过检查它是什么类型来处理(Expression有几个子类,代表例如函数、运算符和成员查找)。每种类型的处理程序可以生成相应的代码并遍历节点的子级(根据表达式类型,这些子级将在不同的属性中可用)。例如,可以通过首先输出"function(",后跟参数名称,后跟"){"来处理函数节点。然后,可以递归处理主体,最后输出"}"。

一些人已经开发了开源库来解决这个问题。 我一直在看的是Linq2CodeDom,它将表达式转换为CodeDom图,然后只要代码兼容,就可以将其编译为JavaScript。

Script# 利用原始 C# 源代码和编译的程序集,而不是表达式树。

我对 Linq2CodeDom 进行了一些小的编辑,以将 JScript 添加为受支持的语言 - 基本上只是添加对 Microsoft.JScript 的引用,更新枚举,并在 GenerateCode 中添加另一个案例。 下面是转换表达式的代码:

var c = new CodeDomGenerator();
c.AddNamespace("Example")
    .AddClass("Container")
    .AddMethod(
        MemberAttributes.Public | MemberAttributes.Static,
        (int x) => "Square",
        Emit.@return<int, int>(x => x * x)
    );
Console.WriteLine(c.GenerateCode(CodeDomGenerator.Language.JScript));

结果如下:

package Example
{
    public class Container
    {
        public static function Square(x : int)
        {
            return (x * x);
        }
    }
}

方法签名反映了 JScript 的更强类型性质。 最好使用 Linq2CodeDom 生成 C#,然后将其传递给 Script# 以将其转换为 JavaScript。 我相信第一个答案是最正确的,但正如您通过查看 Linq2CodeDom 源代码所看到的那样,处理每个案例以正确生成代码需要付出很多努力。