本文主要是在阅读《Rust编程之道》的闭包章节后,对知识点做的相关梳理。目前接触Rust还不久,感觉《Rust编程之道》相对官方教程而言写的更深更细。但看完该书闭包这一章节后,感觉有必要梳理一下,以加深理解。
fn main(){
let mut s="rush".to_string();
{
let mut c=||{s+=" rust"};
c();
c();
println!("{:?}",s);
}
println!("{:?}",s);
}
这个示例是书中6-38的代码。书中提到:1,第1个println那一行会报错;2,第2个println能正确运行,是因为s的所有权被归还。
这个示例代码很短。来验证一下,在Rust的playground中执行
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018
结果发现:
1,能正常执行,并没有报错。即书中关于第1点的阐述不成立。
2,去掉那一对大括号(如下代码),也不会报错。即书中关于第2点的阐述恐难成立。
fn main(){
let mut s="rush".to_string();
let mut c=||{s+=" rust"};
c();
c();
println!("{:?}",s);
println!("{:?}",s);
}
所以书中关于这一示例引申出的解释和结论,需要重新推敲一下。
示例6-33 移动语义类型自动实现FnOnce
fn main(){
let s = "hello".to_string();
let c = || s;
c();//"hello"
//c();//use of moved value: `c`
//println!("{:?}",s);//borrow of moved value: `s`
}
示例6-36 环境变量为移动语义类型时使用move关键字
fn main(){
let s = "hello".to_string();
let c = move || {println!("{:?}",s)};
c();//"hello"
c();//"hello"
//println!("{:?}",s);//borrow of moved value: `s`
}
单独看这两个例子,运行符合预期,似乎没毛病。然则仔细一想,此两个示例有点问题。哪里有问题?这两个示例中闭包的写法不同。先这两个示例的闭包写法“对调”一下。
示例6-33 的改写版
fn main(){
let s = "hello".to_string();
let c = || {println!("{:?}",s)};
c();//"hello"
c();//"hello"
println!("{:?}",s);//"hello"
}
示例6-36 的改写版
fn main(){
let s = "hello".to_string();
let c = move || s;
c();//"hello"
//c();//use of moved value: `c`
//println!("{:?}",s);//borrow of moved value: `s`
}
看出奥秘了吗?那就是,影响闭包函数实现哪个trait,除了与这3个因素:环境变量的语义(复制语义或移动语义)、环境变量是否在闭包中是否修改、是否有move关键字相关,还与闭包中对所捕获的环境变量的”用法“相关。(这里之所以”用法“打引号,是因为到写此文时,自己还没彻底理解,这些用法最终会影响闭包实现trait的根因。)。
上面的两个改写版,在维持以上3因素不变的前提下,仅仅是改写了闭包内部的代码(对环境变量的使用,也就是第4个因素),就得到截然不同的结果。所以,书中关于这两个示例所延申出来的解释和结论,感觉不太可靠。
比如,书中6-36 示例据此推出来的结论:“……可以进行多次调用。同理,该闭包实现的一定是Fn”。实际上,按照上面6-36的改写版,是无法得出这样的结论的。
从这个现象看,似乎闭包中对环境变量的用法不同,会导致其所有权可能转移,也可能不转移。再看下面的测试代码:
fn main(){
println!("ownership of String is not moved by closure.");
let s1 = "hello".to_string();
println!("{:?},{:p}",s1,&s1);//"hello",0x7fff5803b578
let c = || {println!("{:?},{:p}",s1,&s1);};
c();//"hello",0x7fff5803b578
c();//"hello",0x7fff5803b578
println!("{:?},{:p}",s1,&s1);//"hello",0x7fff5803b578 注意地址均相同。
println!("ownership of String is moved by closure.");
let s2 = "hello".to_string();
println!("{:?},{:p}",s2,&s2);//"hello",0x7ffe3c53efd0
let c = || {println!("{:?},{:p}",s2,&s2);s2;};//注意这里多了一个s2;
c();//"hello",0x7ffe3c53f070,注意地址不同。
//c();//use of moved value: `c`
//println!("{:?},{:p}",s2,&s2);//borrow of moved value: `s2`
}
从中可以发现,虽然s1是移动语义,且被闭包捕获,但是从实际表现看,并没有发生所有权的转移(这点和书中总结的“对于移动语义类型,执行移动语义,转移所有权来进行捕获”,就有点对不上了)。s1的引用地址在闭包调用前后都没有发生变化。
如果在闭包中再添加一点代码(这里添加:s2;),就会导致s2的所有权被移动:s2的引用地址也发生了变化;闭包不能多次调用;s2在闭包调用后也不能再使用。
根据这种现象,暂且先猜测如下推论:
1,闭包若捕获移动语义的环境变量,且该变量在闭包中未修改,(暂且不考虑move关键字,下面测试再考虑),那么闭包中存在一些对此变量的用法,这些用法使得该环境变量的所有权,并不会发生转移。这种情况占少数。如下示例:
let c = || {println!("{:?}",s)};
2,除了上述的用法外,其他大部分的用法,均能使得此变量的所有权发生转移。如下示例:
let c = || {s};
let c = || {s;};
let c = || {println!("{:?}",s);s};
let c = || {println!("{:?}",s);s;};
let c = || {let s1=s;};
注:
1,要解释这种现象,可以猜测一些可能性。例如,编译器对闭包是不是存在一些特殊的处理规则。这个疑点先留下来。后续弄明白后再更新此文。
正是因为上述的一些疑问或问题,决定自己动手,写点测试代码,试着摸索总结一下闭包的这两个规律:闭包实现哪个trait;是否转移环境变量的所有权;如何影响环境变量。
影响闭包行为的因素:有无环境变量,环境变量的语义(复制还是移动),闭包中有无修改环境变量,有无move关键字,闭包中对环境变量的用法(为便于描述,姑且称之特殊情况和非特殊情况。测试代码中的“特殊情况”仅有println!,“非特殊情况"则除了println!之外还加一点其他代码)。
闭包表现出来的行为:实现了哪个trait(Fn/FnMut/FnOnce)、环境变量是否转移所有权、如何影响环境变量。
测试结果:
有无 环境变量 |
环境变量的语义(复制还是移动) |
环境变量在闭包中有无修改 | 有无使用move关键字 | 闭包中对环境变量的用法 | 闭包实现的是哪个trait | 环境变量的所有权是否转移 | 环境变量的捕获方式 |
没有 | / | / | / | / | Fn | / | / |
有 | 复制语义 | 不修改 | 无 | 仅有println! | Fn | 不转移 | &T |
不只有println! | Fn | 不转移 | &T |
||||
有 | 仅有println! | Fn | 不转移 | &T |
|||
不只有println! | Fn | 不转移 | &T |
||||
修改 | 无 | /(既然有修改,肯定不会只有println!) |
FnMut | 不转移 | &mut T (原变量会受影响) |
||
有 | FnMut | 不转移 | copy后的新变量 (原变量不受影响) |
||||
引用语义 | 不修改 | 无 | 仅有println! | Fn | 转移 | / | |
不只有println! | FnOnce | 转移 | / | ||||
有 | 仅有println! | Fn | 转移 | / | |||
不只有println! | FnOnce | 转移 | / | ||||
修改 | 无 | /(既然有修改,肯定不会只有println!) | FnMut | 不转移 | &mut T (原变量会受影响) |
||
有 | FnMut | 转移 | mut T(个人猜测) |
观察测试结果,简化一下:
有无 环境变量 |
环境变量的语义(复制还是移动) |
环境变量在闭包中有无修改 | 有无使用move关键字 | 闭包中对环境变量的用法 | 闭包实现的是哪个trait | 环境变量的所有权是否转移 | 环境变量的捕获方式 |
没有 | / | / | / | / | Fn | / | / |
有 | 复制语义 | 不修改 | /(不影响) | /(不影响) | Fn | 不转移 | &T |
修改 | 无 | / |
FnMut | 不转移 | &mut T (原变量会受影响) |
||
有 | copy后的新变量 (原变量不受影响) |
||||||
移动语义 | 不修改 | 无 | 仅有println! | Fn | 转移 | / | |
不只有println! | FnOnce | ||||||
有 | 仅有println! | Fn | |||||
不只有println! | FnOnce | ||||||
修改 | 无 | / | FnMut | 不转移 | &mut T (原变量会受影响) |
||
有 | 转移 | / |
用文字概括一下:
1,对于复制语义的环境变量:
2,对于移动语义的环境变量:
判断闭包实现是哪个trait(Fn,FnMut,Fnonce)的规则
· 如果闭包中没有捕获任何环境变量,则默认自动实现Fn 。
· 如果闭包中捕获了复制语义类型的环境变量,则:
➢ 如果不需要修改环境变量,无论是否使用move关键字,均会自动实现Fn。
➢ 如果需要修改环境变量,则自动实现FnMut。
· 如果闭包中捕获了移动语义类型的环境变量,则:
➢ 如果不需要修改环境变量,且没有使用move关键字,则自动实现FnOnce。
➢ 如果不需要修改环境变量,且使用了move关键字,则自动实现Fn。
➢ 如果需要修改环境变量,则自动实现FnMut。
和书中规则对照,除下划线的两行外,其他是一致的。
书中规则认为:不修改+有move->Fn。反证举例:示例代码中的条件9和条件10,符合”不修改+有move“的情况,但并非二者都实现Fn,而是分别实现了Fn和FnOnce。同理,书中规则认为:”不修改+无move"->FnOnce。反证举例:示例代码中的7和8。
从验证代码看,在移动语义+不修改时,是实现Fn还是FnOnce,跟move没关系。看起来跟闭包内使用变量的方式有关。
闭包自动实现Copy/Clone的规则
·使用 move 关键字,如果捕获的变量是复制语义类型的,则闭包会自动实现Copy/Clone,否则不会自动实现Copy/Clone
此为书中规则,和验证结果一致。
闭包捕获环境变量的规则
对不同环境变量类型介绍过闭包捕获其环境变量的方式:
· 对于复制语义类型,以不可变引用(&T)来进行捕获。
· 对于移动语义类型,执行移动语义,转移所有权来进行捕获。
· 对于可变绑定,并且在闭包中包含对其进行修改的操作,则以可变引用(&mut T)来进行捕获。
这段文字在书中第2章和第6章分别出现一次。当时在第2章初次看到时,感觉有点纳闷:对于可变绑定的复制语义类型,是遵循第1条还是第3条规则?对于可变绑定的移动语义类型,是遵循第2条还是第3条规则?
结合验证结果,以自己的理解,解读一下上述规则。
注:
1,因个人碰rust的时间不长。若理解若有偏差,还请留言批评指正。
测试代码如下
#![feature(fn_traits)]
fn main(){
println!("条件1:环境变量为复制语义+闭包内无修改+无move+闭包中仅有println!使用环境变量");
let v1=1i32;
let c1= || {println!("{:?}",v1);};
c1.call(());//1
//c1.call_mut(()); //cannot borrow `c1` as mutable, as it is not declared as mutable
c1.call_once(());//1
println!("{:?}",v1);//1
println!("条件2:环境变量为复制语义+闭包内无修改+无move+闭包中不只有println!使用环境变量");
let v2=1i32;
let c2= || {println!("{:?}",v2);v2};
c2.call(());//1
//c2.call_mut(()); //cannot borrow `c2` as mutable, as it is not declared as mutable
c2.call_once(());//1
println!("{:?}",v2);//1
println!("条件3:环境变量为复制语义+闭包内无修改+有move+闭包中仅有println!使用环境变量");
let v3=1i32;
let c3= move ||{println!("{:?}",v3);};
c3.call(());//1
//c3.call_mut(());//cannot borrow `c3` as mutable, as it is not declared as mutable
c3.call_once(());//1
println!("{:?}",v3);//1
println!("条件4:环境变量为复制语义+闭包内无修改+有move+闭包中不只有println!使用环境变量");
let v4=1i32;
let c4= move ||{println!("{:?}",v4);v4};
c4.call(());//1
//c4.call_mut(());//cannot borrow `c4` as mutable, as it is not declared as mutable
c4.call_once(());//1
println!("{:?}",v4);//1
println!("条件5:环境变量为复制语义+闭包内有修改+无move");
let mut v5=1i32;
let mut c5= || {v5+=1;println!("{:?}",v5);};
//c5.call(()); //expected a closure that implements the `Fn` trait, but this closure only implements `FnMut`
c5.call_mut(()); //2
c5.call_once(()); //3
println!("{:?}",v5);//3
println!("条件6:环境变量为复制语义+闭包内有修改+有move");
let mut v6=1i32;
let mut c6= move ||{v6+=1;println!("{:?}",v6);};
//c6.call(());//expected a closure that implements the `Fn` trait, but this closure only implements `FnMut`
c6.call_mut(());//2
c6.call_once(());//3
println!("{:?}",v6);//1
println!("条件7:环境变量为移动语义+闭包内无修改+无move+闭包中仅有println!使用环境变量");
let v7="a".to_string();
let c7= || {println!("{:?}",v7);};
c7.call(()); //"a"
//c7.call_mut(()); //expected a closure that implements the `FnMut` trait, but this closure only implements `FnOnce`
c7.call_once(()); //"a"
//println!("{:?}",v7);//borrow of moved value: `v7`
println!("条件8:环境变量为移动语义+闭包内无修改+无move+闭包中不只有println!使用环境变量");
let v8="a".to_string();
let c8= || {println!("{:?}",v8);v8};
//c8.call(()); //expected a closure that implements the `FnMut` trait, but this closure only implements `FnOnce`
//c8.call_mut(()); //expected a closure that implements the `FnMut` trait, but this closure only implements `FnOnce`
c8.call_once(()); //"a"
//println!("{:?}",v8);//borrow of moved value: `v8`
println!("条件9:环境变量为移动语义+闭包内无修改+有move+闭包中仅有println!使用环境变量");
let v9="a".to_string();
let c9= move || {println!("{:?}",v9);};
c9.call(()); //"a"
//c9.call_mut(()); //cannot borrow `c9` as mutable, as it is not declared as mutable
c9.call_once(()); //"a"
//println!("{:?}",v9);//borrow of moved value: `v9`
println!("条件10:环境变量为移动语义+闭包内无修改+有move+闭包中不只有println!使用环境变量");
let v10="a".to_string();
let c10= move || {println!("{:?}",v10);v10};
//c10.call(()); //expected a closure that implements the `FnMut` trait, but this closure only implements `FnOnce`
//c10.call_mut(()); //expected a closure that implements the `FnMut` trait, but this closure only implements `FnOnce`
c10.call_once(()); //"a"
//println!("{:?}",v10);//borrow of moved value: `v10`
println!("条件11:环境变量为移动语义+闭包内有修改+无move");
let mut v11="a".to_string();
let mut c11= || {v11+="a";println!("{:?}",v11);};
//c11.call(()); //expected a closure that implements the `Fn` trait, but this closure only implements `FnMut`
c11.call_mut(()); //"aa"
c11.call_once(()); //"aaa"
println!("{:?}",v11);//"aaa"
println!("条件12:环境变量为移动语义+闭包内有修改+有move");
let mut v12="a".to_string();
let mut c12= move || {v12+="a";println!("{:?}",v12);};
//c12.call(()); //expected a closure that implements the `Fn` trait, but this closure only implements `FnMut`
c12.call_mut(()); //"aa"
c12.call_once(()); //"aaa"
//println!("{:?}",v12);//borrow of moved value: `v12`
}