如何理解Rust闭包的虫洞穿梭

发布时间:2021-10-28 11:45:02 作者:iii
来源:亿速云 阅读:139

这篇文章主要介绍“如何理解Rust闭包的虫洞穿梭”,在日常操作中,相信很多人在如何理解Rust闭包的虫洞穿梭问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”如何理解Rust闭包的虫洞穿梭”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

1. 闭包是什么

闭包(Closure)的概念由来已久。无论哪种语言,闭包的概念都被以下几个特征共同约束:

神奇的是最后一点,理解起来也比较别扭的,习惯就好了。

为了说明上述特征,可以看一个Rust例子。

fn display<T>(age: u32, print_info: T)     where T: Fn(u32) {    print_info(age);}fn main() {     let name = String::from("Ethan");     let print_info_closure = |age|{         println!("name is {}", name);         println!("age is {}", age);     };    let age = 18;     display(age, print_info_closure);}

运行代码:

name is Ethan  age is 18

首先,闭包作为匿名函数存在了print_info_closure栈变量中,然后传递给了函数display作为参数,在display内部调用了闭包,并传递了参数age。最后神奇的事情出现了:在函数display中调用的闭包居然打印出了函数main作用域中的变量name。

如何理解Rust闭包的虫洞穿梭

闭包的精髓,就在于它同时涉及两个作用域,就仿佛打开了一个"虫洞",让不同作用域的变量穿梭其中。

let x_closure = ||{};

单独一行代码,就藏着这个奥妙:

无论左侧右侧,都定义了闭包的属性,天然的联通了两个作用域。

对于闭包,Rust如此,其他语言也大抵如此。不过,Rust不是还有所有权、生命周期这一档子事儿么,所以还可以深入分析下。

2. Rust闭包捕获上下文的方式

Rust闭包如何捕获上下文?

换个问法,main作用域中的变量name是以何种方式进入闭包的作用域的(第1节例子)?转移or借用?

It Depends,视情况而定。

Rust在std中定义了3种trait:

后者能办到的,前者一定能办到。反之则不然。所以,编译器对闭包签名进行推理时:

第1节的例子,将display的泛型参数从Fn改成FnMut,也可以无警告通过。

fn display<T>(age: u32, mut print_info: T)     where T: FnMut(u32) {    print_info(age);}

对环境变量进行捕获的闭包,需要额外的空间支持才能将环境变量进行存储。

3. 作为参数的闭包签名

上面代码display函数定义,要接受一个闭包作为参数,揭示了如何显式的描述闭包的签名:在泛型参数上添加trait约束,比如T:  FnMut(u32),其中(u32)显式的表示了输入参数的类型。尽管是泛型参数约束,但是函数签名(除了没有函数名)描述还是非常精确的。

顺便说一句,Rust的泛型真的是干了不少事情,除了泛型该干的,还能添加trait约束,还能描述生命周期。

描述签名是一回事,但是谁来定义闭包的签名呢?闭包定义处,我们没有看到任何的类型约束,直接就可以调用。

答案是:闭包的签名,编译器全部一手包办了,它会将首次调用闭包传入参数和返回值的类型,绑定到闭包的签名。这就意味着,一旦闭包被调用过一次后,再次调用闭包时传入的参数类型,就必须是和第一次相同。

传入参数和返回值类型绑定好了,但你心中难免还会有一丝忧愁:描述生命周期的泛型参数肿么办?

Rust编译器也搞得定。

fn main(){         let lifttime_closure = |a, b|{         println!("{}", a);         println!("{}", b);         b    };    let a = String::from("abc");     let c;     {        let b = String::from("xyz");         c = lifttime_closure(&a, &b);    }    println!("{}", c); }

以上代码无法通过编译,成功检测出了悬垂引用:

error[E0597]: b does not live long enough

显然,对于闭包,编译期可以对引用的生命周期进行检查,以保证引用始终有效。

这个例子,与其解释闭包与函数的区别,不如解释匿名函数与具名函数的区别:

4. 函数返回闭包

第1节的例子,我们将一个闭包作为函数参数传入,那么根据闭包的特性,它应该能够作为函数的返回值。答案是肯定的。

基于前面介绍的Fn trait,我们定义一个返回闭包的函数,代码如下:

fn closure_return() -> Fn() -> (){     ||{}    }

可是,编译失败了:

error[E0746]: return type cannot have an unboxed trait object  doesn't have a size known at compile-time

失败信息显示,编译器无法确定函数返回值的大小。一个闭包有多大呢?并不重要。

开门见山,通用的解决方法是:为了能够返回闭包,可以使用一次装箱,从而将栈内存变量装箱存入堆内存,这样无论闭包有多大,函数返回值都是一个确定大小的指针。下面的代码里,使用Box::new即可完成装箱。

fn closure_inside() -> Box<dyn FnMut() -> ()> {    let mut age = 1;     let mut name = String::from("Ethan");     let age_closure = move || {         name.push_str(" Yuan");         age += 1;         println!("name is {}", name);         println!("age is {}", age);     };    Box::new(age_closure) }fn main(){     let mut age_closure = closure_inside();     age_closure();    age_closure();}

运行结果如下:

上面的代码,除了让函数成功返回闭包之外,还有一个目的,我们想让闭包捕获函数内部环境中的值,但这次有些不同:

内层函数调用完成后就会销毁内层环境变量,那如何做到呢?幸好,Rust有所有权转移。只要能促成内层函数的环境变量向闭包进行所有权的转移,这个操作顺理成章。

正因为Rust具有所有权转移的概念,返回闭包(同时捕获环境变量)的机理,Rust的要比任何具有垃圾回收语言(JavaScript、Java、C#)的解释都更简单明了。后者总会给人一丝不安:内部函数调用都结束了,居然局部变量还活着。

代码中的所有权转移,这里使用了关键字move,它可以在构建闭包时,强制将要捕获变量的所有权转移至闭包内部的特别存储区。需要注意的是,使用move,并不影响闭包的trait,本例中可以看到闭包是FnMut,而不是FnOnce。

到此,关于“如何理解Rust闭包的虫洞穿梭”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

推荐阅读:
  1. 谈谈自己的理解:python中闭包,闭包的实质
  2. 如何理解和应用闭包

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

上一篇:RedHat AS4.6 下如何安装Proftpd

下一篇:Mysql数据分组排名实现的示例分析

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》