Rust:闭包和迭代器

admin 2024年3月28日23:49:32评论7 views字数 10034阅读33分26秒阅读模式
Rust:闭包和迭代器
Rust:闭包和迭代器
Rust:闭包和迭代器
Rust:闭包和迭代器

闭包(closures)

闭包:可以捕获其所在环境的匿名函数 闭包:

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

闭包的定义

闭包的定义格式

let var = |变量名:变量类型| -> 返回值类型{
 函数体;
 返回值
}

  • 如果变量类型可以自动推断出,可以省略
  • 返回值类型没有或可以自动推断,可以省略
  • 函数体只有一行,括号可以省略

闭包的类型推断

  • 闭包不要求标注参数和返回值类型

  • 闭包通常很短小,只在狭小的上下文中工作,编译器通常能推断出类型
  • 当然也可以手动添加闭包的类型标注注意:闭包的定义最终只会为参数/返回值推断出唯一具体的类型
  • Rust:闭包和迭代器

  • 在这个代码中,如果没有下面的两行代码,只有定义闭包,程序会报错,因为无法推断出闭包的参数类型 当有第4行代码后,程序代码就不会报错了,因为根据第4行的代码,可以推断出闭包中的x类型为String 但是有第5行代码,程序还会报错,因为上面已经推断了闭包中x的类型为String,但是第5行的参数类型是number,类型不匹配,所以代码报错

函数和闭包的定义语法区别

Rust:闭包和迭代器

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

为了确保闭包只调用一次,可以使用创建结构体struct,它持有闭包和闭包调用后的结果,在第一次闭包调用后,将结果存储在结构体的变量中,下次可以直接从变量中读取值。避免多次调用闭包

  • 只会在需要结果时才执行闭包
  • 可缓存结果这个模式通常叫做记忆化(memoization)或延迟计算(lazy evaluation)具体参考下面的实例中的写法

如何让struct持有闭包

  • struct的定义需要知道所有字段的类型
    • 需要指定闭包的类型
  • 每个闭包实例都有自己唯一的匿名类型,即使两个闭包签名完全一样
  • 所以需要使用泛型参数和泛型约束(Trait Bound) 具体参考下面的实例中的写法

使用缓存器(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
            }
        }
    }
}

上面的代码仍存在一些问题:

  • Cacher实例假定针对不同的参数arg,value方法总会得到同样的值 编写如下的测试代码

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

测试结果会失败,因为c.value(1);运行后,结构体的value的值变成1,当执行c.value(2)时,value的值不为空,不会重新运行闭包的代码,所以value的值还是1 解决办法:可以使用hashMap代替单个值

  • 将arg参数作为hashMap的key
  • 将value作为arg对应的value

struct Cacher<T>

    where T: Fn(u32) -> u32

{

    calculation: T,

    value: HashMap<u32,u32>,

}

impl<T> Cacher<T>

    where T: Fn(u32) -> u32

{

    fn new(calculation: T) -> Cacher<T>{

        Cacher{

            calculation,

            value: HashMap::new(),

        }

    }

    fn value(&mut self, arg: u32) -> u32{

        self.value.entry(arg).or_insert((self.calculation)(arg)).clone()

    }

}

限制2:只能接收一个u32类型的参数和u32类型的返回值 解决办法:引入多个泛型

理解闭包的用处

例子:生成自定义运动计划的程序

  • 算法的逻辑并不是重点,重点是算法中的计算过程需要几秒钟时间
  • 目标:不让用户发生不必要的等待
    • 仅在必要时调用该算法,而且该算法应该只调用一次 示例代码如下:

use std::thread;
use std::time::Duration;
fn main(){
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;
    generate_workout(
        simulated_user_specified_value,
        simulated_random_number
    );
}
fn simulated_expensive_calculation(intensity: u32) -> u32{
    println!("calculating 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 break today! Remember to stay hydrated!");
        }else{
            println!(
                "Today, run for {} minutes!",
                simulated_expensive_calculation(intensity)
            );
        }
    }
}

上面的代码中,当intensity小于25时,会打印两句话,并且调用两次耗时的函数,当不小于25,但是随机数不等于3时,也会再调用一次耗时的函数,不符合我们上面的要求,所以需要优化代码

  1. 对于intensity小于25的情况,可以将函数运行的结果存储在变量中,然后输出变量,就可以保证只调用一次

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

  1. 但是这样修改也会有一个新的问题,函数的调用是在if之前做的,也就是无论intensity的值是什么,都会运行函数,但是当random_number == 3时,就不需要执行函数,造成运行资源浪费,这就需要使用闭包,先声明闭包,只有在需要的时候才会调用执行

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

上面代码在if前定义了一个匿名函数闭包,要求传入一个u32的参数,该匿名函数会返回一个u32类型的值,这里只是声明闭包,并没有调用,实际在后面才会调用,但是这样的代码又出现了一个问题,就是闭包被多次调用了,我们希望的是闭包只调用一次 可以使用struct结构体存储闭包和闭包的结果,需要使用Trait和Trait Bound

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

定义了一个结构体,使用泛型参数,约束泛型要求必须是一个Fn,且输入参数为u32类型,返回参数也是u32类型。calculation用于存储闭包,value用于存储闭包的运行结果, 然后为结构体实现关联的构造函数,实现当读取值时,先判断value是否为空,如果为空,则运行闭包获取结果,如果不为空,就直接返回value值

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

然后在定义闭包的地方,先创建一个结构体,将闭包的内容传进去,在需要闭包结果的地方,调用结构体的value方法即可

fn generate_workout(intensity: u32, random_number: u32){
    let mut expensive_closure = Cacher::new(|num| {
        println!("calculating 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 break today! Remember to stay hydrated!");
        }else{
            println!(
                "Today, run for {} minutes!",
                expensive_closure.value(intensity)
            );
        }
    }
}

使用闭包捕获环境

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

fn main(){
 let x = 4;
 let equal_to_x = |z| z==x;
 let y =4
 assert!(equal_to_x(y));
}

上面的代码可以看出,闭包可以直接访问x变量
如果换成函数定义,就会报错,因为函数不能访问外部的变量 

Rust:闭包和迭代器

但是这个操作会产生内存开销

闭包从所在环境捕获值的三种方式

与函数获得参数的三种方式一样:

  • 取得所有权:FnOnce
  • 可变借用:FnMut
  • 不可变借用:Fn 创建闭包时,通过闭包对环境值的使用,Rust推断出具体使用哪个Trait
  • 所有的闭包都实现了FnOnce
  • 没有移动捕获变量的实现了FnMut
  • 无需可变访问捕获变量的闭包实现了Fn 当指定Fn traitBound之一时,首先应该使用Fn,基于闭包体里的情况,rust会提示是否需要FnMut或FnOnce

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

迭代器

什么是迭代器

迭代器模式:对一系列项执行某些任务 迭代器负责:

  • 遍历每个项
  • 确定序列(遍历)何时完成 Rust的迭代器:
  • 懒惰的:除非调用消费迭代器的方法,否则迭代器本身没有任何效果

fn main(){
 let v1 = vec![1,2,3];
 let v1_iter = v1.iter();  //此方法可以生成一个v1的迭代器,但是此时没有使用,所以没有效果
 // 使用迭代器
 for elem in v1_iter{
  println!("{}",elem);
 }
}

lterator Trait

所有的迭代器都实现了iterator Trait iterator Trait定义与标准库,定义大致如下:

pub trait iterator{
 type item;
 fn next(&mut self)-> Option<self::item>;
 // 默认实现被省略的方法
}

type item和self::item定义了与此该trait关联的类型

  • 实现iterator trait需要你定义一个item类型,它用于next方法的返回类型(迭代器的返回类型)

iterator trait 仅要求实现一个方法:next

  • next:每次返回迭代器中的一项,返回的结果包裹在some里,迭代结束的时候,返回None 可直接在迭代器上调用next方法

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

    }

}

几个迭代方法

  • iter方法:在不可变引用上创建迭代器
  • into_iter方法:创建的迭代器会获得所有权
  • iter_mut方法:迭代可变的引用

消耗迭代器的方法

在标准库中,iterator trait有一些带默认实现的方法 其中有一些方法会调用next方法

  • 实现iterator trait时必须实现next方法的原因之一 调用next的方法叫做”消耗性适配器“
  • 因为调用它们会把迭代器耗尽
  • 例如:sum方法(就会耗尽迭代器)
    • 取得迭代器的所有权
    • 通过反复调用next,遍历所有元素
    • 每次迭代,把当前元素添加到一个总和里,迭代结束,返回总和

产生其他迭代器的方法

  • 定义在iterator trait上的另外一些方法叫做”迭代器适配器“
    • 把当前的迭代器转换为不同种类的迭代器
  • 可以通过链式调用使用多个迭代器适配器来执行复杂的操作,这种调用可读性较高 例如map:
  • 接收一个闭包,闭包作于每个元素,
  • collect方法:消耗性适配器,把结果收集到一个集合类型中

#[cfg(test)]
mod tests{
    #[test]
    fn iteratior_sum(){
        let v1 = vec![1,2,3];
        let v2: Vec<i32> = v1.iter().map(|x|x+1).collect();
        println!("{:?}",v2);
    }
}

示例:输入一个迭代器对象,经过过滤,返回结果还是迭代器

#[derive(PartialEq, Debug)]

struct shoe {

    size: u32,

    style: String,

}

fn shoes_in_my_size(shoes: Vec<shoe>, shoe_size: u32) -> Vec<shoe> {

    shoes.into_iter().filter(|s| s.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 in_my_size = shoes_in_my_size(shoes, 10);

    println!("{:?}", in_my_size)

}

创建自定义迭代器

使用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>{

        self.count += 1;

        if self.count <= 5{

            Some(self.count)

        }else{

            None

        }

    }

}

fn main() {

    let counter = Counter::new();

    for i in counter{

        println!("{}", i);

    }

}

上面的代码,先定义了一个结构体,包括一个u32类型的数据,然后给结构体添加了构造函数,确保每次创建的结构体对象都是从0开始,然后给结构体实现迭代器trait,当结构体内的值小于等于5时,会返回迭代器的值,并每次运行+1,当大于5时,会返回none。扩展迭代器的方法:要求:

  • 第一个迭代器就是现有的1-5,第二个迭代器就是当前的去除第一个元素
  • 然后将两个迭代器的元素相乘,产生一个新的迭代器
  • 对新的迭代器进行元素过滤,要求能被3整除
  • 对剩下的元素求和

fn main(){

    let sum = Counter::new().zip(Counter::new().skip(1))

        .map(|(a, b)| a * b)

        .filter(|x| x % 3 == 0)

        .sum::<u32>();

    println!("sum: {}", sum);

}

性能笔记:循环VS迭代器

一个测试:把一本小说的内容放在一个string里面,搜索”the“

  • 迭代器的版本更快一点
  • Rust:闭包和迭代器

  • 零开销抽象:使用抽象不会引入格外的运行开销 在rust中,可以尽情使用闭包、迭代器等高层次的抽象,不用去考虑性能
Rust:闭包和迭代器
Rust简介和环境搭建
Rust基础知识
Rust中的简单数据类型
Rust其他数据类型
Rust流程控制
Rush内存管理及所有权
Rust中泛型、Trait及生命周期
Rust的错误处理机制

原文始发于微信公众号(宁雪):Rust:闭包和迭代器

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年3月28日23:49:32
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Rust:闭包和迭代器https://cn-sec.com/archives/2592464.html

发表评论

匿名网友 填写信息