You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C# DSL表达式求值器中如何灵活表示标量/向量混合中间结果

问题:动态类型表达式求值(标量/向量混合运算)

我需要求值类似如下的表达式:

LARGEST(A) + B
        A 为 double[] {1,2,3,4,5}
        B 为 double[] {2,3,4,5,6}

我正在使用Irony,并已生成合适的抽象语法树(AST):

1. binary_add
2.     function_largest
3.         var_A
4.     var_B

核心问题是:在计算过程中,哪种类或变量类型足够灵活,能够容纳不同类型的中间结果?

  • 中间结果可能是标量或向量(即double或double[]),其类型要到求值时才能确定。
  • 代码需要根据之前的中间结果在运行时选择合适的Add重载:
void Add(double[] vector, double scalar) {}
void Add(double scalar1, double scalar2) {}
void Add(double[] vector1, double[] vector2) {}

求值顺序如下:

evaluation order
binary_add                  4. {7,8,9,10,11}     vector(将largest()的标量结果与向量每个元素逐元素相加得到)
    function_largest        2. 5                 scalar(计算得出)
        var_A               1. {1,2,3,4,5}       vector(给定值)
    var_B                   3. {2,3,4,5,6}       vector(给定值)

我可以用object类型配合大量GetType()判断来选择合适的重载,但有没有更智能、更符合C# idiom的方案?比如使用动态类型(Dynamics)、泛型等?


符合C#习惯的解决方案

1. 使用dynamic类型

直接用dynamic存储中间结果,C#会在运行时自动匹配对应的重载,代码简洁直观:

public class Calculator
{
    public double[] Add(double[] vector, double scalar)
    {
        return vector.Select(x => x + scalar).ToArray();
    }

    public double Add(double scalar1, double scalar2)
    {
        return scalar1 + scalar2;
    }

    public double[] Add(double[] vector1, double[] vector2)
    {
        if (vector1.Length != vector2.Length)
            throw new ArgumentException("向量长度不匹配");
        return vector1.Zip(vector2, (a, b) => a + b).ToArray();
    }

    public double Largest(double[] vector)
    {
        return vector.Max();
    }
}

// 求值示例
var calculator = new Calculator();
dynamic varA = new double[] {1,2,3,4,5};
dynamic varB = new double[] {2,3,4,5,6};
dynamic largestResult = calculator.Largest(varA);
dynamic finalResult = calculator.Add(largestResult, varB);
// finalResult 结果为 {7,8,9,10,11}

优缺点:

  • 优点:代码极简,无需手动判断类型,自动匹配重载,开发效率高。
  • 缺点:失去编译时类型检查,错误只能在运行时暴露;性能略低于静态类型(但绝大多数表达式求值场景可忽略)。

2. 自定义可辨识联合类型

用结构体封装标量和向量,明确类型标识,避免object的装箱和无类型检查问题:

public struct NumericValue
{
    public bool IsScalar { get; }
    public double ScalarValue { get; }
    public double[] VectorValue { get; }

    private NumericValue(double scalar)
    {
        IsScalar = true;
        ScalarValue = scalar;
        VectorValue = null;
    }

    private NumericValue(double[] vector)
    {
        IsScalar = false;
        ScalarValue = default;
        VectorValue = vector ?? throw new ArgumentNullException(nameof(vector));
    }

    // 工厂方法创建实例
    public static NumericValue FromScalar(double value) => new NumericValue(value);
    public static NumericValue FromVector(double[] value) => new NumericValue(value);

    // 重载加法运算符,内部处理所有类型组合
    public static NumericValue operator +(NumericValue a, NumericValue b)
    {
        if (a.IsScalar && b.IsScalar)
            return FromScalar(a.ScalarValue + b.ScalarValue);
        if (a.IsScalar && !b.IsScalar)
            return FromVector(b.VectorValue.Select(x => x + a.ScalarValue).ToArray());
        if (!a.IsScalar && b.IsScalar)
            return FromVector(a.VectorValue.Select(x => x + b.ScalarValue).ToArray());
        // 两个向量相加的情况
        if (a.VectorValue.Length != b.VectorValue.Length)
            throw new ArgumentException("向量长度不匹配");
        return FromVector(a.VectorValue.Zip(b.VectorValue, (x, y) => x + y).ToArray());
    }

    // 实现Largest函数逻辑
    public static NumericValue Largest(NumericValue value)
    {
        if (!value.IsScalar)
            return FromScalar(value.VectorValue.Max());
        throw new InvalidOperationException("Largest只能作用于向量");
    }
}

// 使用示例
var varA = NumericValue.FromVector(new double[] {1,2,3,4,5});
var varB = NumericValue.FromVector(new double[] {2,3,4,5,6});
var largestResult = NumericValue.Largest(varA);
var finalResult = largestResult + varB;

优缺点:

  • 优点:编译时类型安全,明确的类型标识,无装箱开销,错误可提前捕获。
  • 缺点:需要手动实现所有运算的分支逻辑,当运算类型增多时代码量会相应增加。

3. 访问者模式

如果AST节点类型固定,可通过访问者模式将运算逻辑与节点分离,同时处理不同类型的中间结果:

// AST节点基类与访问者接口
public abstract class AstNode
{
    public abstract NumericValue Accept(IAstVisitor visitor);
}

public interface IAstVisitor
{
    NumericValue VisitBinaryAdd(BinaryAddNode node);
    NumericValue VisitFunctionLargest(FunctionLargestNode node);
    NumericValue VisitVariable(VariableNode node);
}

// 具体AST节点类型
public class BinaryAddNode : AstNode
{
    public AstNode Left { get; }
    public AstNode Right { get; }
    public BinaryAddNode(AstNode left, AstNode right)
    {
        Left = left;
        Right = right;
    }

    public override NumericValue Accept(IAstVisitor visitor)
    {
        return visitor.VisitBinaryAdd(this);
    }
}

public class FunctionLargestNode : AstNode
{
    public AstNode Argument { get; }
    public FunctionLargestNode(AstNode argument)
    {
        Argument = argument;
    }

    public override NumericValue Accept(IAstVisitor visitor)
    {
        return visitor.VisitFunctionLargest(this);
    }
}

public class VariableNode : AstNode
{
    public string Name { get; }
    public NumericValue Value { get; }
    public VariableNode(string name, NumericValue value)
    {
        Name = name;
        Value = value;
    }

    public override NumericValue Accept(IAstVisitor visitor)
    {
        return visitor.VisitVariable(this);
    }
}

// 求值访问者实现
public class EvaluationVisitor : IAstVisitor
{
    public NumericValue VisitBinaryAdd(BinaryAddNode node)
    {
        var leftValue = node.Left.Accept(this);
        var rightValue = node.Right.Accept(this);
        return leftValue + rightValue; // 复用NumericValue的加法逻辑
    }

    public NumericValue VisitFunctionLargest(FunctionLargestNode node)
    {
        var argValue = node.Argument.Accept(this);
        return NumericValue.Largest(argValue);
    }

    public NumericValue VisitVariable(VariableNode node)
    {
        return node.Value;
    }
}

// 使用示例
var varA = NumericValue.FromVector(new double[] {1,2,3,4,5});
var varB = NumericValue.FromVector(new double[] {2,3,4,5,6});
var ast = new BinaryAddNode(
    new FunctionLargestNode(new VariableNode("A", varA)),
    new VariableNode("B", varB)
);
var visitor = new EvaluationVisitor();
var finalResult = ast.Accept(visitor);

优缺点:

  • 优点:符合开闭原则,新增运算或节点类型时无需修改现有代码;逻辑清晰,节点与运算完全分离。
  • 缺点:初始代码结构较复杂,适合节点类型和运算较多的复杂场景。

内容的提问来源于stack exchange,提问作者amonroejj

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 21:35:03