能否将C#表达式树解释为JavaScript代码?

29

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

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

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

"function(x) { return x * x; }"
5个回答

33

这可能不是很容易,但是完全可行。像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)等功能。


8

微软内部开发人员使用Script#来实现这一点。


+0... 实际上,Script#是一个非常完整的跨编译器...它可以转换C#源代码,但不能轻松地用于转换表达式对象。 - Alexei Levenkov
我的理解是,OP使用Expression对象作为一个理论例子,来探讨将C#代码编译成JavaScript是否可能,而不是将其用作实际实现的用途。 - Matthew Ratzloff

4
请看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);

更多示例请点击此处

1
表达式已经被 C# 编译器 解析,现在你只需要 遍历 表达式树并 生成 代码。可以通过递归方式遍历树,并通过检查节点的类型(有几个 Expression 的子类,表示函数、运算符和成员查找等)来处理每个节点。每种类型的处理程序都可以生成适当的代码并遍历节点的子节点(这些子节点将在不同的属性中可用,具体取决于表达式类型)。例如,函数节点可以通过首先输出 "function(",然后是参数名称,最后是 ") {" 来进行处理。然后,可以递归地处理主体,最后输出 "}"。

谢谢,"遍历"和"生成"是更准确的动词,我已经更新了问题。 - Chris Fulstow

1

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

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

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

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源代码,可以看到需要花费很多精力来处理每种情况以正确生成代码。


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