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
相关产品推荐
相关产品推荐

