# Rust中的函数是变成语言功能:迭代器和闭包 函数式编程风格通常将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等等。 Rust中被认为是函数式语言类似的特新: (**性能超乎想象**) - 闭包(closures),一个可用存储在变量里的类似的函数的结构 - 迭代器(iterators),一个处理元素序列的方式 ## 闭包可用捕获环境的匿名函数 Rust中的闭包是可以保存进变量或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获调用者作用域中的值,可以复用代码和自定义行为。 ### [使用闭包创建行为的抽象](../examples/closures_practise.rs) ### 闭包类型推断和标注 闭包不要像`fn`函数那样在参数和返回值上标注类型。函数中需要类型标注是因为他们是暴露给用户的显示接口的一部分。严格的定义接口对于保证所有人都认同的函数使用和返回值的类型来说是很重要的。但是闭包并不用于这样暴露在外的接口:他们存储在变量里并被使用,不需要命名他们或暴露给库的用户调用。 闭包通常很短,并只关联于小范围的上下文而非任意情境。在这些有限制的上下文中,编译器能可靠的推断参数和返回值的类型,类似于它能推断大部分变量的类型一样。 ### 使用带有泛型和`Fn trait`的闭包 惰性求值(memoization/lazy evaluation): 可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。 为了让结构体存放闭包,我们需要指定闭包的类型,因为结构体定义需要知道其每一个字段的类型。每一个闭包实例有其自己独有的匿名类型:也就是说,即便两个闭包有着相同的签名,他们的类型任然可以被认为是不同的。为了定义使用闭包的结构体、枚举或者函数参数,需要像第10章讨论的那样使用泛型和`trait bound`。 `Fn`系列`trait`由标准库提供。所有的闭包都实现了trait:`Fn`、`FnMut`或`FnOnce`中的一个。 > 注意:函数也都实现了这三个`Fn`trait。如果不需要捕获环境中的值,则可以使用实现了`Fn`trait的函数而不是闭包。 为了满足`Fn`trait bound 我们增加了代表闭包所必须的参数和返回值类型的类型。 ### Cacher 实现的限制 值缓存是一种更加广泛的实用行为。 ### [闭包会捕获其环境](../examples/closures_capture_practise.rs) 闭包除了作为内联匿名函数来使用外,还有一个另一个函数所没有的功能:可以捕获其环境并访问其被定义的作用域的变量。 当闭包从环境中捕获一个值,闭包会在闭包体中储存这个值以供使用。 **这会使用内存并产生额外的开销**。在更一般的场景中,当我们不需要闭包来捕获环境时,我们不希望产生这些开销。因为函数从未允许捕获环境,定义和使用函数也就从不会有这些额外开销。 闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式: - 获取所有权 - 可变借用 - 不可变借用 这三种捕获值的方式被编码为如下三个 Fn trait: - FnOnce 消费从周围作用域捕获的变量,闭包周围的作用域被称为其 环境,environment。为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的 Once 部分代表了闭包不能多次获取相同变量的所有权的事实,所以它只能被调用一次 - FnMut 获取可变的借用值所以可以改变其环境 - Fn 从其环境获取不可变的借用值 当创建一个闭包时,Rust 根据其如何使用环境中变量来推断我们希望如何引用环境。由于所有闭包都可以被调用至少一次,所以**所有闭包都实现了`FnOnce`**。那些**并没有移动被捕获变量的所有权到闭包内的闭包也实现了`FnMut`** ,而**不需要对被捕获的变量进行可变访问的闭包则也实现了`Fn`**。 如果你希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用`move`关键字。这个技巧在将闭包传递给新线程以便将数据移动到新线程中时最为实用。 ## [使用迭代器处理元素序列](../examples/iterators_practise_1.rs) 迭代器模式允许你对一个元素序列的项进行某些处理。迭代器(iterator)负责遍历序列中的每一项和决定序列何时结束的逻辑。 在Rust中,迭代器是惰性的(lazy),这意味着在调用方法使用迭代器之前它都不会有效果。 在标准库中没有提供迭代器的语言中,需要使用一个从0开始索引的变量,使用这个索引变量索引序列中的值,并循环增加其值直到到达序列的元素数量。 迭代器为我们处理了所有这些逻辑,这减少了重复代码并消除了潜在的混乱。另外,迭代器的实现方式提供了对多种不同序列使用相同逻辑的灵活性。 ## Iterator trait 和 next 方法 迭代器都实现了一个叫做`Iterator`的定义于标准库的trait。这个trait的定义看起来像这样: ```rust pub trait Iterator { type Item; fn next(&mut self) -> Option; // 此处省略了方法的默认实现 } ``` 这里的`type Item`和`Self::Item`是新语法,定义了trait的**关联类型**(associate type)。`Item`类型将是迭代器返回的元素类型。 `next`是`Iterator`实现者被要求定义的唯一方法。`next`一次返回迭代器中的一个项,封装在`Some`中,当迭代器结束时,它返回`None`。 在迭代器上调用`next`方法改变了迭代器中用来记录序列位置的状态,可以这样说:代码**消费**(consuem)或使用了迭代器。每一个`next`调用都会从迭代器中消费一个项。 使用`for`循环时无需使迭代器可变,是因为`for`循环会获取迭代器的所有权并在后台生成使迭代器可变。 ## 性能对比:循环 VS 迭代器 **目的:为了得出一个怎样比较这两种实现的方式性能的思路**。 迭代器,作为一个高级的抽象,被编译成了与手写的底层代码大体一致的代码。迭代器使`Rust`的**零成本抽象**(zero-cost abstraction)之一,它意味着抽象并不会引入运行时开销,与`C++`中所定义的**零开销**(zero-overhead)如出一辙。 ```Rust let buffer: &mut [i32]; let coefficients: [i64; 12]; let qlp_shift: i16; for i in 12..buffer.len() { let prediction = coefficients .iter() .zip(&buffer[i - 12..i]) .map(|(&c, &s)| c * s as i64) .sum::() >> qlp_shift; let delta = buffer[i]; buffer[i] = delta + prediction as i32; } ``` Rust会`展开(unroll)`循环,以移除循环控制代码的开销。 请放心大胆的使用迭代器与闭包,他们使代码看起来更高级,但并不为此引入运行时性能损失。 ## 总结 闭包和迭代器是Rust受函数式编程语言观念所启发的功能。他们对Rust以底层的性能来明确的表达高级概念的能力有很大的贡献。闭包和迭代器的实现达到了不影响运行时性能的程度。这正是Rust竭力提供零成本抽象的目标的一部分。