本文转载自微信公众号《董泽润的技术笔记》,作者董泽润。转载本文请联系董泽润技术笔记公众号。同时,闭包引用变量也有优先级:先是只读借用,然后是可变借用,最后转移所有权。在本文中,让我们看看如何使用闭包作为参数或返回值Go闭包调用packagemainimport"fmt"functest(ffunc()){f()f()}funcmain(){a:=1fn:=func(){a++fmt.Printf("ais%d\n",a)}test(fn)}上面就是go的闭包调用。我们使用fn作为参数并将其传递给函数测试。闭包捕获变量a,做自增操作,函数fn可以多次调用。对于熟悉go的人来说,这是很自然的,但是rust有一个问题fnmain(){lets=String::from("wocao");letf=||{println!("{}",s);};f();}比如上面的rust代码,如果我想用闭包f作为参数,应该怎么写呢?上周我们分享了闭包知道,闭包是匿名的c=hello_cargo::main::closure-2(0x7fffffffe0e0,0x7fffffffe0e4)b=hello_cargo::main::closure-1(0x7fffffffe0e0)a=hello_cargo::main::closure-0在运行的同时,和上面的结构类似,闭包结构命名规则是closure-xxx。同时,我们不知道函数签名。Trait官方文档给出了解决方案。标准库提供了几个内置的特征。闭包必须实现Fn、FnMut、FnOnce之一,然后我们可以调用闭包$catsrc/main.rsfntest(f:T)whereT:Fn(){f();}fnmain(){lets=String::from("董泽润的技术笔记");letf=||{println!("{}",s);};test(f);}$cargorunFinisheddev[unoptimized+debuginfo]target(s)in0.00sRunning`target/debug/hello_cargo`上面董泽润的技术笔记把闭包f以泛型参数的形式传给了函数test,所以为闭包实现了Fn特性。刚接触这个领域的人可能会感到困惑。其实类比gointerface他们都能理解,但本质还是不一样的。letf=||{s.push_str("好");};如果测试语句Unchanged,我们的闭包修改捕获的变量怎么办?||letf=||{s.push_str("Notbad");};|^^-closureis`FnMut`因为它在这里mutatesthevariable`s`|||这个闭包实现了`FnMut`,而不是`Fn`|test(f);报错闭包实现的trait是FnMut,不是Fnfntest(mutf:T)whereT:FnMut(){f();}fnmain(){letmuts=String::from("董泽润的技术笔记");letf=||{s.push_str("Notbad");};test(f);}以上就是变量借用的场景,我们来看看moveownership的情况fntest(f:T)whereT:FnOnce(){f();}fnmain(){lets=String::from("董泽润的技术笔记");letf=||{let_=s;};test(f);}以上,我们将自由变量s的所有权移到了闭包中。这时候T的genericfeature变成了FnOnce,也就是只能执行一次。如果测试两次调用闭包怎么办?1|fntest(f:T)where|-move发生是因为`f`hastype`T`没有实现`Copy`trait...4|f();|---`f`movedduetothiscall5|f();|^valueusedhereaftermove|注意:这个值实现了`FnOnce`,调用时它会被移动-->src/main.rs:4:5|4|f();编译器提示第一次调用时已经移动了,再次调用时无法访问。很明显此时free变量已经被析构了let_=s;它在离开词法作用域后被释放。为了内存安全,Rust当然不允许进一步访问fntest(f:T)whereT:Fn(){f();f();}fnmain(){lets=String::from("董泽润的技术notes");letf=move||{println!("sis{}",s);};test(f);//println!("{}",s);}那么,上面的代码示例是否可以跑步?当然,此时变量s的所有权移到了闭包f中,生命周期和闭包一样,重复调用没有副作用。深入理解,本质上Rust为了内存安全引入了这么麻烦的处理。平时写go程序的时候,谁关心对象什么时候释放,有没有读写冲突?必须有人这样做。Rust选择在编译期间进行检查。FnOnce使用它从其封闭范围(称为闭包环境)捕获的变量。要使用捕获的变量,闭包必须取得这些变量的所有权,并在定义时将它们移入闭包中。名称中的Once部分表示闭包不能多次获得相同变量的所有权,因此它只能被调用一次。FnMut可以改变环境,因为它可变地借用值。Fn不变地从环境中借用值。以上来自官网的解释,Fn表示不可变的借用闭包,可以重复执行,FnMut表示闭包的变量引用修改变量,可以重复执行。FnOnce表示所有权转移,只能执行一次。如果再次执行,free变量将超出作用域并被回收}pubtraitFnMut:FnOnce{extern"rust-call"fncall_mut(&mutself,args:Args)->Self::Output;}pubtraitFnOnce<Args>{typeOutput;extern"rust-call"fncall_once(self,args:Args)->Self::Output;}#}以上是标准库中Fn、FnMut、FnOnce的实现。可以看到Fn继承自FnMut,FnMut继承自FnOnceFn(u32)->u32以上例子都是无参数的,其实也可以带参数。既然Fn是继承自FnMut,那我们能否将实现Fn的闭包传递给FnMut的泛型呢?$catsrc/main.rsfntest(mutf:T)whereT:FnMut(){f();f();}fnmain(){lets=String::from("董泽润的技术笔记");letf=||{println!("sis{}",s);};test(f);}$cargorunCompilinghello_cargov0.1.0(/Users/zerun.dong/code/rusttest/hello_cargo)Finisheddev[unoptimized+debuginfo]target(s)in1.47sRunning`target/debug/hello_cargo`sis董泽润的技术笔记sis董泽润的技术笔记当然可以貌似可以,FnMut告诉函数测试这是一个会修改变量的闭包,所以传入的闭包不会进行修改当然没问题。上图比较出名。由于继承关系,Fn的实现可以用于FnMut和FnOnce的参数,FnMut的实现可以用于FnOnce的参数。函数指针fncall(f:fn()){//functionpointerf();}fnmain(){leta=1;letf=||println!("abc");//anonymousfunctionletc=||println!("{}",&a);//closurecall(f);call(c);}函数和闭包不同,上面例子中的f是匿名函数,而c指的是自由变量,所以是闭包。这段代码无法执行9|letc=||println!("{}",&a);//closure|--------------------thefoundclosure...12|call(c);|^expectedfnpointer,foundclosure编译器告诉我们第12行要求参数是函数指针,不应该是闭包Closure作为返回值引用implTrait轻松返回复杂类型,implTraitis指定一个新的、未命名的方法,它具有实现特定特征的具体类型。可以放在两个地方:参数位置和返回位置fnreturns_closure()->Boxi32>{Box::new(|x|x+1)}fnmain(){letf=returns_closure();println!("resis{}",f(11));}过去,从函数返回闭包的唯一方法是使用特征对象。可以试试报错信息fnreturns_closurewithoutBoxpacking()->implFn(i32)->i32{|x|x+1}fnmain(){letf=returns_closure();println!("resis{}",f(11));}现在我们可以使用impl来实现闭包的返回值声明fntest()->implFnMut(char){letmuts=String::from("董泽润的技术笔记");|c|{s.push(c);}}fnmain(){letmutc=test();c('d');c('e');}让我们看一个与引用生命周期相关的例子。上面的代码返回闭包c,并恢复字符串s。代码执行肯定报错:-->src/main.rs:3:5|3||c|{s.push(c);}|^^^-`s`isborrowedhere|||mayoutliveborrowedvalue`s`|注意:closuresreturnedhere-->src/main.rs:1:14|1|fntest()->implFnMut(char){|^^^^^^^^^^^^^^^^^帮助:toforcetheclosuretotakeownershipof`s(andanyotherreferencedvariables),使用`move`关键字|3|move|c|{s.push(c);}|^^^^^^^^很明显变量s被释放的时候超出范围,编译器也提醒我们将所有权移至闭包。有兴趣的可以自己修改测试。