首页 > 其他分享 >12.函数式语言特性:迭代器与闭包

12.函数式语言特性:迭代器与闭包

时间:2023-01-16 11:55:40浏览次数:38  
标签:闭包 12 迭代 next let u32 fn

一、闭包:能够捕获环境的匿名函数

Rust中的闭包是一种可以存入变量或作为参数传递给其他函数的匿名函数。你在可以在一个地方创建闭包,然后在不同的上下文环境中调用该闭包来完成运算。和一般函数不同,闭包可以从定义它作用域中捕获值。

1、使用闭包来创建抽象化的程序行为

假设有这样一个场景:我们身处的初创公司正在开发一个为用户提供健身计划的应用。使用Rust编写的后端程序在生成计划的过程中需要考虑到年龄、身体质量指数(BMI)、健身偏好、运动历史、指定强度值等因素。具体的算法究竟长什么样子在这个例子中并不重要,重要的是这个计算过程会消耗掉数秒钟时间。我们希望只在必要的时候调用算法,并且只调用一次,以免让用户等待过久。创建程序项目fitness_routines
我们会在如下函数模拟这一假设的算法计算过程,它会依次打印出calculating slowly..并等待两秒,接着返回传递给它的数字。

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

编写算法部分代码

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

1.使用函数来进行重构

首先,我们可以把重复调用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
            )
        }
    }
 }


这个修改统一了所有针对simulated_expensive_calculation的调用,并修复了第一个if块中不必要的两次函数调用。但是我们在所有调用了这个函数的情况下都要等待调用结果,这对于内层if代码来说显得异常浪费。

2.使用闭包存储代码来进行重构

我们定义一个闭包,并将闭包而不是函数的计算结果存储在变量中,如下所示。

fn generate_workout(intensity: u32, random_number: u32) {
	//闭包代码
    let expnsive_closure = |num| {
        println!("calculating slowly..");
        thread::sleep(Duration::from_secs(2));
        num
     };

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

闭包的定义放置在=之后,它会被赋值给语句左侧的expensive_closure变量。为了定义闭包,我们需要以一对竖线(|)开始,并在竖线之间填写闭包的参数;之所以选择这个写法是因为它与Smalltalk及Ruby中的闭包定义类似。
在参数后面,我们使用了一对花括号来包裹闭包的函数体。如果这恶闭包只是单行表达式,你也可以省略花括号。在闭包结束后,我们需要用一个分号来结束当前的let语句。因为闭包代码中的最后一行(num)没有以分号结尾,所以该行产生的值会被当做闭包的结束返回给调用者,其行为与普通函数的完全一致。
注意,这条let语句意味着expensive_closure变量存储了一个匿名函数的定义,而不是调用该匿名函数而产生的返回值。
定义完闭包侯,我们就可以修改if块中的代码来调用闭包,执行代码并求得结果了。调用闭包的类似于调用普通函数:先指定存储闭包定义的变量名,再跟上一对包含传入参数的括号。

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

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

现在,耗时的计算操作只会再一个地方被调用,而具体的代码只会再需要结果的地方得到执行。

2、闭包的类型推断和类型标注

和fn定义的函数不同,闭包并不强制要求你标注参数和返回值的类型。Rust之所以要求我们在函数定义中进行类型标注,是因为类型信息是暴露给用户的显式接口的一部分。严格定义接口有助于所有人对参数和返回值的类型取得明确共识。但是,闭包并不会被用于这样的暴露接口:它被存储在变量中,在使用时既不需要命名,也不会被暴露给代码库的用户。
闭包通常都相当短小,且只在狭窄的代码上下文中使用,而不会被应用在广泛的场景下。在这种限定环境下,编译器能够可靠地推断出闭包参数的类型及返回值的类型。
不过,就和变量一样,假如你愿意为了明确性而接受不必要的繁杂作为代价,那么你仍然可以为闭包手动添加类型标注。

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

添加类型标注之后,闭包的语法就和函数的语法更加相似了。下面列表纵向对比了函数和闭包的定义语法,它们都实现了为参数加1并返回的行为。

fn add_one_v1 (x: u32) -> u32 { x + 1}  //函数定义
fn add_one_v1 |x: u32| -> u32 { x + 1};  //完整标注了类型的闭包定义
fn add_one_v1 |x| { x + 1};  //省去了闭包定义中的类型标注
fn add_one_v1 |x|  x + 1;  //在闭包块只有一个表达式的前提下省去了花括号

闭包定义中的每一个参数及返回值都会被推导为对应的具体类型。如下实例展示了一个直接将参数作为结果返回的闭包。注意,我们并没有为它添加类型标注:如果调用包两次,第一次使用String类型的参数,而第二次使用u32类型的参数,那么就会发生编译错误。

fn main() {
    let example_closure = |x| y;

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


当我们首先使用String值调用example_closure时,编译器将闭包的参数x的类型和返回值的类型都推导为了String类型。接着,这些类型信息被绑定到了example_closure闭包中,当我们尝试使用其他类型调用这一闭包时就会触发类型不匹配的错误。

3、使用泛型参数和fn trait来存储闭包

让我们回到fitness_routines项目中,这个代码依然不必要地多次调用了耗时地计算闭包。这个问题的解决方案是将耗时的闭包的结果存储在变量中,并在随后需要结果的地方使用该变量而不是继续调用闭包。但需要注意的是,这个方法可能会造成大量的代码重复。
我们还有另一种可用的解决方案:创建一个同时存放闭包和闭包返回值的结构体。这个结构体只会在我们需要获得结果值时运行闭包,并将首次运行闭包时的结果缓存起来,这样余下的代码就不必再负责存储结果,而可以直接复用该结果。这种模式一般被称为记忆化(memoization)\或惰性求值(lazy evaluation)
为了将闭包存储在结构体中,我们必须明确指定闭包的类型,因为结构体各个字段的类型在定义时就必须确定。但需要注意的是,每一个闭包实例都有它自己的匿名类型。
标注库中提供了一系列Fn trait,而所有的闭包都至少实现了FnFnMutFnOnce
我们会在Fntrait约束中添加代表了闭包参数和闭包返回值的类型。
如下示例,定义了一个Cacher结构体,它存储了一个闭包和一个可选结果值。

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

Cacher结构体拥有一个泛型T的calculation字段,而trait约束规定的这个T代表一个使用Fn trait的闭包。另外,我们存储在calculation中的闭包必须有一个u32参数(在Fn后面的括号中指定),同时必须返回一个u32值(在->后面指定)。

注意:
函数同样也可以使用这3个Fn trait。假如代码不需要从环境中捕获任何值,那么我们也可以使用实现了Fn trait的函数而不是闭包。

另一个字段value的类型是Option<u32>。在运行闭包之前,value会被初始化为None。而当使用Cacher的代码请求闭包的执行结果时,Cacher会运行闭包并将结果存储在value的Some便体重。之后,如果代码重复请求闭包的结果,Cacher就可以避免再次运行闭包,而将缓存在Some变体中的结果返回给调用者。

impl<T> Cacher<T>
	where T: Fn(u32) -> u32 //1
{
	fn new(calculation: T) -> Cacher<T> { //2
		Cacher { //2
			calculation,
			value: None,
		}
	}
	
	fn value(&mut self, arg: u32) -> u32 { //4
		match self.value { 
			Some(v) => v, //5
			None => {  //6
					let v = (self.calculation)(arg);
					self.value = Some(v);
					v
			},
		}
	}
}

我们希望Cacher自行管理结构体中的各个字段,从而避免调用代码意外地直接修改这些字段中地值,因此这些字段是私有地。
Carcher::new含税会接受一个泛型参数T,它与Cacher结构体有着相同地trait约束。调用Cacger::new会返回一个在calculation字段中存储了闭包地Cacher实例。因为我们还没有执行这个闭包,所以value字段地值被设置为了None。
当调用代码需要获得闭包的执行结果时,它们将会调用vlaue方法而不是调用闭包本身。这个方法会检查self.value中是否已经拥有了一个属于Some变体的返回值,如果有的话,它会直接返回Some中的值作为结果而无须再次执行闭包。
而如果self.value是None的话,则代码会先执行self.calculation中的闭包并将返回值存储在self.value中以便将来使用,最后再把结果返回给调用者。
如下演示了generate_workout函数中使用Cacher结构体。

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

impl<T> Cacher<T>
	where T: Fn(u32) -> u32 //1
{
	fn new(calculation: T) -> Cacher<T> { //2
		Cacher { //2
			calculation,
			value: None,
		}
	}
	
	fn value(&mut self, arg: u32) -> u32 { //4
		match self.value { 
			Some(v) => v, //5
			None => {  //6
					let v = (self.calculation)(arg);
					self.value = Some(v);
					v
			},
		}
	}
}

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

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

上面的代码没有直接将闭包存储在变量中,而是将闭包存储在了新创建的Cacher实例。接着,在每一个需要结果的地方,我们都调用了Cacher实例的value方法。无论我们调用多少次value方法,或者一次都不调用,真正耗时的计算操作都最多只会执行一次。

4、Cacher实现的局限性

缓存值是一种相当通用且有效的策略,但当前的Cacher实现存在两个问题。
第一个问题:Cacher实例假设value方法会为不同的arg参数返回相同值,也就是说,类似于下面的Cacher测试将会失败:

struct Cacher<T>
where
    T: Fn(u32) -> u32,
{
    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
            }
        }
    }
}

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

    #[test]
    fn call_with_different_values() {
        let mut c = Cacher::new(|a| a);

        let v1 = c.value(1);
        let v2 = c.value(2);

        assert_eq!(v2, 2);
    }
}

这个测试中创建的Cacher实例会存储一个原样返回参数值的闭包。它分别使用了1和2作为arg参数来调用Cacher实例的value方法。我们期望在参数为2时调用value方法会返回2。

这里的问题在于我们第一次使用1作为参数来执行c.value时,Cacher实例九江Some(1)存储在了self.value中。在这之后,无论我们在调用value方法时传入的值是什么,它都只会返回1。
解决这个问题的方法是让Cacher存储一个哈希表而不是单一的值。这个哈希表使用传入的arg值作为关键字,并将关键字调用闭包后的结果作为对应的值。相应地,value方法不再简单地判断self.value的值是Some还是None,而是会检查哈希映射里是否存在arg这个关键字。如果存在,Cacher就直接返回对应的值;如果不存在,则调用闭包,使用arg关键字将结果存储哈希表之后再返回。
第二个问题:Cacher实现只能接收一个获取u32类型参数并返回u32类型的值的闭包。

5、使用闭包捕获上下文环境

fitness_routines项目中,我们只把闭包作为一个内部的匿名函数来使用。闭包还有一项函数所不具备的功能:它们可以捕获自己所在的环境并访问自己被定义时的作用域中的变量。
如下示例,有一个存储在equal_to_x变量中的闭包,它使用了自己所在域中的变量x

fn main() {
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}

即使x不是equal_to_x的参数,equal_to_x闭包也可以使用定义在同一个作用域中的变量x

这个功能是函数所不具备的,类似于下面的代码是无法通过编译的。:

fn main() {
    let x = 4;

    fn equal_to_x(z: i32) -> bool { 
        z == x
    }

    let y = 4;

    assert!(equal_to_x(y));
}

运行结果:

当闭包从环境中捕获值时,它会使用额外的空间来存储这些值以便在闭包体内使用。在大多数情况下,我们都不需要在执行代码时捕获环境,也不想为这种场景产生额外的内存开销。因为函数不被允许从环境中捕获变量,所以定义和使用函数永远不会产生这种开销。
闭包可以通过3中方式从它们的环境中捕获值,这和函数接收参数的3种方式完全一致:获取所有权、可变借用及不可变借用。这种3种方式被分别编码在如下所示的3种Fn系列的trait种:

  • FnOnce:意味着闭包可以从它的封闭作用域中消耗捕获的变量。为了实现这一功能,闭包必须在定义时取得这些变量的所有权并将它们移动至闭包中。这也是它的含义:因为闭包不能多次获取并消耗掉同一变量的所有权,所以它只能被调用一次;
  • FnMut:可以从环境中可变地借用值并对它们进行修改;
  • Fn:可以从环境中不可变地借用值;
    当你创建闭包时,Rust会基于闭包从环境中使用值的方式来自动推导出它需要使用的trait。所有闭包都自动实现了FnOnce,因为它们至少都可以被调用一次。那些不需要移动被捕获变量的闭包还会实现Fnmut,而那些不需要对捕获变量进行可变访问的闭包则同时实现了Fn。
    在上述示例中,因为equal_to_x闭包只需要读取x中的值,所以它仅仅不可变地借用了x并实现了Fn trait
    当我们希望强制闭包获取环境中值得所有权,那么你可以在参数列表前添加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);

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

    assert!(equal_to_x(y));
}

这个代码无法被编译,运行结果:

因为我们添加了move关键字,所以x的值会在定义闭包时移动至闭包中。由于闭包拥有了x的所有权,所以main函数就无法在println!语句中使用x。移除println!一行或移除move关键字,就会修复该示例。

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

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

    let equal_to_x = |z| z == x;

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

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

    assert!(equal_to_x(y));
}

二、使用迭代器处理元素序列

迭代器模式允许你依次为序列中的每一个元素执行某些任务。迭代器会在这个过程中负责遍历每一个元素并决定序列何时结束。
在Rust中,迭代器时惰性的。这意味着创建迭代器侯,除非你主动调用方法来消耗并使用迭代器,否则它们不会产生任何实际的效果。
如下示例,通过调用Vec<T>iter方法创建了一个用于遍历动态数组v1的迭代器

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

    let v1_iter = v1.iter();
}

如下代码,将迭代器的创建和for循环中的使用分开。迭代器被存储在v1_iter变量中,此时还没有出现任何遍历,只有当使用了迭代器v1_iter的for循环开始执行时,迭代器才开始为每一次循环产生一个元素,并将每个值打印出来。

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

    let v1_iter = v1.iter();

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

某些语言没有在标准库中提供迭代器特性,为了实现类似的功能,你通常都需要定义一个从0开始的变量作为索引来获得动态数组中的值,并在循环中逐次递增这个变量的值,直到它达到动态数组的总长度为止。
迭代器会为我们处理所有上述的逻辑,这减少了重复代码并消除了潜在的混乱。另外,迭代器还可以用同一的逻辑来灵活处理各种不通种类的序列。

1、Iterator trait和next方法

所有的迭代器都实现了定义于标注库中的Iterator trait。该trait的定义类似于下面这样:

fn main() {
    pub trait Iterator {
        type Item;

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

        // 此处省略了方法的默认实现
    }
}

注意,这个定义使用了两种新语法:type ItemSelf::Item,它们定义了trait的关联类型。这边段代码表明,为了实现Iterator trait,我们必须要定义一个具体的Item类型,而这个Item类型会被用作next方法的返回值类型。换句话说,Item类型将是迭代器返回元素的类型。
Iterator trait只要求实现者手动定义一个方法:next方法,它会在每次被调用时返回一个包裹在Some中的迭代器元素,并在迭代结束时返回None。
我们直接在迭代器上调用next方法。如下示例创建了一个动态数组迭代器,并演示了重复调用迭代器的next方法会得到怎么样的返回值。

fn main() {
    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);
}

注意,这里的v1_iter必须是可变的,因为调用next方法改变了迭代器内部用来记录序列位置的状态。换句话说,这段代码消耗或使用了迭代器,每次调用next都吃掉了迭代器中的一个元素。
另外还需要注意到,iter方法生成的是一个不可变引用的迭代器,我们通过next取得的值实际上是指向动态数组中各个元素本身的不可变引用。如果你需要创建一个取得v1所有权并返回元素本身的迭代器,那么你可以使用into_iter方法。类似地,如果你需要可变引用迭代器,那么你可以使用iter_mut方法。

2、消耗迭代器地方法

标准库为Iterator trait提供了多种默认实现地方法。这些方法中地一部分会在定义中调用next方法,这也是我们需要在实现Iterator trait时手动定义next方法的原因。这些调用next的方法也被称为消耗适配器,因为它们同样消耗了迭代器本身。
sum方法为例,这个方法会获取迭代器的所有权并反复调用next来遍历元素,进而导致迭代器被消耗。在迭代过程中,它会对所有元素进行求和并在迭代结束后将总和作为结果返回。如下示例展示了sum方法的使用场景:

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

    let mut v1_iter = v1.iter();

    let total: i32 = v1_iter.sum();

    assert_eq!(total, 6);
}

3、生成其他迭代器的方法

Iterator trait还定义了另外一些被称为迭代器适配器的方法,这些方法可以使你将已有的迭代器转换成其他不通类型的迭代器。如下示例展示了一个名为map的迭代器适配器方法,它接收一个用来处理所有元素的闭包作为参数并会生成一个新的迭代器。新的迭代器同样会遍历动态数组中的所有元素并返回经过闭包处理后增加了1的值。

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

    v1.iter().map(|x| x + 1);
}

这个代码会出现如下告警:

编译器通过警告提示我们:迭代器适配器使惰性的,除非我们消耗迭代器,否则什么事情都不会发生。为了修复这一问题并消耗迭代器,我们可以使用collect方法,它会消耗迭起并将结果值收集到某种结合数据类型中。
如下示例,我们遍历了通过map方法生成的新迭代器并将返回的结果收集到一个动态数组中。最终,这个动态数组会包含原数组中的所有元素加1之后的值。

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

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

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

由于map接收一个闭包作为参数,所以我们可以对每个元素指定想要执行的任何操作。

4、使用闭包捕获环境

迭代器的filter方法会接收一个闭包作为参数,这个闭包会在遍历迭代器中的元素时返回一个布尔值,而每次遍历的元素只有在闭包返回true时才会被包含在filter生成的新迭代器中。
如下示例,我们传入一个从环境中捕获了变量shoe_size的闭包来使用filter方法,这个闭包会遍历一个由Shoe结构体实例组成的集合,并返回集合中拥有特定尺寸的鞋子。

#[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")
                },
            ]
        );
    }
}

shoes_in_my_size函数接收一个由鞋子组成的动态数组和一个鞋子的尺寸作为参数,它会返回一个至包含指定尺寸鞋子的动态数组。
shoes_in_my_size函数体中,我们调用了into_iter来创建可以获取动态数组所有权的迭代器。接着,我们调用filter来讲这个迭代器是配成一个新的迭代器,新的迭代器只会包含返回值为true的那些元素。
闭包从环境中捕获了shoe_size参数并将它的值与每只鞋子的尺寸进行比较,这一过程会过滤掉所有不符合尺寸的鞋子。最后,调用collect来讲迭代器适配器返回的值收集到动态数组中,并将其作为函数的结果返回。
最后的测试表明,我们在调用shoes_in_my_size时会得到符合指定尺寸的鞋子。

5、使用Iterator trait来创建自定义迭代器

我们会创建一个从1遍历到5的迭代器。首先,我们会创建一个结构体,以便存储迭代过程中所需的数值。接着我们会运用这些数值,为这个结构体实现Iterator trait,从而使它称为迭代器。
如下示例,定义了Counter结构体,并定义了一个关联函数new用于创建Counter的实例。

struct Counter {
    count: u32,
}

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

Counter结构体只有一个名为count的字段,它存储的u32值被用来记录迭代器从1遍历到5这个过程中的状态。count字段被设定为私有的,以便Counter能够独立管理其中的值。new函数则确保了任何一个新实例中的count字段的值都会从0开始。
接下来,我们会为Counter类型实现Iterator trait,并通过定义next方法的函数体来指定迭代器被使用时的具体行为:

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;

        if self.count < 6 {
            Some(self.count)
        } else {
            None
        }
    }
}

我们希望返回值的序列从1开始,而这个迭代器在每次迭代式都会对其内部的状态加1,所以count的值都初始化为了0。当count的值小于6时,next会将当前值包裹在some中返回,而当count大于或等于6时,迭代器则会返回None。

1.使用Counter迭代器的next方法

一旦实现了Iterator trait,我们就拥有了一个迭代器。如下示例的测试借助next方法来直接地使用Counter结构体的迭代器功能。

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

    #[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);
    }
}

这个测试首先在counter变量中创建了一个新的Counter实例,接着返回调用next来验证实现的迭代器行为是否符合我们的期望,也就是返回从1到5的值。

2.使用其他Iterator trait方法

我们只需要提供next方法的定义便可以使用标准库中那些拥有默认实现的Iterator trait方法,因为这些方法都依赖于next方法的功能。
例如,假设我们希望将一个Counter实例产生的值与另一个 Counter实例跳过首元素后的值一一配对,接着将配对的两个值相乘, 最后再对乘积中能被3整除的那些数字求和。
如下示例演示了这一过程:

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

    #[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对值,它在两个迭代器中的任意一个返回None时结束迭代,所以理论上的第五对值永远不会被生成。

三、改进I/O项目

1、使用迭代器代替clone

在minigrep项目中,我们获取了String序列值的一个切片,随后又利用索引访问并克隆这些值来创建新的Config结构体实例,从而使Config结构体可以拥有这些值的所有权。

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
        
        Ok(Config { query, filename, case_sensitive })
    }
}

我们之所以需要在这里使用clone是因为new函数并不持有args参数内元素的所有权,我们获得的仅仅是一个String序列的切片,为了返回Config实例的所有权,我们必须要cloneConfigquery字段和filename字段中的值,只有这样,Config才能拥有这些值的所有权。
在学习了迭代器之后,我们现在可以在new函数中直接使用迭代器作为参数来获取其所有权,而无须在借用切片,还可以使用迭代器附带的功能来进行长度检查和索引。这将使Config::new函数的责任范围更加明确,因为我们通过迭代器将读取具体值的工作分离了出去。

fn main() {
    let args: Vec<String> = env::args().collect();

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

    if let Err(e) = minigrep::run(config) {
        eprintln!("Application error: {}", e);

        process::exit(1);
    }
}

只要Config::new能够获取迭代器的所有权,我们就可以将迭代器产生的String值移动到Config中,而无须调用clone进行二次分配。

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

    if let Err(e) = minigrep::run(config) {
        eprintln!("Application error: {}", e);

        process::exit(1);
    }
}

env::args函数的返回值其实就是一个迭代器。与其将迭代器产生的值收集至动态数组侯再作为切片传入Config::new中,不如选择直接传递迭代器本身。
接下来,我们修改minigrep项目中的Config::new修改如如下实例:

impl Config {
    pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
        
        Ok(Config { query, filename, case_sensitive })
    }
}

env::args函数的标准库文档表明,它会返回一个类型为std::env::Args的迭代器。据此,我们将Config::new函数签名中的args参数的类型从&[String]改为了std::env::Args。由于我们获得了args的所有权并会在函数体中通过迭代来改变它,所以我们在args参数前指定mut关键字。
接下来,我们会对应修复Config::new函数体。因为标注库文档指出std::env::Args实现了Iterator trait,所以我们能够基于它的实例调用next方法。

impl Config {
    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("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 })
    }
}

请记住,env::args的返回值的第一个值使程序本身的名称。为了忽略它,我们必须先调用一次next并忽略返回值。随后,我们再次调用next来取得用于Config中的query字段的值。如果next返回一个Some变体,我们就会使用match来提取这个值;而如果它返回的是None,则表明用户没有提供足够的参数,我们需要让这个函数提前返回Err值。接下来,对filename进行类似的处理。

2、使用迭代器适配器让代码更加清晰

我们可以在minigrep项目的search函数中使用迭代器:

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();
    
    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

通过迭代器适配器的相关方法来更加简单明了地编写这段代码。另外,这样还能避免使用可变地临时变量results。函数式编程风格倾向于在程序中最小化可变状态的数量来使代码更加清晰。消除可变状态也使我们可以在未来通过并行化来提升搜索效率,因为我们不再考虑并发访问results动态数组时的安全问题。

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

searcha函数并用来返回contenst中包含query的所有行。这段代码使用了filter适配器来进行过滤,从而只保留满足line.contains(query)条件的行。接着,我们使用collect方法将所有匹配的行收集成一个动态数组。这样,代码就简单多了。

四、比较循环和迭代器的性能

迭代器比循环要稍微快了一点。迭代器是Rust语言中的一种零开销抽象,这个词意味着我们在使用这些抽象时不会引入额外的运行开销。

标签:闭包,12,迭代,next,let,u32,fn
From: https://www.cnblogs.com/ColoFly/p/17055079.html

相关文章

  • docker 创建seafile 1bec7de0740f4117904bf3be5122ecaf
    docker创建seafile部署Portainerdockerpullportainer/portainerdockerrun-p9000:9000-p8000:8000--nameportainer\--restart=always\-v/var/run/docker.......
  • centos8 编译安装 Realtek 8125 2.5G网卡驱动
    1.通过命令lspci查看网卡型号:#lspciEthernetcontroller:IntelCorporationEthernetConnection(7)I219-V(rev10)Ethernetcontroller:RealtekSemiconductorC......
  • 12.PyQt5【基本组件】多行输入框-QTextEdit
    一、前言QTextEdit文本输入框可以输入多行文本。二、学习目标1.QTextEdit常用方法2.QTextEdit常用信号3.QTextEdit组件的应用三、知识点1.【QTextEdit常用方法】......
  • (12)go-micro微服务JWT跨域认证
    目录一JWT介绍二JWT优缺点三JWT使用1.导包和数据定义2.生成JWT3.解析JWT4.完整代码四最后一JWT介绍JWT英文名是JsonWebToken,是一种用于通信双方之间传递安全......
  • 20211217|写给一年后的培民的一封信
    亲爱的民:培民,见面如晤,没错这是一封来自一年前的信。自四月份考试结束,我就游荡于广州和深圳,记4.14下午自广州到深圳,第一站即是找立彬,我自知今年插本已是与我无缘,随即开始......
  • 20211217|写给一年后的立彬的一封信
    亲爱的彬:立彬,见面如晤,没错这是一封来自一年前的信。自四月份考试结束,我就游荡于广州和深圳,记4.14下午自广州到深圳,第一站即是找你,顶楼你留了门卡,大门你远程开,晚上下班我......
  • 代码随想录18 LettCode 513. 找树左下角的值 112. 路径总和 106. 从中序与后序遍历序
    513.找树左下角的值下面运用层序遍历法比较简单,当遍历到一层时设立一个值去不断覆盖一层的队头,即最左边元素classSolution{public:intfindBottomLeftValue(Tr......
  • java基础12 文本注释javadoc
    文本注释javadoc文本注释,javadoc:/**接回车可以生成一些注释的文档,直接看代码吧用window命令窗口执行:javadocjava文件,中间可以加一大堆的参数使得中文不乱码​......
  • Educational Codeforces Round 120 (Rated for Div. 2) C,D
    EducationalCodeforcesRound120(RatedforDiv.2)C,DCC这个题目大意是给我们n个数,我们可以把ai变成ai-1,或者是把ai变成aj,而我们需要知道最少的操作数把n个数的和......
  • ABB 800XA学习笔记12:系统架构4
    这一篇学习笔记我在新浪博客发表过,地址是ABB800XA学习笔记12:系统架构4_来自金沙江的小鱼_新浪博客(sina.com.cn)在这里也发一遍,以免丢失接着学习。我把学习内容切成一小......