递归枚举indirect
本教程共 93 篇 · 第 49 篇 · 更新于 2026-08-08 · 约 7 分钟阅读
本节目标:读完你理解什么叫递归枚举,会用 indirect 关键字定义能嵌套自身的枚举,并用递归函数处理它。
1-1 什么是递归枚举
普通枚举的成员就是值。但有时一个成员要”带着另一个同类型枚举”作为数据,比如算术表达式:(5 + 4) * 2。这里的乘法,左边是一个加法表达式,右边是一个数字;而那个加法表达式,又由两个数字组成。这种”自己套自己”的结构,普通枚举表达不了,需要递归枚举。
凡是”整体和局部长一样”的数据——表达式、文件系统目录、JSON 节点、家谱——都可能用到它。判断标准很简单:如果某个成员携带的数据里,可能再次出现”和我同类型的东西”,那它就是递归的。
1-2 indirect 关键字
Swift 要求显式用 indirect 标记,告诉编译器插入一层间接,否则编译器没法确定这个类型占多大内存:
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
也可以把 indirect 写在 enum 前面,对整个枚举生效:
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}
后者更省事:所有带关联值的成员都获得间接层。当只有个别成员递归、其余成员不带关联值时,写成单个 case 前的 indirect 更精确;当多个成员都递归时,写在 enum 前更简洁。
Noteindirect 只在编译层面插入一层”指针”间接,让你能无限嵌套;运行时性能和普通枚举无异。它和”递归函数”是两件事:前者是数据能嵌套,后者是逻辑能重复调用。本章的例子恰好两者都用上了:递归数据用递归函数处理。
1-3 构建嵌套表达式
拿 (5 + 4) * 2 举例:
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))
sum 是加法,它的两个操作数又是两个表达式;product 把 sum 和 number(2) 相乘。数据就这样一层层套起来。从外面看 product 是一个值,里面却藏着一棵小树。这种”树形”结构正是递归枚举的用武之地。
1-4 用递归函数求值
处理递归数据,最直观的办法就是递归函数——自己调用自己:
func evaluate(_ expression: ArithmeticExpression) -> Int {
switch expression {
case let .number(value):
return value
case let .addition(left, right):
return evaluate(left) + evaluate(right)
case let .multiplication(left, right):
return evaluate(left) * evaluate(right)
}
}
print(evaluate(product)) // 18
遇到 number 直接返回值;遇到加法/乘法就先算左右两边,再加或乘。switch 穷尽三个成员,无需 default。递归的妙处在于:无论表达式嵌套多少层,这个函数都能一路算到底。每深入一层,问题就变得更小,直到触底到 number。
1-5 递归枚举的常见场景
除了算术表达式,递归枚举还常用来描述树形或层级结构。比如一个简易的文件节点:要么是文件(带名字),要么是文件夹(带名字和一组子节点);子节点本身又是文件节点,于是文件夹就能装文件夹。只要某成员把”同类型枚举”放进关联值,并用 indirect 标记,就能自然表达这种无限嵌套。
Warning初学者容易忘记写 indirect,结果编译器报”枚举不能有递归关联值”。记住:只要成员的关联值里出现了”本枚举自己”,就必须加 indirect,否则编译不过。把 indirect 写在 enum 前能一次性覆盖所有成员,最不容易漏。
1-6 递归的边界要心里有数
递归很优雅,但要小心”层数太深”。虽然枚举本身支持任意嵌套,可对应的递归函数每深入一层就占一层调用栈。实际项目里,如果嵌套可能极深,要考虑会不会栈溢出——不过作为语言知识点,先掌握”数据能递归、函数能递归”这个配合就够。
1-7 递归枚举的存储代价
因为加了 indirect,每个递归成员实际上包了一层间接引用。对于表达式这种嵌套不深的数据,这点开销可以忽略;但如果你要表示非常庞大、扁平的递归结构,可以考虑用数组或栈来替代,避免过深的间接层。语言层面 Swift 已经做了优化,正常情况下你不必担心。
理解这层间接也有助于你建立”枚举到底占多大内存”的直觉:普通成员就是值本身,indirect 成员则是一个指向真实数据的引用,所以它能无限嵌套而不让类型尺寸爆炸。
1-8 和递归函数的配合要点
写递归函数处理递归枚举时,务必保证”每种情况都能触底”。以上面的 evaluate 为例,number 是终点,addition/multiplication 会递归调用自身但操作数最终都会落到 number,所以一定能算完。
如果某个分支又递归又不收敛,函数就会一直调用到自己栈溢出。写之前先在脑子里”走一遍”每个 case 是怎么收尾的。一旦确认每个分支要么直接返回、要么在更小的子问题上递归,这个函数就是安全的。
1-9 一个综合例子:树形菜单
假设要描述一个菜单,菜单项要么是一个可直接点的”按钮”(带标题),要么是一个”分组”(带标题和一组子项)。子项本身又是菜单项,于是分组能装分组:
indirect enum MenuItem {
case button(title: String)
case group(title: String, items: [MenuItem])
}
这就是递归枚举的典型落地:数据自我相似,用 indirect 表达最自然。遍历它同样靠递归函数,遇到 button 处理、遇到 group 再递归处理它的 items,直到最底层。
1-10 什么时候不必用递归枚举
递归枚举虽强,但不是所有”嵌套”都该用它。如果嵌套层数固定、结构简单,用普通的结构体套结构体往往更直白。只有当”子结构和整体同型、层数不固定”时,indirect 才显出价值。拿不准时,先想清楚数据是不是真的自我相似,再决定是否上递归枚举。
1-11 小结
递归枚举用 indirect 标记,让枚举成员能携带同类型实例,从而表达嵌套结构。可写在单个 case 前或整个 enum 前。配合递归函数遍历求值最自然。它常见于表达式、树形、JSON 节点等”子结构和整体同型”的场景。下章我们系统学枚举与模式匹配的配合。