Rust 学习笔记 - 基础4

213 阅读6分钟

闭包

闭包(closures)就是可以捕获其所在环境的匿名函数。

闭包的特点

  • 是匿名函数
  • 保存为变量、作为参数
  • 可在一个地方创建闭包,然后在另一个上下文中调用闭包完成运算
  • 可从其定义的作用域捕获值

闭包的类型推断

  • 不要求标注参数和返回值的类型(也可以标注,不过一般省略)
  • 通常很短小,只在狭小的上下文中工作,编译器通常能推断出类型
  • 闭包的定义最终只会为参数/返回值推断出唯一具体的类型
// 在未使用的时候可能会报错,因为没有指定类型,但是使用之后就不会报错了
let expensive_closure = |num| -> {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};
// 返回值比较简单也可以写成下面这种形式
let example_closure = |x| x;

let s = example_closure(String::from("hello"));

// 此处会报错,因为定义 s 的时候已经确定了闭包的参数和返回类型,不能修了
let n = example_closure(5);

使用泛型参数和 Fn Trait 存储闭包

使用 struct,它持有闭包及其调用结果。struct 的定义需要知道所有字段的类型,而且每一个闭包实例都有自己唯一的匿名函数,即使两个闭包签名完全一样。所以需要使用 泛型和 Trait Bound。

Fn Traits 由标准库提供,所有的闭包都至少实现了以下 trait 之一:

  • Fn
  • FnMut
  • FnOnce
struct Cacher<T>
    where T: Fn(u32) -> u32, // Fn Trait
{
    calculation: T, // 闭包
    value: Option<u32>,
}

impl<T> Cacher<T> 
where
    T: Fn(u32) -> u32,
{
    fn new(calculation: T) -> Cacher<T> {
        Cacher {
            calculation,
            value: None,
        }
    }

    fn value(&mut self, arg: u32) -> u32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v);
                v
            }
        }
    }
}

使用闭包来捕获环境

闭包能访问定义它的作用域内的变量,而普通函数则不能。

fn main() {
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}

如果使用下面的函数写法则会报错

fn main() {
    let x = 4;

    fn equal_to_x(z: i32) -> bool {
        z == x // can't capture dynamic environment in a fn item
    }

    let y = 4;

    assert!(equal_to_x(y));
}

不过闭包捕获他们所在环境的变量会产生内存开销。

闭包从所在环境捕获值的方式(与函数获得参数的三种方式一样):

  1. 取得所有权:FnOnce
  2. 可变借用: FnMut
  3. 不可变借用:Fn

创建闭包时,通过闭包对环境值的使用,Rust 可以推断出具体使用哪个 trait:

  • 所有的闭包都实现了 FnOnce
  • 没有移动捕获变量的实现了 FnMut
  • 无需可变访问捕获变量的闭包实现了 Fn

move 关键字

在参数列表前使用 move 关键字,可以强制闭包取得它所使用的环境值的所有权。

当将闭包传递给新线程以移动数据使其归新线程所有时,此技术最有用。

fn main() {
    let x = vec![1, 2, 3];

    let equal_to_x = move |z| z == x;

    println!("can't use x here: {:?}", x); // borrow of moved value: `x`

    let y = vec![1, 2, 3];

    assert!(equal_to_x(y));
}

最佳实践

当指定 Fn trait bound 之一时,首先用 Fn,基于闭包体里的情况,如果需要 FnOnceFnMut,编译器会再告诉你。

迭代器

迭代器模式:对一系列项执行某些任务。迭代器负责遍历每个项,确定序列(遍历)何时完成。

Rust 的迭代器是惰性的,除非调用消费迭代器的方法,否则迭代器本身没有任何效果。

fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter(); // 产生一个迭代器,用于遍历 v1,到这一步它未使用所以没有任何效果

    for val in v1_iter {
        println!("Got: {}", val);
    }
}

所有的迭代器都实现了 Iterator traitIterator trait 定义域标准库,定义大致如下:

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // methods with default implementations elided
}

type ItemSelf::Item 定义了与该 trait 关联的类型。实现 Iterator trait 需要你定义一个 Item 类型,它用于 next 方法的返回类型(迭代器的返回类型)。

Iterator trait 仅要求实现 next 方法。next 方法返回迭代器中的一项,返回结果包裹在 Some 里。迭代结束,返回 None

fn main() {
    let v1 = vec![1, 2, 3];

    let mut v1_iter = v1.iter(); // for in 不需要要加 mut 因为在其内部已经取得了所有权

    println!("Got: {}", v1_iter.next() == Some(&1));
    println!("Got: {}", v1_iter.next() == Some(&2));
    println!("Got: {}", v1_iter.next() == Some(&3));
}

迭代方法

  • iter:在不可变引用上创建迭代器
  • into_iter:创建的迭代器会获得所有权
  • iter_mut:迭代可变的引用
fn main() {
    let v1 = vec![1, 2, 3];

    let mut v1_iter = v1.into_iter();

    println!("Got: {}", v1_iter.next() == Some(1));
    println!("Got: {}", v1_iter.next() == Some(2));
    println!("Got: {}", v1_iter.next() == Some(3));
}
fn main() {
    let mut v1 = vec![1, 2, 3];

    let mut v1_iter = v1.iter_mut();

    println!("Got: {}", v1_iter.next() == Some(&mut 1));
    println!("Got: {}", v1_iter.next() == Some(&mut 2));
    println!("Got: {}", v1_iter.next() == Some(&mut 3));
}

消耗迭代器的方法

在标准库中,Iterator trait 有一些带默认实现的方法,其中有一些方法会调用 next 方法,所以实现 Iterator trait 时必须实现 netx 方法的原因之一。

调用 next 的方法叫做消耗型适配器。因为调用 next 方法会把元素一个一个都吃掉,最终耗尽迭代器。

比如:sum 方法

fn main() {
    let v1 = vec![1, 2, 3];
    let v1_iter = v1.iter();

    let total: i32 = v1_iter.sum();

    println!("{}", total)
}

产生其它迭代器的方法

定义在 Iterator trait 上的另外一些方法叫做迭代器适配器。它会把当前迭代器转换为不同种类的迭代器。可以通过链式调用使用多个迭代器适配器来执行复杂的操作,这种调用可读性较高。

例如:map,接收一个闭包作为参数,闭包作用于每个元素,产生一个新的迭代器。

fn main() {
    let v1 = vec![1, 2, 3];
    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); // collect 也是消耗型适配器,把结果收集到一个集合类型中。

    println!("{:?}", v2); // [2, 3, 4]
}

使用闭包捕获环境

filter 方法,接收一个闭包,这个闭包在遍历迭代器的某个元素时,返回 bool 类型,如果闭包返回 true, 当前元素将会包含在 filter 产生的迭代器中,如果闭包返回 false,当前元素将不会包含在 filter 产生的迭代器中。

struct Shoe {
    size: u32,
    style: String,
}

fn shoe_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|x| x.size == shoe_size).collect()
}

fn main() {
    let shoes = vec![
        Shoe {
            size: 10,
            style: String::from("sneaker"),
        },
        Shoe {
            size: 13,
            style: String::from("sandal"),
        },
        Shoe {
            size: 10,
            style: String::from("boot"),
        },
    ];
    let result = shoe_in_my_size(shoes, 10);
    println!("{}", result.len())
}

创建自定义迭代器

使用 Iterator trait 创建自定义迭代器,关键的一步就是要提供一个 next 方法。

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
           None 
        }
    }
}

fn main() {
    let mut counter = Counter::new();

    println!("{}", counter.next() == Some(1));
    println!("{}", counter.next() == Some(2));
    println!("{}", counter.next() == Some(3));
    println!("{}", counter.next() == Some(4));
    println!("{}", counter.next() == Some(5));
    println!("{}", counter.next() == None);
}

性能对比:循环 VS 迭代器

在 Rust 中,循环和迭代器性能基本是一样的,因为在编译之后,生成了和手写的底层代码基本一样的产物。这一套东西在 Rust 里面就叫零开销抽象(Zero-Cost Abstraction),即使用抽象时不会引入额外的运动时开销。