前天我们用已有知识实现了一个minigrep
的匹配小工具,然后昨天我们优化了下这个小项目
今天该继续往下学了
Iterators
)和闭包(Closures
)rust
语言的设计是参考了很多语言和技术的,其中函数式编程(functional programming
)对rust
的意义是非常重大的。比如把函数作为参数/返回值传递等。
rust
中对闭包的定义是:一个匿名函数可以捕获它的环境。
有点拗口。
它可以被用于以下几个地方
在看例子之前,我们有一个疑问,那就是为什么我们非得用闭包才能捕获上下文中的数据?用函数不行吗?
函数有时候可以,但是有时候不行,我们来看个错误。
a
是一个动态的变量,而B
是一个常量会被hardcode
进binary
中。 从这里可以知道函数中并不能获取上下文中的动态变量,因为这会导致额外的开销,与其让函数变得复杂,还不如再抽一种类型出来。
这也就是为什么rust
中闭包是一个单独的概念了,而在js
中,闭包就是依赖于函数的。
ok
,我们来看下闭包的例子
我们准备免费赠送一批衣服,这堆衣服里有红蓝cp
两种颜色,如果用户指定了颜色 && 这个颜色是存在的 && 还有库存的,那么他就可以获得。反之没有指定就会默认选择销量最好的那一款。
我们在giveaway
方法中写了一个闭包,作为unwrap_or_else
的参数。
unwrap_or_else
这个方法我们之前用到过
可以看到如果有值就返回值,没有则执行传进去的闭包,然后将闭包的返回值作为返回值。
所以有一点需要注意就是你这个闭包的值得和Some
的值是一致的。
扯远了。
||
之间相当于()
,可以放参数,闭包体是{}
,只有单语句的时候可以不写。
闭包的规范是|xx| { ... }
。
找不到用户想要的颜色时,它就会去执行这个most_stocked
的method
。
其实说到底,就是类似js
中的callback
。
我们来看下闭包和函数的区别
closure
)和函数(function
)的区别毕竟fn
函数是面向用户/其它开发者的,所以需要说清楚传入传出是啥。
而闭包不需要,它的参数和返回值类型会被编译器推断出来(除了极少数部分需要声明),因为它和它所在的上下文是有关联的,大部分情况下会在该上下文中被执行,所以也就能推断出来。
第一个是一个fn
,接受一个u32
,然后返回+1
。
第二个到第四个都是闭包,都是正常的写法。
其中三和四需要在上下文中被使用,这样编译器才能推断出来它的类型。这和我们之前学习vector
的时候很像,我们可以直接let v = Vec::new()
,然后不定义它的类型,但是需要在上下文中将至少一个数据传入到这个vector
里。
capturing references
)或者移动所有权(moving ownership
)前面说过,闭包可以捕获它所在环境中的数据。
类比函数中的参数,它有以下三种方式来捕获:
rust
编译器会通过闭包体里是怎么对待被捕获的数据的来推断使用哪种方式来捕获。默认自然是不可变借用,然后可变借用再然后是拥有所有权。
来看个例子
我们写了一个only_borrows
的闭包,这个闭包引用了list
这个vector
,但是仅仅只是标准输出了这个list
,所以这个时候就没有必要可变引用或者获取所有权,直接改为不可变引用即可。
编译后会变成类似这样
顺便说下,调用也是和函数一样的直接xxx()
。
我们再来看个例子
这次我们直接往list
中push
了一个数字7
,这时不可变借用已经不能用了,所以这个时候就会使用可变引用的方式。
这里需要注意,在闭包被调用和定义之间,不能对可变引用的数据做引用或者迁移所有权处理,道理相信大家现在也都清楚了,事关内存安全。
然后我们再来看个例子
我们新开了一个线程,然后把闭包传入到里面去了。虽然还是标注输出list
,但是这个时候不用move
不行,为什么呢?新开的线程可能早于或者晚于main
主线程销毁,这个时候引用指向的内存也会被销毁。而我们的main
线程还在引用这个list
引用,这样就有问题了。依旧是一个内存安全问题。
关键字move
,放在闭包的参数声明的||
之前,表示引用的数据的所有权被move
到闭包里了。
Fn trait
前面说了,rust
捕获环境数据的时候有三种方式,而不同的方式以及如何处理数据会影响这个闭包实现不同的trait
。
通俗点的说就是对数据的不同捕获方式和对数据的处理方式会使这个闭包去implement
不同的trait
。
所以目前trait
我们已知可被impl
的有三种: struct、fn、closure
。
不过closure
不需要我们去手动impl
,rust
编译器会在编译的阶段帮我们去实现不同的trait
。
闭包能实现的trait
有以下三种
FnOnce
: 所有闭包默认都会实现的一个trait
,毕竟只要是闭包那就得能被调用。如果一个闭包只是把捕获的数据抛出去,那么这个闭包就只会实现这个trait
。和trait
的名字一样,这个闭包只会被调用一次,毕竟捕获的数据都抛出去了,没东西了自然就挂了。FnMut
: 和第一个相反,实现这个trait
的不会去抛出数据,它可能会改变这个捕获的数据。所以它可能被调用N
次。Fn
: 实现这个trait
的闭包不会去抛出捕获的数据,也不会改变捕获的数据,并且也有可能是捕获不到数据的。这种不会影响环境所以它们可以在同一时刻可以被调用很多次。这三个trait
是嵌套的,如果实现了Fn
这个trait
,那么就一定实现了FnMut
这个trait
,而实现了FnMut
,则一定实现了FnOnce
。
所以一个闭包可能实现了两个甚至这三个trait
。
另外,函数也是可以实现这三个trait
的。
我们来看下例子
我们给unwrap_or_else
方法传入的就是一个实现FnOnce
这个trait
的闭包
为什么呢?
并不是因为它返回了一个值,这个值不一定是捕获的值。另外这个闭包可能在里面对捕获的值做了改变,当然也有可能不改。所以对unwrap_or_else
这个方法来说,传入的闭包的可能性太多了,不能确定。能确定的就只有这个闭包一定实现了FnOnce
这个trait
。
如果我们在使用这个闭包的时候并不需要捕获数据,那么完全是可以用函数传入的。
来看个例子
这时我们的闭包没有捕获任何的环境数据,所以这个时候它实现的trait
包括FnOnce
和Fn
。
我们来改下代码
这个时候我们传入的就不是闭包了,而是一个method
,非常的简洁。
扯远了。
我们回到闭包会实现的三个trait
中。
再来看个例子
我们创建了一个数组,数据是一系列矩形。
矩形有宽高两个字段。
然后我们调用数组的sort_by_key
这个方法根据矩形的width
来从小到大排序。
可以看到数据按照width
来排序了。
我们传给sort_by_key
这个方法的数据是一个闭包,它接受一个参数是矩形的引用。 看情况它应该是一个impl
了FnOnce
的trait
,没有其他的了。
但实际上并不是,实际上它还实现了FnMut
这个trait
。
官方的理由是:这个闭包被调用了N
次。
就这一点它确实不能是FnOnce
,因为如果这里面发生捕获数据的抛出,那么它只能抛一次,遍历的第二次就没东西抛了。
比如下面这个例子
为什么会报错呢,就是因为这个闭包把它辛辛苦苦捕获到的数据全量扔给了sort_operations
这个人渣,然后它卷款跑路了。
第二次自然就炸了。
但是为什么不是Fn
这个trait
呢?
这个咱也不知道啊。
可能有在遍历过程中需要改动其它数据的场景吧。
比如这样
计算有几个矩形。。。。
至于Fn
这个trait
,对于闭包来说不太常用,而在函数中就很常见了。。
今天学的闭包,有三种捕获数据的方式以及三种可实现的trait
。
编辑于 2022-12-28 11:27・IP 属地广东