迭代器与闭包
想要了解更多函数式语言的特点,可以去看sicp
闭包:能够捕获环境变量的匿名函数
闭包是一种可以存入变量或者作为参数传递给其它函数的匿名函数(在别的语言里可能是ambda表达式)
创建闭包:查看下面的例子,基本上是|参数| {函数体}
let f = |x:u32| -> u32 {x+1}; //创建闭包,不省略参数类型和返回值类型
let f = |x| {x+1}; //创建闭包,自动推断类型
let f = |x| x+1 ; //只含一个表达式的闭包可以省略大括号
fn f (x:u32) -> u32 {x+1} //函数定义
闭包的类型推断和类型标注
编译器可以根据上下文推断出闭包中参数的类型和返回的类型,但是注意:
闭包中的类型最终会被推定为唯一的具体类型,也就是说下面这样是不行的
let f = |x| x;
let s = f("a");
let a = f(1);
使用泛型参数和Fn trait存储闭包:以惰性求值为例
惰性化求值是一种思想,其将已经得到的值缓存起来,这样再请求时可以直接去缓存里寻找;只有新值需要求
可以这么实现:创建一个同时存放闭包和闭包返回值的结构体
- 需要获得值时运行背包,并将结果缓存到别的字段
- 请求旧值时,直接到缓存字段查找
为了将闭包存储在结构体中,我们使用一个实现了Fn trait的泛型来指示闭包的类型
Fn trait需要知道闭包的参数和返回类型,像是Fn(u32)->u32
给出一个例子:
struct Cacher<T>
where T: Fn(u32)->u32
{
cal:T,
value:Option<u32>,
}
impl<T> Cacher<T> where T:Fn(u32)->u32 {
fn new(cal:T) -> Cacher<T> {
Cacher { cal, value: None, }
}
fn value(&mut self,arg:u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.cal)(arg);
self.value = v;
v
},
}
}
}
改进
由于上面只会存储第一次调用的结果,让我们很难在不同的上下文中复用
- 解决方法是在结构体中存储一个map,以传入的arg为关键字,以返回值为键(就是将函数做成查找表)
第二个问题是拓展性差 - 解决方法是引入更多的泛型参数
可能看起来一个接受一个参数返回一个结果的Cacher是下面这样
struct Cacher<T,U,V>
where U:Copy+Eq+Hash,V:Copy,T: Fn(U)->V
{
cal:T,
value:HashMap<U,V>,
}
impl<T,U,V> Cacher<T,U,V> where U:Copy+Eq+Hash,V:Copy,T: Fn(U)->V {
fn new(cal:T) -> Cacher<T,U,V> {
Cacher { cal, value: HashMap::new(), }
}
fn value(&mut self,arg:U) -> V {
let v = (self.value).entry(arg).or_insert(
(self.cal)(arg)
);
*v
}
}
使用闭包捕获上下文环境
闭包可以捕获自己所在的环境并访问自己被定义时作用域中的变量(还记得c++中,捕获变量列表位于lambda表达式一开始的[]
内)
当闭包从环境中捕获值时,会使用额外的空间来储存这些值以在闭包体中使用
闭包可以通过三种方式从环境中捕获值,这三种方式被分别编码在如下所示的3种Fn系列的trait中
- FnOnce,意味着闭包可以从环境中获得变量的所有权
- FnMut,意味着闭包可以获得环境中变量的可变引用
- Fn 可以从环境中不可变的借用值
闭包的trait可以被自动推导出
- 所有闭包都实现了FnOnce
- 不需要move被捕获变量的闭包实现了FnMut
- 不需要修改被捕获变量的实现了Fn
- 不需要move被捕获变量的闭包实现了FnMut
如果希望闭包强制捕获所有权,在参数列表前加move关键字(该特性在多线程中尤其有用)
let x = vec![1,2,3];
let f = move |z| z==x;
在上面的例子中,x被强制移动到闭包内,即在闭包定义后面就不许使用x这个名称了
倘若上面的例子去掉move
关键字,则该闭包只会获得环境变量的不可变引用
使用迭代器处理元素序列
迭代器模式使得你可以依次为序列中的每一个元素执行某些任务
迭代器是惰性的,创建迭代器后,除非主动调用方法消耗并使用迭代器,否则不会产生任何实际效果
Iterator trait 和 next 方法
所有迭代器都实现了Iterator trait,该trait定义如下所示
pub trait Iterator {
type item;
fn next(&mut self) -> Option<Self::item>;
//以及一系列的默认方法,大部分是基于next方法的
}
Item
是关联类型,这里先不做深入讨论
该trait最重要的点在于实现next这个方法,在每次调用时返回一个包裹在Some中的迭代器元素,并在迭代结束时返回None
创建迭代器
对于大部分的容器类型
.iter()
生成不可变引用迭代器.into_iter()
取得所有权并返回元素本身的迭代器.iter_mut()
生成可变引用迭代器
自定义的迭代器类型
分为三步:创建一个类型,为该类型实现Iterator trai (指定item类型,定义next方法)
比如我们做一个倒计时迭代器
struct Counter {
count:u32
}
impl Counter {
fn new() -> Counter {
Counter {count:11}
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
self.count -= 1;
if self.count > 0 {
Some(self.count)
} else {
None
}
}
}
消耗迭代器的方法
所有调用next的方法称为消耗适配器,Iterator trait提供了很多这类的方法
let total = v.iter().sum();
生成其它迭代器的方法
将一个迭代器转化成另一个迭代器方法称为迭代器适配器
因为所有迭代器都是惰性的,你总是需要一个消耗适配器才能得到效果
- map :接受一个闭包作为参数,闭包的参数是迭代器返回的类型,遍历迭代器,将其元素经过闭包处理,并输出成迭代器
//将所有数字加1
let v2:Vec<_> = v1.iter().map(|x| x+1).collect();
- filter:接受一个返回布尔值的闭包作为参数,闭包的参数是迭代器返回的类型,遍历迭代器,保留闭包返回true的元素,输出成迭代器
//取出所有偶数
let v2:Vec<_> = v1.iter().filter(|x| x%2==0).collect();
使用迭代器重构I/O项目
主要是两点
- 在Config::new中直接接受std:env::args作为参数
pub fn new(mut args:std::env::Args) -> Result<Config,&'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("lack of query string."),
};
let filename = match args.next() {
Some(arg) => arg,
None => return Err("lack of file name."),
};
let case_sensitive = env::var("CASE_SENSITIVE").is_err();
Ok(Config { query, filename, case_sensitive})
}
- 使用链式的风格,替代循环
pub fn search_case_insensitive<'a>(query:&str,contents:&'a str) -> Vec<&'a str> {
let query = query.to_lowercase();
contents.lines()
.filter(|line| (line.to_lowercase()).contains(&query))
.collect()
}
重构后的项目在nanogrep
迭代器VS循环
迭代器是Rust中的零开销抽象,这意味着使用迭代器这个抽象时不会引入额外的运行时开销
大部分的时候,使用迭代器甚至要比使用循环要快速,这依赖于编译器对迭代器的优化