Lambda 与代码块参数
本章节的 Lambda 指 Move 里一种匿名代码片段的写法:|参数 …| 表达式 或带类型的 |x: T| -> R { … }。它不是可以随便传来传去的「函数值」——在 Move 中,Lambda 只能作为 macro fun 调用的实参出现;编译器在宏展开时把它嵌进宏体,再按普通 Move 规则做类型检查与借用检查。
什么是 Lambda(在本章语境下)
若你熟悉 Rust、JavaScript 等语言里的闭包,可以把 Move 的 Lambda 想成:一段尚未单独命名、专门交给某个宏去「按需嵌入」的代码。它与普通 fun 的差别在于:
普通 fun | Lambda | |
|---|---|---|
| 能否单独声明、到处传递 | 可以(public fun 等) | 不能作为独立值类型传递 |
| 典型用途 | 模块 API、可复用逻辑 | 仅作宏参数,由宏展开成具体语句 |
| 是否有「函数指针」 | 无(Move 无通用函数类型) | 无;展开后就是普通代码块 |
因此:Lambda = 宏专用的匿名代码参数,不是语言里与 u64、vector<T> 并列的「Lambda 类型」。
Lambda 的类型形式(对宏可见)
宏定义里若需要「接收一段代码」,会把形参声明成 Lambda 类型,写法为:
|T1, T2, ...| -> R
含义是:接受若干个参数,类型依次为 T1、T2…,整体这段代码的结果类型为 R。
- 省略返回类型时,默认为
()(与「只产生副作用、不返回值」的块一致)。 - 参数类型、返回类型都必须写清楚(或由宏实现推断上下文);与 §11.4 展开与求值 一起读,更易理解「宏为什么需要这种类型」。
示例(类型层面的直觉,不必死记名字):
|u64, u64| -> u128 // 两个 u64,结果为 u128
|&u64| // 一个不可变引用,结果为 ()
|&mut u64| -> () // 可变引用,常用于原地修改
标准库里 vector::fold! 的累加器一侧、do_ref! 的元素类型等,都会在展开后对应到这类签名。
Lambda 的定义形式
1. 省略参数类型(由上下文推断)
在能推断的前提下,可以只写参数名:
|x| x + 1
|a, b| a + b
|_| 0u64 // 常用:占位「我不关心这个参数」
2. 显式标注参数类型与返回类型
需要时写成:
|x: u64| -> u64 { x + 1 }
|acc: u64, e: u64| -> u64 { acc + e }
花括号 { … } 表示块体:内部可以有多条语句,最后一式或显式构造的值为返回(与 R 一致)。
3. 单表达式体(无花括号)
|x| 2 * x
等价于「单表达式、返回类型可推断」的简写。
4. 与 & / &mut 配合
遍历时常出现引用形参,例如「对每个元素的引用做只读访问」:
vec.do_ref!(|e| use_read_only(*e));
vec.do_mut!(|e| *e = *e + 1);
这里 e 的类型由宏展开后的签名决定(如 &T 或 &mut T),写错借用方式时,报错会落在展开后的代码上,见 §11.8。
使用方式:只能作为宏实参
合法:宏调用里传入 Lambda:
let v = vector[1u64, 2, 3];
let s = v.fold!(0u64, |acc, x| acc + x);
option::some(42u64).do!(|n| assert!(n == 42));
不合法:把 Lambda 赋给变量、放进 struct 字段、当作普通函数参数传递——这些在 Move 中没有对应的类型与语义。
// 不能编译 — Move 没有「Lambda 类型」的值
// let f = |x| x + 1;
记住:|...| 只出现在 xxx!( ... ) 的括号里。
捕获外围变量
Lambda 体内部可以使用当前作用域里已经存在的变量(常称为「捕获」)。展开后,这些名字就是普通 Move 代码里的变量,因此必须满足:
- 所有权:不能捕获后违反移动/二次使用规则。
- 借用:若 Lambda 展开为多次使用某引用,须符合借用检查器要求。
示例(用 fold! 在 Lambda 里使用外层变量 k):
let k = 10u64;
let v = vector[1u64, 2, 3];
let sum = v.fold!(0u64, |acc, x| acc + x * k);
若某次展开导致「同一值被移动两次」或「可变与不可变借用冲突」,错误信息与 §11.4 里讲的「片段代入」一致:本质是展开后的普通 Move 代码不合法。
实例
例 1:fold! 累加
let v = vector[1u64, 2, 3, 4, 5];
let sum = v.fold!(0u64, |acc, x| acc + x);
// sum == 15
- 第一个 Lambda 参数:累加器
acc(类型与初值0u64一致)。 - 第二个:当前元素
x。
例 2:do_ref! 只读遍历
let mut total = 0u64;
let v = vector[10u64, 20, 30];
v.do_ref!(|e| total = total + *e);
例 3:tabulate! 用下标生成向量
let indices = vector::tabulate!(5, |i| i);
// vector[0,1,2,3,4]
例 4:option::do! 仅在 some 时执行
let opt = option::some(100u64);
opt.do!(|n| assert!(n > 0));
例 5:占位参数 |_|
重复执行某操作、但不需要元素或下标时:
3u8.do!(|_| do_something());
限制与常见误区
-
出现位置
只能写在name!(...)的实参里。任何试图「保存 Lambda、返回 Lambda」的写法都不符合 Move 设计。 -
类型必须与宏期望一致
宏内部对 Lambda 形参有固定签名(如fold!的两个参数)。实参 Lambda 的参数个数、类型、返回类型必须匹配,否则在展开后类型检查失败。 -
不是延迟到链上再「解释执行」
Lambda 与宏一起在编译期展开,链上只有普通字节码,没有「Lambda 对象」。 -
捕获与多次求值
若宏展开后同一表达式被复制多份(取决于具体宏实现),可能带来「重复移动」或「重复执行副作用」类问题;以具体宏文档与编译报错为准,必要时先展开成手写循环对照(见 §11.8)。 -
与
fun的取舍
需要命名、复用、跨模块调用的逻辑,应写成普通fun,在 Lambda 里调用该函数即可,而不是把业务全堆在 Lambda 里。
小结
| 要点 | 内容 |
|---|---|
| 本质 | 仅用于宏实参的匿名代码片段,不是独立函数类型 |
| 类型写法 | |T1, T2, …| -> R,默认 R 为 () |
| 定义 | 可省略类型、可用 |_| 占位、可有 |x: T| -> R { … } 块体 |
| 捕获 | 遵循展开后的所有权与借用规则 |
| 限制 | 不能脱离宏单独存在;不能与「高阶函数值」类比过度 |
写完 Lambda 后务必 sui move build:类型与借用错误会以展开后的代码为基准报错,熟悉这一点后排错会更快。更多标准库宏与 Lambda 的搭配见 §11.6、§11.7。