用 C# 开发一个解释器语言——基于《Crafting Interpreters》的实战系列(三)表达式的抽象语法树设计(Expr)
在前两篇博客中,我们完成了源码的词法分析,将代码拆解为 Token 流,接下来就进入理解如何用代码结构来表示程序中的表达式,也就是抽象语法树(AST)的构建。
本篇重点围绕书中核心数据结构 Expr 类展开,结合 C# 实现,详细讲解它的设计理念、组成部分及其作用。理解了它,就为后续语法分析器和解释器实现打下坚实基础。
为什么需要抽象语法树(AST)?
编程语言的源代码本质上是字符串,但解释器需要有“结构化”语义才能理解执行。抽象语法树正是将线性文本转换成结构化、树状的模型。
举例:
(1 + 2) * 3
文本难以直接表达操作顺序和层次,但 AST 会明确表达“先计算括号内的加法,再乘以3”:
*
/ \
+ 3
/ \
1 2
Expr 类的设计理念
Expr 是一个抽象基类,代表所有表达式的通用类型。不同的具体表达式继承自它:
- Literal:字面量表达式,比如数字、字符串等
- Binary:二元运算表达式,如加法、乘法
- Grouping:用括号分组的表达式
- Unary:一元运算表达式,如取负
- Variable、Assign 等(后续)
访问者模式在 Expr 中的应用
为了对表达式做各种操作(打印、求值、编译等),书中采用了访问者模式(Visitor Pattern),它能避免在表达式类里写死所有逻辑,而是分离操作和数据结构。
Visitor接口定义
public interface Visitor<T>
{
T VisitBinaryExpr(Binary expr);
T VisitGroupingExpr(Grouping expr);
T VisitLiteralExpr(Literal expr);
T VisitUnaryExpr(Unary expr);
}
- 这里的泛型
<T>表示访问方法的返回类型,可以是字符串、求值结果等。
Expr 抽象基类的 Accept 方法
public abstract T Accept<T>(Visitor<T> visitor);
- 每个具体表达式类都实现此方法,调用对应的访问者方法。
具体表达式类详解
1. Binary(二元表达式)
public class Binary : Expr
{
public Expr Left { get; }
public Token Operator { get; }
public Expr Right { get; }
public Binary(Expr left, Token op, Expr right)
{
Left = left;
Operator = op;
Right = right;
}
public override T Accept<T>(Visitor<T> visitor)
{
return visitor.VisitBinaryExpr(this);
}
}
Left和Right是操作数,均为Expr类型,支持嵌套表达式。Operator是Token,记录运算符本身,比如+。
2. Literal(字面量表达式)
public class Literal : Expr
{
public object Value { get; }
public Literal(object value)
{
Value = value;
}
public override T Accept<T>(Visitor<T> visitor)
{
return visitor.VisitLiteralExpr(this);
}
}
- 保存字面量值,可以是数字、字符串、布尔等。
- 访问时直接返回该值。
3. Grouping(分组表达式)
public class Grouping : Expr
{
public Expr Expression { get; }
public Grouping(Expr expression)
{
Expression = expression;
}
public override T Accept<T>(Visitor<T> visitor)
{
return visitor.VisitGroupingExpr(this);
}
}
- 表示括号内的表达式,用于明确优先级。
4. Unary(一元表达式)
public class Unary : Expr
{
public Token Operator { get; }
public Expr Right { get; }
public Unary(Token op, Expr right)
{
Operator = op;
Right = right;
}
public override T Accept<T>(Visitor<T> visitor)
{
return visitor.VisitUnaryExpr(this);
}
}
- 一元操作符,如负号
-。
Expr 的作用和价值
- 表达式树是解释器的核心数据模型,它将文本代码转换为层次分明的结构。
- 使用访问者模式后,我们可以灵活地扩展各种表达式操作,比如打印、求值、静态分析。
Expr的设计让代码清晰且易维护,符合面向对象设计原则。

浙公网安备 33010602011771号