本文是【GoF设计模式】系列第22篇的前置知识,更多内容欢迎关注公众号:咖啡八杯

前言
解释器模式是 GoF 23 种设计模式中公认比较难理解的一个。它的难不在代码本身,而在前置概念——文法和 AST(抽象语法树)。如果不先搞懂这两个概念,直接看解释器模式的代码会一头雾水。
本文从零开始,用数学表达式 3 + 4 * 2 作为贯穿全文的例子,把文法和 AST 讲清楚。
文法与 BNF
什么是文法
文法就是语法规则。先看一个自然语言的例子:
我 吃 苹果 ✅
我 苹果 吃 ❌
为什么第一句对、第二句错?因为汉语的规则是:
主语 + 谓语 + 宾语
这就是一条文法规则。虽然大部分人没刻意学过这条规则,但能用它判断一句话合不合法。
计算机也一样。要让计算机理解 3 + 4 * 2,也得告诉它规则:
一个表达式 → 一个数字,后面可以跟「加号 + 数字」或者「乘号 + 数字」
但上面这句话其实有歧义——"后面可以跟"到底能跟多少个?是先加还是先乘?计算机听不懂模糊的话。
BNF 符号速览
科学家发明了一套记号,叫 BNF(巴科斯范式),用来精确描述文法。BNF 总共就几个符号:
| 符号 | 意思 | 一句话解释 |
|---|---|---|
→ |
"由……组成" | 左边是规则名,右边是它的结构 |
'...' |
引号里的就是字面本身 | '+' 就是加号字符 |
| | | 或者 | '+' | '*' 要么是加号要么是乘号 |
(...) |
分组 | 把一堆东西捆在一起 |
(...)* |
重复 0 次或无数次 | 可以有,也可以没有 |
| 普通单词 | 引用另一条规则 | 跳到那条规则去看 |
就这 6 个符号,没了。
用 BNF 描述数学表达式
数学表达式 3 + 4 * 2 和 10 * 2 + 3 * 5 的文法用 BNF 写出来是这样的:
expr → term ('+' term)*
term → factor ('*' factor)*
factor → NUMBER
逐句翻译:
第一句:expr → term ('+' term)*
expr → term ('+' term)*│ │ ││ │ └── 后面可以跟 0 组或多组「+ 号 + 另一个 term」│ ││ └── 开头先是一个 term│└── 「一个 expr(表达式)由以下部分组成:」
连起来:一个表达式 = 一个 term,后面可以跟零组或多组「加号 + 又一个 term」。
举个例子:
term→ 就是一个 term(没有加法)term + term→ 两个 term 相加term + term + term→ 三个 term 相加
第二句:term → factor ('*' factor)*
同理:一个 term = 一个 factor,后面可以跟零组或多组「乘号 + 又一个 factor」。
第三句:factor → NUMBER
一个 factor 就是一个数字。
注意:term、factor 这些名字不是关键字,可以随便起名,比如:
加法式 → 乘项 ('+' 乘项)*
乘项 → 因子 ('*' 因子)*
因子 → 数字
意思完全一样。
推导一个实际表达式
光看规则还是抽象。拿 3 + 4 * 2 实际走一遍推导过程:
第1步: expr ← 从根规则开始
第2步: → term ('+' term)* ← 展开 expr
第3步: → term '+' term ← 遇到 '+', 展开一组
第4步: → factor '+' term ← 第一个 term 展开成 factor
第5步: → "3" '+' term ← factor 匹配到数字 3
第6步: → "3" '+' factor '*' factor ← 第二个 term 展开成 factor * factor
第7步: → "3" '+' "4" '*' "2" ← 两个 factor 分别匹配到 4 和 2
推导完毕,所有字符都匹配上了 → 3 + 4 * 2 是合法的。
为什么文法层级决定优先级
注意第 6 步:term 展开成了 factor '*' factor,也就是说乘法在 term 这一层就被消化掉了,根本没机会传到 expr 层。
expr → term ('+' term)* ← expr 只看得见加法
term → factor ('*' factor)* ← term 内部处理乘法
所以 3 + 4 * 2 在计算机眼里是 3 + (4 * 2),不是 (3 + 4) * 2。优先级靠层级结构而非约定决定——在文法中嵌套的层级越深,优先级越高。这是 BNF 最核心的价值。
AST(抽象语法树)
从推导过程长出一棵树
上一节的推导过程,每一步都在"展开"规则。如果把展开的过程画出来,会看到一棵树在慢慢长成:
第1步: expr第3步: expr/ \term term第7步(最终):expr/ \term term│ / \factor factor factor│ │ │3 4 2
这棵树叫语法树——它完整记录了 3 + 4 * 2 是怎么从文法规则推导出来的。
从语法树到 AST
仔细看上面的树,里面有 expr、term、factor 这些节点。这些是推导过程中的中间产物,不是表达式里真正的东西。
问一个人 3 + 4 * 2 的运算结构,他会说:"就是一个加法,左边是 3,右边是 4 乘 2。"他不会说:"这是一个 expr,它展开成一个 term 加一个 term……"
所以需要抽象语法树(AST)——把那些中间推导节点去掉,只保留真正有意义的东西:
AddExpr/ \Number(3) MulExpr/ \Number(4) Number(2)
AST 为什么叫"抽象"? 因为它丢弃了:
- 空格——解析完就没用了
expr、term等中间概念——只是推导过程的脚手架- 括号(如果有的话)——优先级已经被树的结构固定了
只留下真正有用的信息:要算什么运算、操作数是谁。
对比三种表现形式:
| 形式 | 长什么样 | 特点 |
|---|---|---|
| 字符串 | "3 + 4 * 2" |
人类写的原始输入,含空格 |
| 语法树 | 包含 expr、term 等中间节点 | 信息完整但啰嗦 |
| AST | 只有 Add、Mul、Number | 只保留运算结构,干净 |
AST 的两类节点
看这棵树:
AddExpr ← 非终结符(内部节点)/ \Number(3) MulExpr ← 非终结符(内部节点)/ \Number(4) Number(2) ← 终结符(叶子节点)
| 节点类型 | 对应文法中的 | 说明 |
|---|---|---|
| 叶子节点(终结符) | factor → NUMBER |
直接有值,不用问别人,像一个士兵——自己就有战斗力 |
| 内部节点(非终结符) | expr → ...、term → ... |
需要先问下属拿到结果,才能决策,像一个将军 |
AST 的执行顺序
AST 的执行顺序是从下往上、从左到右(后序遍历):
AddExpr 第3步: 3 + 8 = 11/ \/ \Number(3) MulExpr 第2步: 4 * 2 = 8↑ / \│ / \第1步: 3 Number(4) Number(2)↑ ↑第1步: 4 第1步: 2
执行顺序:
- 先算叶子节点:
3、4、2(直接就是自身值) - 再算 MulExpr:
4 * 2 = 8 - 最后算 AddExpr:
3 + 8 = 11
这种"先子节点、后父节点"的顺序天然适合递归。每个节点先问完孩子,再算自己:
最外层节点(AddExpr):"告诉我值是多少?"它问左边 → 打开是 3它问右边 → 又是个套娃(MulExpr)打开 → 左边是 4,右边是 2"4 * 2 = 8"好,现在知道了:3 + 8 = 11
每一层只管一件事:问左边要结果,问右边要结果,然后自己算。这就是递归求值的本质,也是解释器模式中 interpret() 方法的底层逻辑。
总结
| 概念 | 一句话记住 |
|---|---|
| 文法 | 描述"一句话该怎么写"的规则 |
| BNF | 精确描述文法的记号,总共就 6 个符号 |
| 推导 | 按规则一步步展开,直到匹配所有字符 |
| 优先级 | 在文法中嵌套的层级越深,优先级越高 |
| 语法树 | 文法推导过程的完整记录 |
| AST | 去掉中间产物,只保留运算结构的精简树 |
| 叶子节点 | 直接有值,不用问别人 |
| 内部节点 | 需要先问子节点,再算自己 |
| 执行顺序 | 从下往上(后序遍历),每个节点递归计算 |
理解文法和 AST 之后,再看解释器模式就简单了:每条文法规则对应一个类,按文法构建 AST,递归执行 interpret()。把文法、AST 和递归求值写成代码,就是解释器模式。
技术交流 & 更多原创内容,关注公众号:咖啡八杯