在 Excel 公式里实现递归,这件事在过去听起来像天方夜谭。你有一组数据,需要反复套用同一个逻辑,但公式世界里没有 for 循环,也没有 while;你想定义一个自己的函数,但函数列表永远是那几百个内置函数。于是,项目只要遇到一点分治、迭代、嵌套结构,就不得不切到 VBA,或者铺十几条辅助列硬扛。
直到LAMBDA与REDUCE出现。Excel 365 和 WPS 新版本把它们带进了普通公式,让用户终于可以在单元格里写出“自己调用自己”的函数,也可以像写 reduce 一样把整个数组压成一个值。很多人看到这两个函数时,会产生一个很有趣的问题:REDUCE 和 LAMBDA 递归,到底谁是盟主?
我的判断是:这根本算不上同层级的竞争。REDUCE是折叠数组的循环工具,LAMBDA是让一段计算获得名字与自引用能力的入口。如果只是单纯重复几千次,REDUCE 往往更稳;如果要做树形遍历、整数转字符串、分治算法,你绕不开被命名后的 LAMBDA 递归。而真正决定递归成败的,是那个藏在IF里的终止条件。
下面我会把 LAMBDA、REDUCE、递归、终止条件、循环深度放到同一个坐标系里拆开讲,并带几个可以直接复制到 Excel/WPS 名称管理器里的实战函数:整数转字符串、各位数字求和、阶乘。看完你会清楚:什么时候该用 REDUCE,什么时候该用 LAMBDA 递归,以及遇到#NUM!和卡顿时,第一步该去查什么。
1. 这篇文章真正要解决的问题
很多 Excel 用户学会SUM、IF、VLOOKUP之后,会进入一个瓶颈期:能处理 80% 的日常表,但遇到需要“重复处理同一件事”的场景就卡住了。
比如这样几个问题:
- 把一个整数
20240607转成字符串"20240607",不借助文本函数之外的操作,纯函数式怎么写? - 把一列数字逐位相加,直到结果为个位数,比如
12345 → 1+2+3+4+5=15 → 1+5=6,公式怎么写? - 有一批订单号,结构是“前缀 + 不定长数字”,需要把它们按规则重新拼装,怎么办?
传统解决方案无非三种:辅助列、迭代计算、VBA。辅助列会污染表格结构;迭代计算开启后全局生效,容易引入隐藏 bug;VBA 又要开启宏,在许多企业环境里受限。
LAMBDA和REDUCE改变了这个局面。它们让 Excel/WPS 公式第一次拥有了“函数式编程”的完整能力:匿名函数、高阶函数、递归、累积器。这意味着你可以在一个单元格里完成过去需要 VBA 才能实现的逻辑,而且公式可以保存到名称管理器里,像内置函数一样复用。
但这里也有一个需要提前说明的现实:LAMBDA 递归和 REDUCE 并不总是等价替换。两者的底层思路不一样,适用的场景也不一样。如果不理解终止条件和循环深度,很容易写出一个“看起来正确、但一运行就溢出”的公式。
这篇文章适合三类读者:
- 已经熟悉
SUM、IF、VLOOKUP等常用函数,想进阶函数式写法的 Excel/WPS 用户。 - 对 Java/Python 中
lambda和reduce有了解,想知道 Excel 里对应实现方式的开发者。 - 正在评估“能不能用纯公式替代 VBA/辅助列”的办公自动化实践者。
2. 基础概念:LAMBDA、REDUCE、递归、终止条件、循环深度
2.1 LAMBDA:让公式拥有“函数名”
LAMBDA的语法非常直接:
=LAMBDA(参数1, 参数2, ..., 计算表达式)它本质上就是一个匿名函数。比如:
=LAMBDA(x, x * 2)(21)运行结果是42。括号里的21是传给参数x的实际值。
如果只是这样,LAMBDA 还没有太大威力。关键在于:你可以在名称管理器中给它定义一个名字。例如新建一个名称为Double的条目,引用位置填写:
=LAMBDA(x, x * 2)保存之后,你在工作表中写=Double(21)会得到42。这就是“自定义函数”的真实实现方式。
递归必须依赖“函数拥有名字”这个前提。一个匿名函数没有办法调用自身,因为它在定义时还没有名字;但一旦在名称管理器中命名为F,函数体里就可以写F(...)来引用自己。这是整个 LAMBDA 递归的基石。
2.2 REDUCE:数组折叠器
REDUCE的语法是:
=REDUCE([初始值], 数组, LAMBDA(累加器, 当前值, 计算表达式))它的作用是把一个数组从左到右扫描一遍,不断地用“累加器”和“当前值”计算出一个新累加器,最后只返回一个值。
看一个最简单的例子:
=REDUCE(0, {1;2;3;4;5}, LAMBDA(acc, v, acc + v))运算过程是:
- 初始
acc = 0 - 取
v = 1,acc = 0 + 1 = 1 - 取
v = 2,acc = 1 + 2 = 3 - 取
v = 3,acc = 3 + 3 = 6 - 取
v = 4,acc = 6 + 4 = 10 - 取
v = 5,acc = 10 + 5 = 15
最终结果是15。
这个模式对应编程语言里的fold/reduce:functools.reduce、JavaStream.reduce、JavaScriptArray.reduce都是同一类东西。
注意,REDUCE里的LAMBDA不需要名称管理器,它是内联的匿名函数。REDUCE引擎会负责迭代,你不需要自己写递归调用,所以它天然规避了“自引用”问题,也不需要显式写终止条件——数组遍历完,过程自然结束。
2.3 递归:自己调用自己,但必须会停下来
递归是一种解决问题的方法:函数在执行过程中调用自身,把一个大问题拆成更小的同构子问题,直到某个最小问题可以直接给出答案。
在 Excel/WPS 公式里,一个递归过程必须同时满足三个条件:
- 基础情况:有一个参数取值,能直接返回结果,不再调用自身。
- 递归调用:调用自身时,参数要朝基础情况收敛。
- 终止条件判断:通常用
IF区分基础情况和递归情况。
没有终止条件的递归,相当于一个没有出口的循环,最终会耗尽计算资源。Excel/WPS 会返回错误或卡死。
2.4 循环深度:递归能叠多少层
循环深度指递归调用最多嵌套多少层,也叫递归深度或调用栈深度。普通REDUCE的“层数”由数组长度决定,引擎内部自动折返;而 LAMBDA 递归的层数由你调用自己的次数决定,每层都占用一份栈空间。
不同版本、不同计算引擎对递归深度支持的极限不一样。有的版本能跑到几百层,有的版本可能几十层就报错。掌握这个概念,你才能判断一个递归公式能否落地。
下面用一个表格对比循环、迭代、递归在 Excel 公式中的表现:
| 维度 | REDUCE 循环 | LAMBDA 递归 |
|---|---|---|
| 函数是否需要名称 | 不需要 | 需要在名称管理器中命名 |
| 终止条件 | 数组遍历结束自动停止 | 必须用 IF 显式设计 |
| 典型场景 | 累加、拼接、过滤后折叠 | 分治、树的遍历、数学递归 |
| 循环深度风险 | 与数组长度相关,相对稳定 | 与递归次数相关,更容易溢出 |
| 可读性 | 接近函数式编程 | 需要画递归树来理解 |
| 对新手友好度 | 更高 | 需要更多练习 |
把这两个函数区别清楚之后,我们就可以进入实际操作了。
3. 环境准备:先确认你的 Excel/WPS 能不能跑
3.1 版本与可用性检查
LAMBDA和REDUCE并不是所有版本都支持。打开你的 Excel 或 WPS,在任意单元格输入下面两个公式进行探测:
=LAMBDA(a, a)(1)=REDUCE(0, {1;2;3}, LAMBDA(a, b, a + b))如果都能正常返回1和6,说明当前版本支持这两个函数。如果提示名称错误或显示#NAME?,则说明当前版本不具备该能力,需要考虑升级,或者继续使用辅助列。
WPS 的支持情况跟随版本变化较快,不同时期的版本体验可能不同。稳妥的判断方式是:以官方更新日志为准,并在自己的 WPS 版本里跑一下上面的最小示例。
3.2 名称管理器:LAMBDA 递归的“户口本”
LAMBDA 递归必须通过名称管理器给函数命名。具体位置:
- Excel:
公式→名称管理器→新建 - WPS:
公式→名称管理器→新建
在名称管理器中,一条 LAMBDA 递归定义通常包含两个字段:
- 名称:例如
NumToStr - 引用位置:例如
=LAMBDA(n, IF(...))
引用位置必须是等号开头。名称管理器保存后,你可以在任意单元格直接使用=NumToStr(2024),就像使用内置函数一样。
这里有一个新手容易踩的坑:在单元格里直接写=LAMBDA(n, IF(n<10, TEXT(n,"0"), n & ""))是能运行的,但函数体内部如果再写NumToStr(...),Excel/WPS 会因为找不到这个名字而报错。必须先到名称管理器里新建NumToStr,然后函数体里才能引用它。
4. 第一回合:用 REDUCE 实现循环折叠
我们先用 REDUCE 处理一批典型的“循环折叠”任务,感受它作为循环工具的能力和边界。
4.1 累加求和
累加是最经典的 reduce 应用。在单元格输入:
=REDUCE(0, {1;2;3;4;5;6;7;8;9;10}, LAMBDA(acc, v, acc + v))结果应该是55。
这个例子对应 Python 的:
from functools import reduce result = reduce(lambda acc, v: acc + v, range(1, 11), 0) print(result) # 55也对应 Java 的:
import java.util.List; int result = List.of(1,2,3,4,5,6,7,8,9,10) .stream() .reduce(0, Integer::sum); // 55可以清楚地看到,Excel 的REDUCE和编程语言里的reduce是同一个思路:初始值 + 数组 + 合并逻辑。
4.2 字符串拼接
REDUCE 也擅长把多个单元格内容拼起来。比如把一组字母拼成单词:
=REDUCE("", {"J";"A";"V";"A"}, LAMBDA(acc, v, acc & v))结果是"JAVA"。
如果有一个区域是A1:A5,内容是"A"、"B"、"C"、"D"、"E",可以这样写:
=REDUCE("", A1:A5, LAMBDA(acc, v, acc & v))在旧版 Excel 里,这种操作通常需要CONCATENATE或TEXTJOIN。但TEXTJOIN只能按指定分隔符拼接,无法在拼接过程中对每个元素做额外逻辑。REDUCE 可以:你想在拼接前判断、格式化、过滤,都不需要辅助列。
4.3 带分支的累积:只累加偶数
REDUCE 的 LAMBDA 参数中可以使用IF,所以它可以做“带条件”的循环。比如对1 到 10只累加偶数:
=REDUCE(0, {1;2;3;4;5;6;7;8;9;10}, LAMBDA(acc, v, IF(MOD(v, 2) = 0, acc + v, acc)))结果应该是30,也就是2 + 4 + 6 + 8 + 10。
这个公式里,IF负责控制累积行为:满足条件才加,不满足就保留原累加器。这相当于编程语言里if (v % 2 == 0) sum += v;的逻辑。
通过这几个示例,你应该已经感受到:REDUCE 适合处理“对整个数组做一遍规则明确、可累积处理”的场景。它是循环层的工具,不需要递归自引用,因此也不存在终止条件的设计问题。
5. 第二回合:用 LAMBDA 递归实现分治与转化
有些问题不适合用“遍历数组”来理解,而更适合用“把大问题拆成小问题”。这类问题需要递归。
5.1 阶乘:最朴素的递归入门
阶乘的定义是:
F(0) = 1F(n) = n * F(n-1)
打开名称管理器,新建名称MYFACT,引用位置填写:
=LAMBDA(n, IF(n <= 1, 1, n * MYFACT(n - 1)))单元格输入:
=MYFACT(5)得到120。
把这个函数拆开看:
- 终止条件:
IF(n <= 1, 1, ...),当n小于等于 1 时直接返回 1。 - 递归调用:
n * MYFACT(n - 1),每次n都减 1,最终必然触达n <= 1。
如果去掉IF,直接写=LAMBDA(n, n * MYFACT(n - 1)),那么当n为 5 时,会无限调用MYFACT(4->3->2->1->0->-1->...),最终导致计算溢出。
5.2 递归法将一个整数 n 转换成字符串
这是递归里非常典型的一道题,对应我们常见的“整数转字符串”需求。常规思路是:取n的最后一位数字,再递归处理除掉最后一位后的部分。
新建名称NumToStr,引用位置:
=LAMBDA(n, IF(n < 10, TEXT(n, "0"), NumToStr(INT(n / 10)) & TEXT(MOD(n, 10), "0")))单元格输入:
=NumToStr(20240607)返回结果:
20240607注意:结果是字符串,不是数字。因为TEXT返回文本,&也是文本拼接运算。
这段公式的执行过程可以展开成:
NumToStr(20240607) = NumToStr(2024060) & "7" = (NumToStr(202406) & "0") & "7" = ((NumToStr(20240) & "6") & "0") & "7" = ... = "2" & "0" & "2" & "4" & "0" & "6" & "0" & "7" = "20240607"这里的终止条件就是n < 10:当数字只剩一位时,直接返回它的文本形式。每次递归都通过INT(n / 10)把数字缩到原来的十分之一,所以一定会收敛。
这个示例也说明了一个通用套路:递归处理数字时,最常用的两个操作是INT去掉低位、MOD取出低位。它天然适合从右往左逐位拆解。
5.3 处理负数的递归增强版
上面的NumToStr只适用于非负整数。如果n是负数,比如-123,n < 10成立,但TEXT(-123, "0")会直接返回"-123",后续拼接逻辑会乱掉。
一个更健壮的写法是:先处理负号,再递归处理绝对值部分。
新建名称NumToStr2:
=LAMBDA(n, IF(n < 0, "-" & NumToStr2(-n), IF(n < 10, TEXT(n, "0"), NumToStr2(INT(n / 10)) & TEXT(MOD(n, 10), "0"))))这个公式里有两个终止分支:
n < 0:先加负号,再递归到正数分支。n < 10:数字只剩一位时直接返回。
你可以在工作表中验证:
=NumToStr2(-2024)结果:
-2024这里想强调一个递归设计的要点:分支越多,越要保证每个分支都朝“基础情况”收敛。负号分支把负数转为正数,正数分支再通过INT、MOD不断缩小数字,最终一定能落到n < 10。
5.4 数字各位求和:递归的二次收敛
“各位数字求和直到一位数”也是一个很适合递归的小题。它的规则是:如果数字小于 10,直接返回;否则把所有位的数字相加,然后再递归判断。
新建名称DigitSum:
=LAMBDA(n, IF(n < 10, n, DigitSum(INT(n / 10) + MOD(n, 10))))这里和前面不同:递归参数不是单纯缩小一位数字,而是“把当前数字的各位加总”之后再次进入函数。比如:
DigitSum(12345) = DigitSum(1234 + 5) = DigitSum(1239) = DigitSum(123 + 9) = DigitSum(132) = DigitSum(13 + 2) = DigitSum(15) = DigitSum(1 + 5) = DigitSum(6) = 6最终得到6。这个递归里,终止条件同样是n < 10,但收敛方式更漂亮:每次调用都把数字长度缩短,同时数值也在不断变小。
5.5 REDUCE 能替代这些递归吗
如果非要用 REDUCE 实现整数转字符串,一种思路是先拆出每一位数字到一个数组,再用手服务。这里就能看到递归的不可替代性了。REDUCE 天生处理“已经存在的数组”,而递归处理“如何生成这个数组/区间/层次”的问题。
两者并不谁替代谁,而是分工不同:
- 已经有数组,做折叠合并:用 REDUCE。
- 问题需要逐层拆解、规模递减:用 LAMBDA 递归。
6. 终止条件:递归能不能活下来的关键
6.1 终止条件的三个必要条件
Excel/WPS 的 LAMBDA 递归,在设计时至少要考虑三件事:
第一,是否存在基础情况。所谓基础情况,就是某个参数值下直接返回结果,不再调用自身。阶乘里的n <= 1、整数转字符串里的n < 10,都是基础情况。
第二,递归是否朝基础情况收敛。每次调用自身时,参数必须变得更接近基础情况。例如MYFACT(n - 1)让n递减,NumToStr(INT(n / 10))让数字位数减少。如果递归参数不收敛,就会无限循环。
第三,IF 条件是否覆盖所有输入。如果函数允许输入负数、小数、空值,而你只设计了正整数的分支,那么在特殊值上可能出现不可预期的结果。要么提前丢弃非法输入,要么像NumToStr2一样增加负号分支。
6.2 没有终止条件会发生什么
如果把阶乘写错成这样:
=LAMBDA(n, n * MYFACT(n - 1))那么=MYFACT(5)会一直调用MYFACT(4)、MYFACT(3)……直到计算引擎的递归深度或栈空间被耗尽。现象通常是:单元格变成一个等待状态,或者直接报#NUM!,甚至整个工作簿都卡顿很久。
更隐蔽的错误是“终止条件看起来有,但收敛速度太慢”。比如两个递归分支互相来回转换,参数的数值在一段时间内反复震荡。这种设计比直接没有终止条件更难排查,因为你会看到它确实调用了几次,但最终依然溢出。
6.3 怎么验证终止条件没有问题
一个实用的方法是:先用小参数做最小验证。
- 阶乘:先测
=MYFACT(1)、=MYFACT(2)。 - 整数转字符串:先测
=NumToStr(9)、=NumToStr(10)、=NumToStr(99)。 - 数字求和:先测
=DigitSum(9)、=DigitSum(99)、=DigitSum(123)。
如果最小的几个数据都正确,再慢慢加大规模。不要一开始就把100000丢进递归函数。递归公式的调试远比普通公式难,因为你没办法在公式里加断点。控制变量、从小数据开始,是最经济的手段。
7. 循环深度:递归的物理上限
7.1 什么是调用栈
每次调用一个函数,计算引擎都要为这次调用保存当前的状态,包括参数、返回值位置、局部变量。这些信息存放在一块叫“栈”的内存区域。递归每深入一层,栈就多一层。当递归层次过深,栈空间用尽,就会溢出。
普通非递归公式不容易触发这个限制,因为 Excel 的求值引擎会尽量简化中间过程。但 LAMBDA 递归是真正的“函数调用”,每层递归都可能消耗栈空间。因此,循环深度是设计递归公式时绕不开的工程约束。
REDUCE 有所区别:它的迭代过程在引擎内部实现,你可以把它理解成“伪递归”。虽然 LAMBDA 表达式在概念上每处理一个元素都会“调用一次”,但引擎通常会把它优化成稳定的循环结构,所以它比手写递归的栈负担更小。
7.2 测试自己环境的递归上限
不同版本的 Excel/WPS,以及不同操作系统的 Excel,递归深度上限都可能不同。与其在网上查一个写死的数字,不如自己在环境里测一下。
新建名称DepthTest:
=LAMBDA(n, IF(n = 0, "OK", DepthTest(n - 1)))然后分别在单元格输入:
=DepthTest(10) =DepthTest(50) =DepthTest(100) =DepthTest(200) =DepthTest(500)哪个开始报#NUM!或卡顿,你就知道自己当前环境大概能承受多少层。注意,这是一个极其耗资源的测试方式,建议一次性从小数字向上逐步测,不要直接输入一个非常大的数字。
7.3 不同编程语言的递归深度对比
递归深度并不是 Excel 独有的问题。几乎所有主流语言都有类似限制:
- Python 默认递归深度约为 1000 层,超过会抛出
RecursionError。 - Java 的递归深度受 JVM 栈大小影响,默认栈通常能支持几千层,但深了同样会
StackOverflowError。 - JavaScript 中,过深递归最终会触发“Maximum call stack size exceeded”。
这说明一个问题:递归的数学表达很漂亮,但工程落地时必须关心栈资源。Excel/WPS 公式不像 VBA 那样可以调整线程或栈大小,所以更要遵守“能迭代就不递归,能递归就控制层次”的原则。
7.4 深度限制下的工程对策
遇到深度过深,如何改造?
第一种是增加终止条件阈值。比如把n <= 1改成n <= 10,然后把基础值提前算好。但这只是小规模优化,治标不治本。
第二种是“转迭代”。当你明显不需要递归结构,只是重复同样的逻辑时,优先用 REDUCE 或辅助列。例如求和、拼接、条件累积,REDUCE 都能稳定完成,不需要自己维护递归栈。
第三种是“改分治”。把一个大问题切成两个小问题,递归深度从 O(n) 降到 O(log n)。最典型的就是归并排序、二分查找。虽然 Excel 公式实现分治语法比较繁琐,但思路完全一致。这也是为什么“递归二路归并排序”这类题目在算法学习中极度重要——它不只训练递归,更训练你控制递归深度。
如果你的工作簿已经逼近深度上限,还有一个简单粗暴但工程上很有效的方式:拆步骤。用辅助列或中间结果把一次递归拆成两段。虽然失去了“纯公式”的优雅,但胜在稳定,不会把整个工作簿拖垮。
8. 常见问题与排查思路
下面把 LAMBDA 递归和 REDUCE 使用中常见的问题整理成一个排查表:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
单元格报#NAME? | 当前版本不支持 LAMBDA/REDUCE,或名称未正确定义 | 先输入=LAMBDA(a,a)(1)和=REDUCE(0,{1;2},LAMBDA(a,b,a+b))做检测 | 升级版本;确认名称管理器中名称已保存 |
公式能够输入,但返回#NUM! | 递归深度超出当前环境上限,或缺少终止条件 | 用DepthTest公式测试当前环境的深度上限;检查 IF 分支是否收敛 | 增加终止条件;改用 REDUCE;用分治降低递归深度 |
| 公式卡顿,长时间不返回 | 递归没有终止条件,或参数没有朝基础情况收敛 | 检查递归参数变化;先输入小参数测试 | 修正递归收敛逻辑;避免使用大参数直接测试 |
| 名称管理器中定义后,单元格仍找不到名称 | 名称拼写错误,或名称管理器作用域不对 | 打开名称管理器,检查名称是否存在;输入名称时等待自动补全提示 | 删除多余空格;重新命名;复制名称管理器中的完整路径 |
| 在 WPS 中打开文件,LAMBDA 相关函数全部失效 | WPS 版本不支持或解析差异 | 在 WPS 中运行最小示例=LAMBDA(a,a)(1) | 以 WPS 官方版本支持为准;无法使用则退回辅助列/VBA |
| 递归结果在小数据正确,大数据出错 | 超出递归深度上限,或计算资源不足 | 逐步增加参数,观察哪个数量级开始失效 | 改用 REDUCE 迭代;拆步骤;控制数组规模 |
MOD对负数结果与预期不一致 | 负数取模在不同环境规则不同 | 单独测试=MOD(-7,10)的返回结果 | 在递归中先统一处理符号,如IF(n<0, ...) |
排查时有一个总原则:先把异常范围缩小到最小。如果公式出错,先用常量数组而不是区域引用测试;如果递归出错,先用 0、1、2 这种小参数测试。这样可以快速区分是逻辑错误还是环境限制。
9. 最佳实践与工程建议
9.1 命名规范要清晰
名称管理器里会积累很多 LAMBDA 递归函数,命名不清晰会非常痛苦。建议统一前缀,例如用FN_开头表示自定义函数:
FN_FACTFN_NUM_TO_STRFN_DIGIT_SUM
这样在工作表中输入=FN_时,自动补全列表会很整齐,别人接手工作簿时也能一眼辨认出哪些是自定义函数。
9.2 能用 REDUCE 就优先 REDUCE
凡是“已经有一批数据,要做规则统一的累积处理”,优先使用 REDUCE,不要为了炫技而手写递归。原因有两点:
一是 REDUCE 不需要名称管理器,公式整体内联,复制到其他工作簿不容易丢失。 二是 REDUCE 的迭代流程由引擎控制,通常不会因为栈空间不足而失败。
递归留给那些真正体现“自然分层”的问题:树形结构、数字拆解、分治算法。
9.3 先写终止条件,再写递归体
编写 LAMBDA 递归时,我建议的顺序是:
- 先确定基础情况是什么,写成 IF 的第一个分支。
- 再写递归调用,并确认参数一步一步接近基础情况。
- 最后补充边界值,比如负数、0、空字符串。
顺序颠倒很容易导致“递归体写得漂亮,但终止条件漏了”,这是最常见也最可怕的错误。
9.4 分批测试,永远从小数据开始
递归公式没法下断点,所以测试策略特别重要。建议准备一个专门的测试区域,输入几组有代表性的数据:
=MYFACT(0) =MYFACT(1) =MYFACT(5) =NumToStr(9) =NumToStr(2024) =DigitSum(9) =DigitSum(999)如果这些数据全部正确,再尝试大规模数据。永远不要把一个需要几十层递归的大数一把丢进公式,除非你已经确认当前环境的深度上限。
9.5 避免易变函数与隐藏依赖
LAMBDA 递归公式里尽量不要使用RAND()、NOW()、TODAY()这类易变函数。递归过程本身计算量就大,如果在每一层递归里都调用一次易变函数,整个工作簿的性能会急剧下降,而且公式结果不稳定,难以排查。
9.6 做好工作簿文档化
名称管理器里放一堆函数,时间久了谁都记不住参数含义。建议在工作簿里专门建一个“函数说明”工作表,记录每个名称的功能、参数、返回结果、注意事项:
FN_NUM_TO_STR(n) 功能:把非负整数转换为字符串 参数:n 为非负整数 示例:=FN_NUM_TO_STR(20240607) -> "20240607" 注意:负数请使用 FN_NUM_TO_STR2这一步看似简单,但对长期维护和工作簿交接帮助极大。
10. 总结:谁是盟主?下一步学什么
回到最初的问题:Reduce 与 Lambda 递归,谁是盟主?
我的结论是:如果把“递归”看作一种能力,盟主只能是被命名后的 LAMBDA。因为只有它能让你实现真正的自引用,也只有它能在公式世界里模拟“分而治之”的思维。REDUCE 是一个非常优秀的循环折叠工具,适合处理确定数组上的批量重复任务,但它并不具备真正的递归建模能力。
不过更准确的判断是:递归的真正命门不是某个函数,而是终止条件。没有终止条件,LAMBDA 递归只会耗尽栈空间;有了终止条件并保证收敛,你才能写出可靠的递归函数。循环深度则决定这个递归能否在现实工作簿里落地。
所以你在项目中的选择应该是:
- 要折叠一个数组、累积一个结果:用 REDUCE。
- 要按数学定义处理分层问题:用名称管理器里的 LAMBDA 递归。
- 无论选哪种,先设计终止条件,再设计主体逻辑。
下一步,你可以尝试把文章里的几个例子改造成自己的需求。比如:把FN_NUM_TO_STR改成“金额转中文大写”的递归版,把DigitSum改成判断某个数字能否被 3 整除的递归版;再进一步,可以研究“递归二路归并排序”,思考如何用 Excel 数组公式配合 LAMBDA 实现分治排序的流程。
LAMBDA 和 REDUCE 给 Excel/WPS 带来的不只是两个新函数,而是一整套函数式编程的思维方式。当你开始习惯用“终止条件 + 递归调用 + 累积器”去思考公式时,就会发现很多过去只能靠 VBA 的问题,现在真的能用纯公式跑通了。