Rust学习笔记(13)

闭包

Rust的闭包,是指匿名函数,可以当作一个参数来传递,或者当作一个变量来保存。这个概念跟js中的闭包概念是一样的。

要理解闭包,需要举个例子来说明。

假设我们需要模拟一个算法,来给用户生成一个训练计划。先写一个模拟算法,这是一个代价比较大的算法,会花不少时间来运算。这里消耗时间我们用sleep来模拟:

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

fn simulated_expensive_calculation(intensity: u32) -> u32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    intensity
}

下面就是main函数:

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(simulated_user_specified_value, simulated_random_number);
}

simulated_user_specified_value模拟用户的输入,用户指定一个激烈系数,simulated_random_number是系统随机数,用于生成健身计划时候的一个运算参数。我们实现一下generate_workout:

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 break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                simulated_expensive_calculation(intensity)
            );
        }
    }
}

使用提取函数来重构

代码里面,if的第一个块中有两段代码调用了simulated_expensive_calculation,现在我们把这一段提取出来:

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_result = simulated_expensive_calculation(intensity);

    if intensity < 25 {
        println!("Today, do {} pushups!", expensive_result);
        println!("Next, do {} situps!", expensive_result);
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!("Today, run for {} minutes!", expensive_result);
        }
    }
}

这样确实做到了只有一次调用,但是这样提取出来,导致if的两个分支都会被这个执行的消耗给block住,这并不是一个很好的选择。

使用闭包来重构

现在我们使用闭包来重构,我们写一个闭包:

    let expensive_closure = |num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

|num|expensive_closure这个闭包的参数,如果需要多个参数,可以用逗号分开:|param1, param2|

这样我们的generate_workout可以这样写:

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!("Today, do {} pushups!", expensive_closure(intensity));
        println!("Next, do {} situps!", expensive_closure(intensity));
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure(intensity)
            );
        }
    }
}

闭包类型推断和说明

闭包的参数和返回值并不需要指定类型,这一点和函数不同。因为函数是提供给其他人使用的,所以需要有明确的显式的类型定义,这样其他人才知道需要传什么参数,以及返回什么样的值。但是闭包本身是保存为一个变量使用的,并不需要暴露给外面,所以这个意义上来说,就不用刻意显示的指定参数类型和返回值类型了。因为闭包一般都比较短小和简单,所以编译器也可以很容易来推断参数以及返回值的类型。刻意增加类型说明,反而让代码显得冗余了。当然,如果要增加可读性,也是可以给闭包增加参数和返回值类型申明的:

    let expensive_closure = |num: u32| -> u32 {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

我们看一下闭包的各种写法,对比一下第一行的函数:

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

我们虽然可以省略闭包参数的类型,但是一旦编译器推断出类型后,我们是不可以乱用的,比如下面这个用法就会编译报错:

    let example_closure = |x| x;

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

第一次使用的时候,传入闭包的参数是String类型,所以编译器会推断这个闭包的x是String类型的。那么当我们传入数字5时,就会报错,类型不匹配:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |                             ^- help: try using a conversion method: `.to_string()`
  |                             |
  |                             expected struct `String`, found integer

使用特征Fn来将闭包作为泛型参数使用

Fn这个特征(trait)是标准库里面提供的,所有的闭包必须至少实现FnFnMut或者FnOnce这三个trait之一。这些trait让闭包可以作为一个泛型参数进行定义,这个做法有点类似TS中定义了一个Interface,其中一个变量是闭包函数。Rust中这样来写:

struct Cacher<T>
where
    T: Fn(u32) -> u32,
{
    calculation: T,
    value: Option<u32>,
}

这里的calculation就是定义为一个闭包。

实现方法类似下面这么写:

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
            }
        }
    }
}

我们来使用一下这个Cacher,把之前的generate_workout函数改一下:

fn generate_workout(intensity: u32, random_number: u32) {
    let mut expensive_result = Cacher::new(|num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    });

    if intensity < 25 {
        println!("Today, do {} pushups!", expensive_result.value(intensity));
        println!("Next, do {} situps!", expensive_result.value(intensity));
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result.value(intensity)
            );
        }
    }
}

函数和闭包还有一个很大的区别,那就是闭包内是可以使用当前生命周期中的变量,比如下面这样的:

fn main() {
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}

x虽然不是equal_to_x的参数,但是依然可以在闭包中访问,而函数是不可以这样访问的。

注意了,闭包的使用同样有ownership的问题,默认情况下,闭包中的变量是引用,而不是“move”,不过也可以用move关键字来进行说明,这样就会存在owner问题了,看例子:

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

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

    println!("can't use x here: {:?}", x);

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

    assert!(equal_to_x(y));
}

这段代码会编译出错,因为闭包中的x不再是从main中引用来的,而是move进了闭包中,这样main中定义的x就失效了,导致println打印x会报错。

迭代器

迭代器有点类似Java里面的iterator,通常用于一组数据的遍历。迭代器是懒加载的,如果不读取,那它什么都不会做。看个迭代器用于循环的例子:

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

    let v1_iter = v1.iter();

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

所有的迭代器都实现自Iterator特性,这是标准库的一个trait,样子大概这样:

pub trait Iterator {
    type Item;

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

    // methods with default implementations elided
}

其中的next方法用于获取下一个元素,如果没有下一个,则返回None:

    #[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));
        assert_eq!(v1_iter.next(), None);
    }

我们把调用next的方法,都称为迭代消费适配器(consuming adaptors),比如像sum方法:

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

        let v1_iter = v1.iter();

        let total: i32 = v1_iter.sum();

        assert_eq!(total, 6);
    }

注意,这里调用了sum之后,v1_iter的owner就是sum了,后面不可以再使用。

还有map方法,可以生成一个新的迭代器,这种方法,称为迭代适配器:

    let v1: Vec<i32> = vec![1, 2, 3];

    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

    assert_eq!(v2, vec![2, 3, 4]);

再来看一个叫filter的迭代适配器的用法,这filter其实跟java中的stream流里面filter方法一样:

#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filters_by_size() {
        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 in_my_size = shoes_in_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("sneaker")
                },
                Shoe {
                    size: 10,
                    style: String::from("boot")
                },
            ]
        );
    }
}

定义自己的迭代器

假设我们自定义了一个结构体:

struct Counter {
    count: u32,
}

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

现在我们想让我们这个Counter也能使用迭代器特性,那就需要实现Iterator:

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
        }
    }
}

那现在我们的Counter就可以使用迭代器特性了:

    #[test]
    fn calling_next_directly() {
        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_trait_methods() {
        let sum: u32 = Counter::new()
            .zip(Counter::new().skip(1))
            .map(|(a, b)| a * b)
            .filter(|x| x % 3 == 0)
            .sum();
        assert_eq!(18, sum);
    }

zip方法拼接出了4对值:(1,1),(2,3),(3,4),(5,None),实际第4对值(5,None)不会真正出现,因为迭代遇到None就结束了。

改造minigrep的命令行参数

我们在12章写的minigrep中,Config.new传入的是一个vector,现在我们可以直接传入一个迭代器。首先修改main中的调用:

fn main() {
    let config = Config::new(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    // --snip--
}

然后修改lib.rs中的Config的new方法:

impl Config {
    pub fn new(mut args: env::Args) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };

        let filename = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file name"),
        };

        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config {
            query,
            filename,
            case_sensitive,
        })
    }
}

还可以修改下search方法,让代码更简洁:

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 jimmyseraph@testops.vip

×

喜欢就点赞,疼爱就打赏