tao-of-rust-codes icon indicating copy to clipboard operation
tao-of-rust-codes copied to clipboard

6.2.3 闭包和所有权。能不能总结一个正交的规则

Open frostRed opened this issue 5 years ago • 5 comments

环境变量语义 环境变量操作 有无move 结果是哪种闭包 环境变量被怎么处理
复制语义 读环境变量 有 move Fn 闭包 &env
复制语义 读环境变量 无 move Fn闭包 &env
复制语义 修改环境变量 有 move
复制语义 修改环境变量 无 move
移动语义 读环境变量 有 move
移动语义 读环境变量 无 move
移动语义 修改环境变量 有 move FnMut 闭包 转移所有权
移动语义 修改环境变量 无 move FnMut 闭包 &mut env


现在书的内容看完之后有点懵,反而把我以前的理解搞乱了。

比如这段代码

fn main() {
    let mut s = 1;
    let c = || {
        println!("{:?}", s);
        println!("{:p}", &s);
    };
    c();
    c();
    let d = &mut s;
    println!("{:p}", d);
    println!("{:?}", s);
    println!("{:p}", &s);
}

按 179 页第二段说,闭包会以「不可变借用捕获了环境中的自由变量」,那不应该还能在闭包外面使用 &mut 的。

希望作者能帮我解惑。

frostRed avatar Mar 09 '19 14:03 frostRed

@frostRed 感谢反馈,后续在勘误表里尝试总结一下。不过也希望你可以自己总结一下,分享出来。

ZhangHanDong avatar Mar 09 '19 15:03 ZhangHanDong

在 frostRed 的表格上进行了补全,大家看看没有错误

环境变量语义 环境变量操作 有无 move 闭包 Trait 环境变量被怎么处理 闭包能否Copy
复制语义 读环境变量 有 move Fn copy
复制语义 读环境变量 无 move Fn immutable borrow
复制语义 修改环境变量 有 move FnMut copy
复制语义 修改环境变量 无 move FnMut mutable borrow ×
移动语义 读环境变量 有 move Fn move ×
移动语义 读环境变量 无 move Fn immutable borrow
移动语义 修改环境变量 有 move FnMut move ×
移动语义 修改环境变量 无 move FnMut mutable borrow ×
移动语义 消耗环境变量 有 move FnOnce move ×
移动语义 消耗环境变量 无 move FnOnce move ×

闭包 Trait

  1. 没有捕获环境变量以及只是不可变借用环境变量(捕获不可变引用的值)的闭包自动实现 Fn Trait
  2. 可变借用环境变量的闭包自动实现 FnMut Trait
  3. 消耗环境变量的闭包自动实现 FnOnce Trait
  4. 因为 FnOnce 是 FnMut 的父 Trait,FnMut又是Fn的父Trait,所以实现某个Trait时也会自动实现其父 Trait

move 作用

强行让闭包获取捕获的环境变量的所有权,这可能改变闭包本身的性质。下面例子就是,如果没有 move,闭包捕获的是一个 Copy 语意的不可变引用,所以闭包本身也会自动实现 Copy。在闭包前加上 move 强行转移所有权,导致闭包不能自动实现 Copy。

fn main() {
    let s = "hello".to_owned();
    let f = move || {
        println!("{}", s);
    };

    f(); 
    // foo(f);  // 因为 string move 到闭包环境里,让闭包不能自动实现 Copy
}

fn foo<F: Fn() + Copy>(f: F) {
    f()
}

同样的道理,用 move 可以让本来不能 Copy 的闭包自动实现 Copy (可变引用没有实现 Copy)

fn main() {
    let mut x = 0;
    let mut f = move || {
        x += 1;
        println!("{}", x);
    };
    
    f();
    foo(f);
    f();
    foo(f);
    
    println!("{}", x);
}

fn foo<F: FnMut() + Copy>(mut f: F) {
    f()
}

运行结果,作为函数参数传递的闭包每次复制新的闭包环境:

Standard Error
   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.85s
     Running `target/debug/playground`
Standard Output
1
2
2
3
0

闭包能否自动实现 Copy

取决于捕获的变量类型,如果捕获的是不可变引用、 move 的值本身实现了 Copy,闭包就能自动实现 Copy。

yim7 avatar Apr 14 '19 00:04 yim7

@yim7 辛苦了,已加精。

ZhangHanDong avatar Apr 14 '19 03:04 ZhangHanDong

复制语义 | 读环境变量 | 无 move | Fn | copy | √

这里环境变量的处理方式不是copy, 而是immutable borrow.

fn main() {
    let mut a = 1;
    let print = || {&a;};
    let aa = &mut a;
    print();
}
error[E0502]: cannot borrow `a` as mutable because it is also borrowed as immutable
 --> main.rs:4:14
  |
3 |     let print = || {&a;};
  |                 --   - first borrow occurs due to use of `a` in closure
  |                 |
  |                 immutable borrow occurs here
4 |     let aa = &mut a;
  |              ^^^^^^ mutable borrow occurs here
5 |     print();
  |     ----- immutable borrow later used here

error: aborting due to previous error

闭包的类型与闭包中使用环境变量的方式, 以及环境变量是否Copy有关; 闭包中环境变量的捕获方式, 与闭包中环境变量的使用方式, 以及是否有move有关; 至于是复制还是移动, 则与环境变量类型本身的语义有关.

可以确认一下是不是这样.

loxp avatar Aug 16 '19 06:08 loxp

感谢纠正疏漏之处@loxp

yim7 avatar Aug 16 '19 07:08 yim7