Rust学习笔记-5-基础篇:闭包

背景

本文主要是在阅读《Rust编程之道》的闭包章节后,对知识点做的相关梳理。目前接触Rust还不久,感觉《Rust编程之道》相对官方教程而言写的更深更细。但看完该书闭包这一章节后,感觉有必要梳理一下,以加深理解。

示例之疑1

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);
}

所以书中关于这一示例引申出的解释和结论,需要重新推敲一下。

示例之疑2

示例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,对于复制语义的环境变量:

  • 所有权不会转移到闭包。变量的捕获方式和对原变量的影响:无修改->&T(无影响)有修改+无move->&mut T(有影响)有修改+有move->copy(无影响)。(看来move对于复制语义,又不能真的移动,只好复制。)
  • 闭包实现哪个trait:无修改->Fn有修改->FnMut

2,对于移动语义的环境变量:

  • 所有权的转移、变量的捕获方式和对原变量的影响:无修改,或者有修改+有move->转移所有权(原变量的所有权已转移到闭包,闭包外已不能访问)有修改+无move->&mut T(有影响).  
  • 闭包实现哪个trait:有修改->FnMut无修改+非特殊情况->FnOnce无修改+特殊情况->Fn。这里的特殊情况,指的是类似仅调用println!的方法。

和书中的规则对照

判断闭包实现是哪个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条规则?

结合验证结果,以自己的理解,解读一下上述规则。

  • ·对于复制语义类型:无修改->&T有修改+无move->&mut T有修改+有move->copy
  • ·对于移动语义类型:无修改,或者有修改+有move->转移所有权有修改+无move->&mut T

注:

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`
}

 

 

你可能感兴趣的:(rust,rust)