RUST Functional
Rust 程序设计语言 - Rust 程序设计语言 简体中文版 (kaisery.github.io)
Functional Programming
函数作为一个对象,可以作为参数,返回值,给变量赋值然后执行
Closure
闭包是一个匿名函数,他可以被存储在一个变量或作为另一个函数的参数。可以在一个地方定义闭包,然后再其他地方执行他,与函数不同的是,闭包在执行时可以获取他定义时所在上下文的值。
由于闭包没有名字且一般都是在很小的上下文范围内使用,编译器一般可以推断出闭包的参数类型和返回值类型
基本写法
和普通函数写法类似,使用||传递参数,之后用大括号里面为函数体,当只有一句时,可以省略大括号。
如果一个闭包没有被使用,编译器无法推断出其数据类型,这个时候就不能省略其参数类型和返回值类型。编译器只会给闭包的参数和返回值推断一种数据类型,不能像模板一样支持多个类型。
1 | let add_one_1 = | x: u32| -> u32 { x + 1 }; |
闭包使用外部值
在闭包中使用外部值分为三种情况(和函数参数相同):
- 作为不可变引用immutable reference
- 作为可变引用 mutable reference
- 获取所有权 taking ownership,在
||前使用move
编译器会根据场景使用最小的使用权。
1 | use std::thread; |
Fn Traits
闭包体内如何对外部引用使用方法决定了闭包实现类哪种类型的Fn trait,而函数或结构体可以指定自己使用哪种Fn trait类型的闭包。闭包会自动实现三种类型的Fn trait,这三种类型从严格到宽松。
FnOnce这种闭包只能被执行一次。所有的闭包都实现了这个trait。当一个闭包把一个获取的引用移出了闭包体,这个闭包只能是FnOnce。FnMut这种闭包不会把引用值移出闭包体,但是会修改引用的值。这种闭包可以被调用多次。Fn这种闭包不会改变引用值,就像没有从外部获取值一样。这种闭包可以被调用多次,即使在多线程时调用也不影响。
当然普通的函数也可以实现以上三种Fn traits。
Option<T>的unwrap_or_else方法声明了它会使用FnOnce的闭包,当impl<T>的值为None时,它会调用传入的闭包f一次,这个闭包返回的类型为T。
1 | impl<T> Option<T> { |
例如以下例子中,list的get返回一个Option<&Rectangle>,如果值为None时,使用闭包输出不存在,并返回一个新的Rectangle对象。
1 | let mut list = [ |
对list排序的sort_by_key方法,就使用FnMut类型的闭包,因为这个闭包里面把list的一个元素作为参数传入,返回一个可以用作排序的值K。例如使用长方形的款作为排序的key,其中闭包获取一个元素r作为入参,返回r的宽度作为排序比较的key值,虽然这个方法使用的闭包不会修改任何值,但是他需要这个闭包可以被多次执行以遍历list中的所有元素,所以它使用的闭包类型定义为FnMut.
1 |
|
对于只实现了FnTrait的闭包sort_by_key就不能使用。闭包体中的sort_operations.push(value)从外部获取value的所有权,并将所有权又传出去给了外部变量sort_operations,导致下一次执行这个闭包时,已经无法获取到value的所有权了。而num_sort_operations变量只是可变引用,可以被多次执行。
1 |
|
返回闭包
闭包可以看做是一种trait,所以不能直接返回它,因为trait的大小是未知的。但是可以通过trait object方式返回闭包。即给闭包增加一个指针。
1 | fn returns_closure() -> Box<dyn Fn(i32) -> i32> { |
函数指针
函数也可以作为参数传递给另一个函数。fn类型称作函数指针。使用函数指针可以复用已经实现过的函数。
例如已经有了一个实现整数加1的函数,我们想实现整数加一操作执行多次,就可以在新的函数中调用已经实现的函数。
1 | fn add_one(x: i32) -> i32 { |
函数指针实现了三种类型的闭包,所以可以使用闭包的地方,都可以使用函数指针。
1 | let list_of_numbers = vec![1, 2, 3]; |
枚举的每个变量名也是一个初始化函数,所以这个变量名也是函数指针。
1 | enum Status { |
迭代器Iterator
迭代器模式可以对一系列数据元素逐个访问。迭代器对象是懒加载的,只有消费了迭代器,它才会执行遍历。
迭代器Trait
Iterator trait有一个next()方法,它返回一个Option<Self::Item>类型对象,其中的Item是这个迭代器的关联类型。当迭代器遍历完所有元素后,next返回None.
1 | pub trait Iterator { |
一般定义的迭代器类型都是mut类型因为执行next方法会修改迭代器对象内的索引。
- 使用
iter()获取到原始列表的v1不可变引用 - 使用
iter_mut()获取到原始列表的v1可变引用 - 使用
into_iter()获取到原始列表v1的所有权
消费迭代器
Iterator trait中定义了一些方法调用next方法称为consuming adaptors,因为他们通过next遍历每一个元素从而用尽迭代器。例如sum()方法就遍历所有元素累加各个元素的和,同时它会获取迭代器的所有权。
1 | fn iterator_sum() { |
生产迭代器
有些 Iterator trait的方法可以以迭代器作为输入并产生变化后的迭代器,这些方法称作Iterator adaptors 。例如map()会对迭代器的每一个元素执行指定的闭包操作,并返回一个新的迭代器。由于这个新的迭代器是懒加载,所以需要执行collect()使其转换为一个vector。
1 | let v1: Vec<i32> = vec![1, 2, 3]; |
使用闭包和迭代器
filter 方法使用一个闭包作为参数,遍历每一个元素过程中,当闭包返回true时,就把这个元素加新生成的迭代器中,如果返回false,就丢掉。
1 |
|
迭代器性能
使用迭代器虽然看似高层次的抽象,但是rust编译器最终会对代码优化,不会带来额外的运行成本,甚至可能比直接手写for循环效率高。迭代器是rust中零成本zero-cost抽象的一个特性。
Bjarne Stroustrup, the original designer and implementor of C++, defines zero-overhead in “Foundations of C++” (2012):
In general, C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.
书中举了一个音频编码的例子,
1 | let buffer: &mut [i32]; |
对于coefficients遍历,rust知道其中有12个元素,为了减少循环控制代码性能损耗,rust会生成12个重复的代码来优化这个循环。
Rust knows that there are 12 iterations, so it “unrolls” the loop. Unrolling is an optimization that removes the overhead of the loop controlling code and instead generates repetitive code for each iteration of the loop.