翻开设计模式的书,解释器模式总会在接近最后的位置出现。这是一个很容易因为“这到底什么时候会用到”而略过的模式。
不过,在开发计算器 App 时,我遇到了必须直接计算字符串表达式的时刻。那时解释器模式正好派上了用场。
本文将使用 Swift 亲手创建一个非常小的语言解释器,也就是迷你解释器。目标是读取类似 1 + 2 * 3 的表达式,并输出数字 7。
先说结论:
解释器模式把每条语法规则转换为类(或枚举),组装成树结构,再通过递归遍历得到结果。
听起来很难,但看过代码后会发现比想象中简单。让我们一起实现它。
什么是解释器模式?
解释器模式是一种创建“自己的小型语言”,并用代码表达解释该语言的规则的方法。
这里说的语言并不复杂。只要是具有固定语法的小型表达式,例如公式、搜索过滤条件或游戏规则脚本,都属于这一范畴。
核心思想只有一个。
把语法的每个元素创建为对象,并让这些对象拥有解释自身的 interpret() 方法。
例如,考虑表达式 1 + 2。其中有数字 1、数字 2,以及加法运算 +。
数字会解释为“我只返回自己的值”,加法则解释为“先解释左侧和右侧,再把两者相加”。
这些小规则组合成一棵树,只要从最顶层调用一次解释,就会沿着树向下完成计算。
用 Swift 设计表达式树
在 Swift 中,enum 非常适合这种模式。使用递归枚举可以非常简洁地表示树结构。
我一开始用协议和类实现,但改成 enum 后,代码减少了一半。
首先定义表示表达式的枚举,分为一个数字,以及将两个表达式相加或相乘的情况。
这个枚举会再次包含自身,因此必须添加 indirect 关键字。
indirect enum Expr {
case number(Double) // 数字字面量
case add(Expr, Expr) // 加法
case multiply(Expr, Expr) // 乘法
}
现在创建解释这棵树的函数。递归处理每种情况,就是解释器模式的核心。
下面的函数接收一个表达式,计算并返回实际的数字值。
func interpret(_ expr: Expr) -> Double {
switch expr {
case .number(let value):
return value
case .add(let l, let r):
return interpret(l) + interpret(r)
case .multiply(let l, let r):
return interpret(l) * interpret(r)
}
}
做到这里,就可以把 1 + 2 * 3 组装成树并进行计算。
像 .add(.number(1), .multiply(.number(2), .number(3))) 那样手动创建树,再传入 interpret,就会得到结果 7。
如何把字符串表达式转换成树?
这里可能会产生一个疑问:用户输入的不是树,而是类似 "1 + 2 * 3" 的字符串。
把这个字符串转换成树的过程称为解析(parsing),执行这项工作的代码称为解析器。
按照标准做法,解析器分为两个阶段。
- 词法分析器(lexer):将字符串拆分为一个个 Token(例如:
1、+、2) - 解析器(parser):根据语法规则将 Token 组装成树
完整实现这两个阶段会让文章太长,所以这里仅介绍概念。
关键在于:解释器模式本身只负责解释树。把字符串解析成树是另一项工作。
因此在学习阶段,我建议暂时跳过解析器,手动创建树,先完成解释器。这样才能看清模式的本质。
实际用在哪里?
说实话,从底层亲自实现解释器模式的机会并不多。对于复杂语言,使用已经成熟的解析器库要好得多。
即便如此,理解这个模式仍然有很多收获。
- 可以了解正则表达式引擎在内部是如何工作的
- 可以理解为什么 SwiftUI 的声明式语法是树结构
- 创建搜索过滤器或表达式计算等小型 DSL(Domain-Specific Language,领域专用语言)时,可以直接应用
尤其是在计算器、条件过滤器或游戏脚本这类语法简单、范围有限的场景中,自己实现反而更简洁。
掌握这个模式后,我看 SwiftUI 的视图结构完全不同了。每个视图最终都是那棵待解释树中的一个节点。
总结
今天我们通过用 Swift 创建迷你语言解释器,了解了了解释器模式。
用递归枚举创建表达式树,再通过 interpret 函数递归解释。这两点就是全部。
刚开始可能会觉得陌生,但亲自计算 1 + 2 * 3 后,就会出现“啊,原来如此”的瞬间。请务必亲自敲一遍今天学到的代码。

