当前位置: 首页 > 科技观察

Rust闭包的虫洞梭

时间:2023-03-12 04:19:53 科技观察

1。什么是闭包?闭包(Closure)的概念由来已久。不管是哪种语言,闭包的概念都受到以下特点的制约:匿名函数(非排他性,函数指针也可以);可以调用闭包并显式传递参数(非排他性,也可以使用函数指针);以变??量的形式存在,可以传递(不排斥,函数指针也可以);可以直接捕获并使用闭包中定义范围的值(独占);魔术是最后一点,理解它很别扭,但你习惯了。为了说明上述特性,让我们看一个Rust示例。fndisplay(age:u32,print_info:T)whereT:Fn(u32){print_info(age);}fnmain(){letname=String::from("Ethan");letprint_info_closure=|age|{println!("nameis{}",name);println!("ageis{}",age);};letage=18;display(age,print_info_closure);}运行代码:nameisEthanageis18首先闭包作为匿名函数print_info_closure存在在栈变量中,作为参数传递给函数display,在display内部调用闭包,传递参数age。最后,神奇的事情发生了:在函数display中调用的闭包实际上打印出了函数main范围内的变量名。闭包的本质是同时涉及到两个作用域,就像开了一个“虫洞”,让不同作用域的变量穿梭其中。letx_closure=||{};一行代码隐藏了这个谜团:赋值的左边=是存储闭包的变量,它在一个作用域内,也就是我们所说的定义闭包的环境上下文;赋值的右边=,在一对花括号{}中,也是一个作用域,在调用闭包的地方动态生成;左右两边都定义了闭包的属性,自然而然的把两个作用域联系起来了。对于闭包,Rust是这样,其他大部分语言也是这样。不过Rust不是还有所有权和生命周期这些东西吗,所以可以深入分析一下。2.Rust闭包如何捕获上下文Rust闭包如何捕获上下文?还有一个问题,main作用域中的变量名是如何进入闭包作用域的(第一节的例子)?转让还是借用?视情况而定。Rust在std中定义了三个traits:FnOnce:在闭包中对外部变量进行了传递操作,使得外部变量不可用(因此只能调用一次);FnMut:直接使用和修改闭包中的外部变量;Fn:外部变量,不加修改,直接在闭包中使用;后者能做到,前者也一定能做到。反之则不然。因此,当编译器推断闭包签名时:实现FnMut的也实现了FnOnce;实现Fn的那个也实现了FnMut和FnOnce。在第1节的示例中,将display的通用参数从Fn更改为FnMut也可以在没有警告的情况下通过。fndisplay(age:u32,mutprint_info:T)whereT:FnMut(u32){print_info(age);}捕获环境变量的闭包需要额外的空间支持来存储环境变量。3.闭包签名作为参数上面代码中显示函数的定义接受一个闭包作为参数,揭示了如何显式描述闭包的签名:给泛型参数添加trait约束,比如T:FnMut(u32),其中(u32)明确表示输入参数的类型。尽管有泛型参数限制,但函数签名(无函数名除外)描述非常精确。顺便说一下,R??ust的泛型确实可以做很多事情。除了泛型应该做的事情之外,它们还可以添加trait约束和描述生命周期。描述签名是一回事,但是谁来定义闭包的签名呢?在闭包定义处,我们看不到任何类型约束,我们可以直接调用它。答案是:闭包的签名全部由编译器处理。它将第一次调用闭包的参数和返回值的类型绑定到闭包的签名上。这意味着一旦闭包被调用一次,再次调用闭包时传入的参数类型必须与第一次相同。传入参数和返回值类型是绑定的,但是你心里难免会有一点担心:那些描述生命周期的泛型参数呢?Rust编译器也可以处理它。fnmain(){letlifttime_closure=|a,b|{println!("{}",a);println!("{}",b);b};leta=String::from("abc");letc;{letb=String::from("xyz");c=lifttime_closure(&a,&b);}println!("{}",c);}上面代码编译失败,成功检测到悬挂引用:error[E0597]:bdoesnotlivelongenough显然,对于闭包,可以在编译时检查引用的生命周期,以确保引用始终有效。在这个例子中,与其解释闭包和函数的区别,不如解释匿名函数和命名函数的区别:命名函数的签名在前。对于编译器、调用者和函数内部实现,只要分别遵守签名约定即可。能。推断匿名函数的签名。编译器要看透调用者的实际输入和函数内部的实际返回,所以检查自然是顺便做了。4.函数返回一个闭包在第1节的例子中,我们传递了一个闭包作为函数参数,所以根据闭包的特性,它应该可以作为函数的返回值。答案是肯定的。基于前面介绍的Fntrait,我们定义一个返回闭包的函数,代码如下:fnclosure_return()->Fn()->(){||{}}但是编译失败:error[E0746]:returntypecannothaveanunboxedtraitobjectdoesn'thaveasizeknownatcompile-time失败信息表明编译器无法确定函数返回值的大小。闭包有多大?没关系。开门见山,一般的解决方法是:为了能够返回闭包,可以使用一个装箱,让栈内存变量装箱存储在堆内存中,这样不管闭包有多大,函数的返回值是一个一定大小的指针。在下面的代码中,可以使用Box::new来完成装箱。fnclosure_inside()->Box()>{letmutage=1;letmutname=String::from("Ethan");letage_closure=move||{name.push_str("元");age+=1;println!("nameis{}",name);println!("ageis{}",age);};Box::new(age_closure)}fnmain(){letmutage_closure=closure_inside();age_closure();age_closure();}运行结果如下:nameisEthanYuanageis2nameisEthanYuanYuanageis3上面的代码,除了让函数成功返回闭包外,还有一个目的,我们希望闭包能够捕获函数内部环境中的值,但这次有点不同:在第1节的代码示例中,我们通过闭包将外部环境上下文传递到内部函数中。这不难理解,因为外层变量的生命周期更长,当访问内层函数时,外层变量还活着;本节代码所做的是将内部函数的环境变量通过闭包传递给外部环境;内部函数调用完成后会销毁内部环境变量,那又如何呢?幸运的是,Rust有所有权转移。只要能促进内部函数的环境变量所有权转移到闭包,这个操作是合乎逻辑的。因为Rust有所有权转移的概念,返回闭包的机制(同时捕获环境变量),所以Rust的解释比任何带有垃圾收集的语言(JavaScript、Java、C#)都简单明了。后者总会给人一点不安:内部函数调用都结束了,局部变量还活着。代码中的所有权转移,这里使用了关键字move,可以在构造闭包时强制将要捕获的变量的所有权转移到闭包内部的一个特殊存储区域。需要注意的是,使用move不会影响闭包的特性。在这个例子中,我们可以看到闭包是FnMut,而不是FnOnce。