Rust第十二节-闭包与迭代器

12. 闭包与迭代器

12.1闭包

12.1.1 闭包的构成

闭包可以作为参数、返回值或者将其存储在一个变量中。

我们先来举个例子来讲讲闭包的构成:

rust 复制代码
let y: i32 = 1;
let get_sum = |x| x + y;
let result = get_sum(1);
println!("{}", result)

这里我们声明了一个闭包get_sum,它和函数的声明完全一样,只是我这是用||来作为闭包传参的入口,并且省略了{},当然我们可以加上:

rust 复制代码
let get_sum = |x| { let sum = x + y; sum };

从上面的例子中我们会发现,闭包可以去读取环境中的变量,这是它和函数的区别

12.1.2 闭包的类型

从上面的例子来看,其实我们并没有在闭包内部给它定义类型,其实Rust内部给我们推导了类型,例如:

rust 复制代码
let get_self = |x| x;
get_self(1);
get_self('1'); // error

当我们第一次传入了一个i32的数字类型,这个时候其实已经被默认推导成了i32的类型,当我们再传入一个不同类型的值,会编译出错。

在我们声明一个闭包时,它会自动被推导为三种特征,怎么决定使用哪个特征,是看闭包内怎么去使用读区环境量的参数:

  • 所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次
  • 不对捕获变量改变 的闭包自动实现了 Fn 特征
  • 改变了捕获变量的值但是没有改变捕获变量所有权 的闭包自动实现了 FnMut 特征

我们先来举一个Fn的列子: 我们可以看到我们并没有对y值进行修改,所以这里会被推导为Fn类型;

rust 复制代码
let y = 1;
let get_self = |x| x + y;

我们再来看一个FnMut的例子: 这里我们在函数里面使用了引用来去修改环境参数的字符串,但是没有修改所有权,所以会被推导为FnMut

rust 复制代码
let mut str = String::from("hello");
let mut combine_str = || {
    &str.push_str("world");
};
combine_str();
println!("{:?}", str);

我们最后来看一个强制改变了捕获环境值所有权的用例,它会自动推导为FnOnce,这里我们使用了move关键字,他可以将捕获的环境参数所有权强制移动到闭包内部,例如:

rust 复制代码
let str = String::from("hello");
let combine_str = move || str;
combine_str();

刚刚我们举的例子都是闭包作为一个变量进行存储,我们接下里写一个将闭包作为参数传递的例子:

rust 复制代码
let s = String::from("hello");

let update_str = || println!("{}", s);

fn executeFn<T>(f: T)
where
    T: Fn() -> (),
{
    f();
}

executeFn(update_str);

fn executeFnOnce<T>(f: T)
where
    T: FnOnce() -> (),
{
    f();
}
executeFnOnce(update_str);

fn executeFnMut<T>(mut f: T)
where
    T: FnMut() -> (),
{
    f();
}
executeFnMut(update_str)

12.2 迭代器

12.1.1 创建迭代器

首先我们先来创建一个迭代器,如下:

rust 复制代码
let arr =  vec![1,2,3];
let iter_arr = arr.iter();

我们这里使用iter方法,实际是对arr数组的不可变引用 ,当我们先获取可变引用时,我们需要使用iter_mut方法,或者当我们想直接获取其所有权时,我们可以使用into_iter方法。

12.1.2 Iterator 和 next

所以可迭代的类型都是实现了特征Iterator,并且实现next方法,让我们来看看iterator特征:

rust 复制代码
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

其中Item是一个关联类型,后续会详细讲到。这里我们实现的特征,需要实现next,执行next后,会消耗迭代器。

12.1.3 消耗迭代器

接下里我们来讲讲怎么去消耗迭代器,我们首先创建一个迭代器,如:

rust 复制代码
let iter_arr = vec![1, 2, 3].iter();

然后我们来执行sum方法,它会去执行next方法,迭代每个元素,并且将其加在一起,并且返回得到一个总和,如:

rust 复制代码
let sum: i32 = iter_arr.sum();
println!("{}", sum);

接下来我们来看看怎么用其他的方法来创建迭代器。

12.1.4 使用其他方法创建迭代器

我们来使用map方法来将数组中每个值+1,如下:

rust 复制代码
let arr = vec![1, 2, 3];
let iter_arr = arr.iter().map(|x| x + 1);
println!("{:?}", iter_arr)

我们来看看打印值,如:

rust 复制代码
// Map { iter: Iter([1, 2, 3]) }

我们可以看到,我们的值并没有加1。这是因为我们的迭代器并且执行消耗,所以值不会变化,这里我们执行collect方法去消耗这个迭代器之后,会将其转化为一个动态数组,如:

rust 复制代码
let arr = vec![1, 2, 3];
let iter_arr: Vec<i32> = arr.iter().map(|x| x + 1).collect();
println!("{:?}", iter_arr) // [2,3,4]

12.1.5 使用环境参数

我们还可以去使用闭包的特性去使环境参数。比如,我们使用可迭代类型的filter方法,它会根据根据我们传入的值进行筛选,如果为true则返回,false不返回。例如:

rust 复制代码
#[derive(Debug)]
struct User {
    name: String,
    age: i32,
}
let arr = vec![
    User {
        name: String::from("stephen"),
        age: 18,
    },
    User {
        name: String::from("kyrie"),
        age: 34,
    },
    User {
        name: String::from("kris"),
        age: 22,
    },
];
let less_age: i32 = 30;
// 筛选出年纪小于less_age的用户
let iter_arr: Vec<&User> = arr.iter().filter(|user| user.age < less_age).collect();
println!("{:#?}", iter_arr);

这里我们写了一个简单的用例,根据环境参数less_age去使用filter方法去筛选,年纪小于less_age的用户。

12.1.6 自定义迭代器

最后,我们来自定义一个迭代器,我们首先写一个结构体Counter,如:

rust 复制代码
struct Counter {
    count: i32,
}

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

impl Iterator for Counter {
    type Item = i32;
    fn next(&mut self) -> Option<Self::Item> {
        self.count = self.count + 1;
        if self.count < 6 {
            Some(self.count)
        } else {
            None
        }
    }
}

let mut counter = Counter::new();
println!("{:?}", counter.next()); // 1
println!("{:?}", counter.next()); // 2
println!("{:?}", counter.next()); // 3
println!("{:?}", counter.next()); // 4
println!("{:?}", counter.next()); // 5
println!("{:?}", counter.next()); // None

我们定义了一个结构体,并且为它声明了一个new方法,并且实现Iterator特征,然后我们去执行next方法,就可以可以拿到1-5的值。

12.3 使用迭代器、闭包改进I/O项目

接下来,我们来使用学习的新内容来修改之前做的一个检索功能。

12.3.1 修改入参

之前我们在声明Confignew方法时,我们使用到了一个叫clone克隆的方法,这是为了拿到传入参数的所有权方便后续操作,但是这个方法是很消耗性能的,所以我们这里我们使用迭代器去改造一下。我们先看看之前的这个方法:

rust 复制代码
impl Config {
fn new(args: &[String]) -> Result<Config, &str> {
    if args.len() < 3 {
        return Err("输入的参数不能小于2位");
    }
    let match_text = args[1].clone(); // 消耗性能
    let file_name = args[2].clone();
    Ok(Config {
        match_text,
        file_name,
    })
}
}

我们首先将我们的入参修改,我们之前传入的是一个动态字符串,现在我们直接传入一个迭代器:

rust 复制代码
Config::new(env::args())

并且我们需要在new方法中修改:

rust 复制代码
fn new(mut args: env::Args)

因为迭代器是要消耗每个元素的,所以我们需要加上mut。 因为env::arg()获取到的第一个参数是项目,所以我们需要执行一下arg.next()。然后第二次执行next方法时,我们可以拿到我们需要匹配的值,第三次则可以拿到文件名。因为next拿到的是Option枚举,所以我们需要使用match去拿到最终的值,如:

rust 复制代码
impl Config {
    fn new(mut args: env::Args) -> Result<Config, &'static str> {
        // if args.len() < 3 {
        //     return Err("输入的参数不能小于2位");
        // }
        // let match_text = args[1].clone();
        // let file_name = args[2].clone();
        args.next();
        let match_text = match args.next() {
            Some(text) => text,
            None => return Err("未获取到匹配内容"),
        };
        let file_name = match args.next() {
            Some(name) => name,
            None => return Err("未获取到文件名"),
        };
        Ok(Config {
            match_text,
            file_name,
        })
    }
}

12.3.2 修改匹配

还有一处,之前我们进行匹配的操作时,我们使用的for循环,现在我们直接使用filter方法,去过滤,直接省去for循环遍历这一步,例如:

rust 复制代码
fs::read_to_string().unwrap().lines().filter(|line| line.contains(match_text)).collect();

我们直接使用lines方法获取文件的迭代器,并且执行filter去完成过滤匹配,为什么在rust中,我们使用的更多的是迭代器还不是for循环呢?

  1. 迭代器是一种更高层次的抽象概念,避免我们在开发时疲于去声明、维护各个变量的状态以及处理循环中的逻辑
  2. 迭代器编译出来的是一种零开销底层代码,性能要优于for循环遍历

所以在Rust中,我们经常看到迭代器的使用,并且我们也要习惯于这种写法。

相关推荐
Мартин.7 分钟前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。2 小时前
案例-表白墙简单实现
前端·javascript·css
数云界2 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd2 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常2 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer2 小时前
Vite:为什么选 Vite
前端
小御姐@stella2 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing2 小时前
【React】增量传输与渲染
前端·javascript·面试
eHackyd2 小时前
前端知识汇总(持续更新)
前端
万叶学编程5 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js