解释器模式前置:文法、BNF与AST

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

image

前言

解释器模式是 GoF 23 种设计模式中公认比较难理解的一个。它的难不在代码本身,而在前置概念——文法AST(抽象语法树)。如果不先搞懂这两个概念,直接看解释器模式的代码会一头雾水。

本文从零开始,用数学表达式 3 + 4 * 2 作为贯穿全文的例子,把文法和 AST 讲清楚。

文法与 BNF

什么是文法

文法就是语法规则。先看一个自然语言的例子:

我  吃  苹果  ✅
我  苹果  吃  ❌

为什么第一句对、第二句错?因为汉语的规则是:

主语 + 谓语 + 宾语

这就是一条文法规则。虽然大部分人没刻意学过这条规则,但能用它判断一句话合不合法。

计算机也一样。要让计算机理解 3 + 4 * 2,也得告诉它规则:

一个表达式 → 一个数字,后面可以跟「加号 + 数字」或者「乘号 + 数字」

但上面这句话其实有歧义——"后面可以跟"到底能跟多少个?是先加还是先乘?计算机听不懂模糊的话。

BNF 符号速览

科学家发明了一套记号,叫 BNF(巴科斯范式),用来精确描述文法。BNF 总共就几个符号:

符号 意思 一句话解释
"由……组成" 左边是规则名,右边是它的结构
'...' 引号里的就是字面本身 '+' 就是加号字符
| 或者 '+' | '*' 要么是加号要么是乘号
(...) 分组 把一堆东西捆在一起
(...)* 重复 0 次或无数次 可以有,也可以没有
普通单词 引用另一条规则 跳到那条规则去看

就这 6 个符号,没了。

用 BNF 描述数学表达式

数学表达式 3 + 4 * 210 * 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 就是一个数字

注意:termfactor 这些名字不是关键字,可以随便起名,比如:

加法式 → 乘项 ('+' 乘项)*
乘项   → 因子 ('*' 因子)*
因子   → 数字

意思完全一样。

推导一个实际表达式

光看规则还是抽象。拿 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

仔细看上面的树,里面有 exprtermfactor 这些节点。这些是推导过程中的中间产物,不是表达式里真正的东西。

问一个人 3 + 4 * 2 的运算结构,他会说:"就是一个加法,左边是 3,右边是 4 乘 2。"他不会说:"这是一个 expr,它展开成一个 term 加一个 term……"

所以需要抽象语法树(AST)——把那些中间推导节点去掉,只保留真正有意义的东西:

          AddExpr/       \Number(3)    MulExpr/       \Number(4)  Number(2)

AST 为什么叫"抽象"? 因为它丢弃了:

  • 空格——解析完就没用了
  • exprterm 等中间概念——只是推导过程的脚手架
  • 括号(如果有的话)——优先级已经被树的结构固定了

只留下真正有用的信息:要算什么运算、操作数是谁

对比三种表现形式:

形式 长什么样 特点
字符串 "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

执行顺序:

  1. 先算叶子节点:342(直接就是自身值)
  2. 再算 MulExpr:4 * 2 = 8
  3. 最后算 AddExpr:3 + 8 = 11

这种"先子节点、后父节点"的顺序天然适合递归。每个节点先问完孩子,再算自己:

最外层节点(AddExpr):"告诉我值是多少?"它问左边 → 打开是 3它问右边 → 又是个套娃(MulExpr)打开 → 左边是 4,右边是 2"4 * 2 = 8"好,现在知道了:3 + 8 = 11

每一层只管一件事:问左边要结果,问右边要结果,然后自己算。这就是递归求值的本质,也是解释器模式中 interpret() 方法的底层逻辑。

总结

概念 一句话记住
文法 描述"一句话该怎么写"的规则
BNF 精确描述文法的记号,总共就 6 个符号
推导 按规则一步步展开,直到匹配所有字符
优先级 在文法中嵌套的层级越深,优先级越高
语法树 文法推导过程的完整记录
AST 去掉中间产物,只保留运算结构的精简树
叶子节点 直接有值,不用问别人
内部节点 需要先问子节点,再算自己
执行顺序 从下往上(后序遍历),每个节点递归计算

理解文法和 AST 之后,再看解释器模式就简单了:每条文法规则对应一个类,按文法构建 AST,递归执行 interpret()。把文法、AST 和递归求值写成代码,就是解释器模式。

技术交流 & 更多原创内容,关注公众号:咖啡八杯