iovxw

Rust 闭包环境捕获行为与 Copy trait

历史遗留问题

今天 Rust 群里有人遇到了反直觉的关于闭包的引用错误, 在群友合力排除各种干扰找到原因之后

我们就又有了一个可以塞到 100 黑 里的坑 (悲)

不过因为篇幅太长 (太罗嗦), 这里独立拆分出一篇文章讲解


闭包在捕获环境时, 如不显式添加 move 关键字, 捕获的方法是由编译器推导的

规则为: 如果捕获的变量在闭包内被引用, 或者可变引用, 那么就捕获到引用或可变引用

let v = String::new();
let c = || v.len(); // v: &String

let v = String::new();
let c = || v.clear(); // v: &mut String

可以在 MIR 里查看闭包捕获的类型, 这里我就直接在注释里写出来了

然后如果捕获的变量在闭包内被消耗, move 走的话, 就捕获所有权

let v = String::new();
let c = || std::mem::drop(v); // v: String

到这还是非常符合直觉的 —— 除非你捕获的变量实现了 Copy

let v = false;
let c = || std::mem::drop(v); // v: &bool

按理来说, 上面这个闭包的行为应该和给闭包加 move 一样, 因为我们把 v 消耗掉了

然而并不, c 捕获到的 v 的类型是 &bool, 是个引用

std::mem::drop 接受的参数也是 &bool 吗?

也不是, 仍然是 bool, 因为编译器把上面的例子展开后差不多是这样的

let v = false;
let ref_v = &v;
let c = || std::mem::drop(*ref_v);

闭包只捕获了 v 的引用, 然后在使用时 * 解引用, 又因为 vCopy, 相当于无事发生

换句话说如果 Rust 的闭包在捕获环境时没有手动指定 move, 那么编译器会尝试惰性的 Copy

如果不了解这个行为, 会让你撞上奇怪的 "短命" 还有引用问题, 并且编译器不会提示加 move 续命, 比如 这样这样

总之非常反直觉


更新:

相关 issues: #54060