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
的引用, 然后在使用时 *
解引用, 又因为 v
是 Copy
, 相当于无事发生
换句话说如果 Rust 的闭包在捕获环境时没有手动指定 move
, 那么编译器会尝试惰性的 Copy
如果不了解这个行为, 会让你撞上奇怪的 "短命" 还有引用问题, 并且编译器不会提示加 move
续命, 比如 这样
和 这样
总之非常反直觉
更新:
相关 issues: #54060