Rust 学习笔记(十)|闭包和迭代器

1 闭包

1.1 什么是闭包

闭包是可以捕获其所在环境的匿名函数。这个匿名函数可以保存为变量,作为参数或者作为返回值存在。可以在某一个地方创建闭包,在另一个地方使用闭包来完成运算。

使用下面的方式定义个使用一个闭包:

let example_closure = |x| x;    // 定义一个闭包,但是没有规定参数和返回值的类型,参数为x,返回值为x

let s = example_closure(String::from("hello"));    // 使用一次闭包,此时闭包的定义唯一确定不可更改

看下面这个程序的例子:程序中多次调用simulated_expensive_calculation函数,并且这个函数执行一次需要的时间较长,并且在一次执行中会多次调用。实际上我们可以利用闭包简化这个程序的写法,并且通过提前执行这个函数获得函数的执行结果来减少程序的运行时间。

use std::thread;
use std::time::Duration;

fn main() {
    generate_workout(1, 2);
}

fn simulated_expensive_calculation(intensity: u32) -> u32 {
    println!("Calculation slowly...");
    thread::sleep(Duration::from_secs(2)); // 延时两秒
    intensity
}

fn generate_workout(intensity: u32, random_number: u32) {
    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            simulated_expensive_calculation(intensity)
        );
        println!(
            "Next, do {} situps!",
            simulated_expensive_calculation(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a brake today!");
        } else {
            println!(
                "Today, run for {} minutes!",
                simulated_expensive_calculation(intensity)
            );
        }
    }
}

优化后的写法(使用闭包):

use std::thread;
use std::time::Duration;

fn main() {
    generate_workout(1, 2);
}

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num| {
        println!("Calculation slowly...");
        thread::sleep(Duration::from_secs(2)); // 延时两秒
        num
    };

    if intensity < 25 {
        let result = expensive_closure(intensity); // shadowing
        println!("Today, do {} pushups!", result);
        println!("Next, do {} situps!", result);
    } else {
        if random_number == 3 {
            println!("Take a brake today!");
        } else {
            println!("Today, run for {} minutes!", expensive_closure(intensity));
        }
    }
}

但是如果闭包仅仅用来用作简化,那么意义就不大了。我们希望当长时间函数未执行时,仅执行一次;如果函数已经被执行,则直接取返回值即可。可以看看下面的方法:

1.2 使用Fn trait和结构体存储闭包

要让struct持有闭包,struct的定义必须知道所有的字段类型,而每个闭包实力都有自己的匿名类型,即使两个闭包签名完全相同,所以需要使用泛型参数和Trait Bound。

所有的闭包都至少实现了以下几个Fn Trait之一:FnFnMutFnOnce。这里只使用了FnTrait。

use std::thread;
use std::time::Duration;

struct Cacher<T>
where
    T: Fn(u32) -> u32, // T 就是闭包的类型
{
    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() {
    generate_workout(1, 2);
}

fn generate_workout(intensity: u32, random_number: u32) {
    let mut expensive_closure = Cacher::new(|num| {
        println!("Calculation slowly...");
        thread::sleep(Duration::from_secs(2)); // 延时两秒
        num
    });

    if intensity < 25 {
        println!("Today, do {} pushups!", expensive_closure.value(intensity));
        println!("Next, do {} situps!", expensive_closure.value(intensity));
    } else {
        if random_number == 3 {
            println!("Take a brake today!");
        } else {
            println!("Today, run for {} minutes!", expensive_closure.value(intensity));
        }
    }
}

1.3 闭包捕获外部环境变量

其实大多数时候,闭包和函数并没有使用上的区别,闭包就是一个匿名的,可以由编译器自动推断参数和返回值类型的函数。区别在于闭包可以捕获与它同作用域的变量,但是函数却不可以(除非是全局变量)。但是闭包捕获外部变量需要一定的内存开销和运行时开销,在实际使用中也很少仅仅因为它可以捕获外部变量就使用闭包。

下面是一个闭包捕获环境变量的例子:

fn main() {
    let x = 4;
    
    let equal_to_x = |z| z == x;

    let y = 4;
    println!("{}", equal_to_x(y));      // true
}

闭包从所在环境获取值的方式和函数获取参数的方式类似,三种fn trait对应三种方式:

  • Fn:不可变借用
  • FnMut:可变借用
  • FnOnce:直接取得所有权

创建闭包时,通过对环境值的使用,Rust会自动推断此时是使用那个Trait。所有的闭包都实现了FnOnce,没有移动被捕获变量的实现了FnMut,无需改变被捕获变量的实现了Fn实际指定Trait Bound时,应该先指定Fn,根据编译器信息来更改是否需要改成FnMut或者FnOnce。这种方式并不是很优雅,因为程序的编译时间有可能会很长。

可以使用move关键字强制获取被捕获变量的所有权。这种技术在将闭包传递给新线程,以移动数据所有权归新线程所有时最有用:

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

    let equal_to_x = move |z| z == x;
    println!("Can't use x: {:?}", x);   // 报错

    let y = vec![1, 2, 3, 4];
    println!("{}", equal_to_x(y));      // true
}

2 迭代器

2.1 迭代器简介

迭代器是一种常见的编程模型,它负责遍历每个项,对每个项执行某些操作,并确定序列何时遍历完成。

Rust中的迭代器是懒惰的(lazy),即如果不使用消耗迭代器的方法,否则迭代器本身没有任何效果。Rust的迭代器是通过实现Iterator trait实现的。这个trait的定义大致如下。type itemself::item定义了与这个trait相关联的类型。实现iterator需要实现item类型,它用于定义next方法的返回类型。定义一个迭代器基本上代表着实现next方法即可。

pub trait iterator {
    type item;
    
    fn next(&mut self) -> Option<self::item>;

    // methods with default implementations elided
}

2.2 迭代器消耗方法

next方法就是一个消耗迭代器的方法,它的返回值包裹在Some()中,迭代到最后会返回None。我们可以直接在程序中使用next方法。注意:next方法会消耗迭代器,也就是说它会修改迭代器的内部值,定义时需要mut关键字来标记它是可变的

#[cfg(test)]
mod tests {

    #[test]
    fn iterator_demonstration() {
        let v1 = vec![1, 2, 3];
        let mut v1_iter = v1.iter();    // 在不可变引用上创建迭代器

        assert_eq!(v1_iter.next(), Some(&1));
        assert_eq!(v1_iter.next(), Some(&2));
        assert_eq!(v1_iter.next(), Some(&3));
    }
}

使用for循环遍历迭代器时,不需要mut关键字,这是因为for循环会取得迭代器的所有权,并且在内部把它变成可变的。

iter方法在创建的时候,实际上是在集合的元素的不可变引用上创建迭代器。如果要迭代可变的引用,需要使用iter_mut方法。如果要取得元素的所有权,则需要使用into_iter方法。

Rust标准库的Iterator trait中还有其他的方法会调用next方法,我们把这些方法称为消耗型适配器。因为它会耗尽迭代器。例如sum方法就会消耗迭代器。

2.3 迭代器生成方法

定义在Iterator trait上的其他方法称为迭代器适配器,可以把迭代器转换为不同种类的迭代器。在使用时可以使用链式调用多个迭代器适配器来执行复杂的操作,并且这种方法可读性比较高。下面是一个迭代器生成方法的例子。

  • map方法:接收一个闭包作为参数,闭包作用于每个元素,并且产生一个新的迭代器
fn iterator_sum() {
    let v1 = vec![1, 2, 3];
    
    // v1.iter().map(|x| x + 1);    // 仅产生一个新的迭代器,没有执行消耗方法,所以内部元素不会被+1
    // 如果要使得元素加一,就必须使用消耗型适配器
    let result :Vec<_> = v1.iter().map(|x| x + 1).collect();
    assert_eq!(vec![2, 3, 4], result);
}

下面介绍另一个常用迭代器适配器(方法)和例子(使用闭包和迭代器捕获环境):

  • filter方法:接收一个闭包作为参数,闭包返回值为bool类型,如果返回true,就把当前的元素放在一个新的迭代器中,最后生成一个新的迭代器
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|x| x.size == shoe_size).collect() // 获得所有权,返回一个新的Vec
}

2.4 自定义迭代器

实现自定义迭代器,其实就是为某个结构体(类型)实现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
        }
    }
}

#[test]
fn calling_next() {
    let mut counter = Counter::new();

    assert_eq!(counter.next(), Some(1));
    assert_eq!(counter.next(), Some(2));
    assert_eq!(counter.next(), Some(3));
    assert_eq!(counter.next(), Some(4));
    assert_eq!(counter.next(), Some(5));
    assert_eq!(counter.next(), None);
}

#[test]
fn using_other_iterator_methods() {
    let result: u32 = Counter::new()
        .zip(Counter::new().skip(1))    // 产生一个新的迭代器,迭代器的元素是元组
        .map(|(a, b)| a * b).filter(|x| x % 3 == 0)
        .sum();

    assert_eq!(result, 18);
}

2.5 比较循环和迭代器的性能

在Rust中,使用迭代器的代码会在编译时进行优化,迭代器本身是一种高层次的抽象,但是在编译后可以生成和手写后几乎一样的代码,这在Rust中称为零开销抽象(Zero-Cost Abstraction),即使用抽象时不会引入额外的运行时开销。所以在Rust中,我们就可以尽情使用类似迭代器的高层次抽象了,反而比使用诸如for遍历元素的更加快速。

转载声明:

除特殊声明外,本站所有文章均由 debussy 原创,均采用 CC BY-NC-SA 4.0 协议,转载请注明出处:Include Everything 的博客
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇