{行动是绝望的毒药|Action is the antidote to despair} -- 美国音乐家 琼·贝兹
大家好,我是柒八九。
今天,我们继续Rust学习笔记的探索。我们来谈谈关于Rust学习笔记之闭包和迭代器的相关知识点。
如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。
文章list
- Rust学习笔记之Rust环境配置和入门指南
- Rust学习笔记之基础概念
- Rust学习笔记之所有权
- Rust学习笔记之结构体
- Rust学习笔记之枚举和匹配模式
- Rust学习笔记之包、Crate和模块
- Rust学习笔记之集合
- Rust学习笔记之错误处理
- Rust学习笔记之泛型、trait 与生命周期
你能所学到的知识点
- 函数式编程 推荐阅读指数 ⭐️⭐️⭐️⭐️
- 闭包:可以捕获环境的匿名函数 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- 使用迭代器处理元素序列 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
好了,天不早了,干点正事哇。
函数式编程
函数式编程是一种编程范式,它将计算视为数学函数的求值过程,强调函数的纯洁性和不可变性。函数式编程语言通常支持高阶函数、闭包、惰性求值等特性,可以提高代码的可读性、可维护性和可扩展性。
常见的函数式编程语言包括:Haskell
、Lisp
、Scheme
、ML
、Erlang
、Clojure
、Scala
、F#
等。
JS中函数式编程的体现
在JS
中,高阶函数
、闭包
、惰性求值
等特性都是函数式编程的重要组成部分。
高阶函数
高阶函数
是指接受函数作为参数或返回函数的函数。它可以将函数作为一等公民来处理,实现代码的抽象和复用。
例如,下面的代码定义了一个高阶函数map
,它接受一个函数和一个数组作为参数,返回一个新的数组,其中每个元素都是原数组中对应元素经过函数处理后的结果。
const map = (f, arr) => arr.map(f); const square = x => x * x; const numbers = [1, 2, 3, 4, 5]; console.log(map(square, numbers)); // [1, 4, 9, 16, 25] 复制代码
闭包
闭包
是指函数和其相关的引用环境组合而成的实体。它可以实现数据的封装和隐藏,避免全局变量的污染和冲突。
例如,下面的代码定义了一个闭包,它返回一个函数,每次调用时都会累加传入的参数,并返回累加后的结果。
const add = (() => { let sum = 0; return x => { sum += x; return sum; }; })(); console.log(add(1)); // 1 console.log(add(2)); // 3 console.log(add(3)); // 6 复制代码
惰性求值
惰性求值
是指在需要时才进行计算,避免不必要的计算和资源浪费。它可以实现懒加载和缓存等优化策略,提高代码的性能和效率。
例如,下面的代码定义了一个惰性求值函数,它接受一个函数作为参数,返回一个新的函数,每次调用时都会检查是否已经计算过结果,如果没有则进行计算并缓存结果,提高代码的性能和效率。
const memoize = fn => { const cache = new Map(); return (...args) => { const key = JSON.stringify(args); const val = cache.get(key); if (val) { return val; } const res = fn(...args); cache.set(key, res); return res; }; }; const factorial = memoize(n => { if (n === 0) { return 1; } return n * factorial(n - 1); }); console.log(factorial(5)); // 120 console.log(factorial(5)); // 120 (返回缓存数据) 复制代码
虽然Rust
的设计灵感来源于很多现存的语言和技术。但是其中一个显著的影响就是 {函数式编程|functional programming}。
下面,我们就针对一些特性来展开说明
闭包:可以捕获环境的匿名函数
Rust
的 闭包(closures
)是可以保存进变量或作为参数传递给其他函数的匿名函数。
可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。
- 不同于函数,闭包允许捕获调用者作用域中的值。
使用闭包创建行为的抽象
假定存在如下场景:我们在一个通过 app
生成自定义健身计划的初创企业工作。其后端使用 Rust
编写,而生成健身计划的算法需要考虑很多不同的因素,比如用户的年龄、身体质量指数、用户喜好、最近的健身活动和用户指定的强度系数。
将通过调用 simulated_expensive_calculation
函数来模拟调用假定的算法。
use std::thread; use std::time::Duration; fn simulated_expensive_calculation(intensity: u32) -> u32 { println!("计算中..."); thread::sleep(Duration::from_secs(2)); intensity } 复制代码
main
函数中将会包含健身 app
中的重要部分。
所需的输入有这些:
- 一个来自用户的
intensity
数字,请求健身计划时指定,它代表用户喜好低强度还是高强度健身。 - 一个随机数,其会在健身计划中生成变化。
fn main() { let simulated_user_specified_value = 10; let simulated_random_number = 7; generate_workout( simulated_user_specified_value, simulated_random_number ); } 复制代码
generate_workout
函数包含我们最关心的 app
业务逻辑。
fn generate_workout(intensity: u32, random_number: u32) { if intensity < 25 { println!( "今天做 {} 俯卧撑!", simulated_expensive_calculation(intensity) ); println!( "接下来做 {} 仰卧起坐!", simulated_expensive_calculation(intensity) ); } else { if random_number == 3 { println!("休息一会"); } else { println!( "今天运动了 {} 分钟!", simulated_expensive_calculation(intensity) ); } } } 复制代码
代码有多处调用了慢计算函数simulated_expensive_calculation
。第一个 if
块调用了 simulated_expensive_calculation
两次, else
中的 if
没有调用它,而第二个 else
中的代码调用了它一次。
为了简化更新步骤,我们将重构代码来让它只调用 simulated_expensive_calculation
一次。同时还希望去掉目前多余的连续两次函数调用,并不希望在计算过程中增加任何其他此函数的调用。也就是说,我们不希望在完全无需其结果的情况调用函数,不过仍然希望只调用函数一次。
使用函数重构
尝试的是将重复的 simulated_expensive_calculation
函数调用提取到一个变量中。
fn generate_workout(intensity: u32, random_number: u32) { let expensive_result = simulated_expensive_calculation(intensity); if intensity < 25 { println!( "今天做 {} 俯卧撑!", expensive_result ); println!( "接下来做 {} 仰卧起坐!", expensive_result ); } else { if random_number == 3 { println!("休息一会"); } else { println!( "今天运动了 {} 分钟!", expensive_result ); } } } 复制代码
将 simulated_expensive_calculation
调用提取到一个位置,并将结果储存在变量 expensive_result
中。
这个修改统一了 simulated_expensive_calculation
调用并解决了第一个 if
块中不必要的两次调用函数的问题。不幸的是,现在所有的情况下都需要调用函数并等待结果,包括那个完全不需要这一结果的内部 if
块。
希望能够在程序的一个位置指定某些代码,并只在程序的某处实际需要结果的时候执行这些代码。这正是闭包的用武之地!
重构使用闭包储存代码
不同于总是在 if
块之前调用 simulated_expensive_calculation
函数并储存其结果,我们可以定义一个闭包并将其储存在变量中。实际上可以选择将整个 simulated_expensive_calculation
函数体移动到这里引入的闭包中:
let expensive_closure = |num| { println!("计算中..."); thread::sleep(Duration::from_secs(2)); num }; 复制代码
闭包定义是
expensive_closure
赋值的=
之后的部分。闭包的定义以一对竖线(|
)开始,在竖线中指定闭包的参数。
这个闭包有一个参数 num
;如果有多于一个参数,可以使用逗号分隔,比如 |param1, param2|
。
参数之后是存放闭包体的大括号 —— 如果闭包体只有一行则大括号是可以省略的。大括号之后闭包的结尾,需要用于 let
语句的分号。因为闭包体的最后一行没有分号,所以闭包体(num
)最后一行的返回值作为调用闭包时的返回值 。
这个 let
语句意味着 expensive_closure
包含一个匿名函数的定义,不是调用匿名函数的返回值。
使用闭包的原因是我们需要在一个位置定义代码,储存代码,并在之后的位置实际调用它;
期望调用的代码现在储存在 expensive_closure
中。
定义了闭包之后,可以改变 if
块中的代码来调用闭包以执行代码并获取结果值。调用闭包类似于调用函数;指定存放闭包定义的变量名并后跟包含期望使用的参数的括号。
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!( "今天做 {} 俯卧撑!", expensive_closure(intensity) ); println!( "接下来做 {} 仰卧起坐!", expensive_closure(intensity) ); } else { if random_number == 3 { println!("休息一会"); } else { println!( "今天运动了 {} 分钟!", expensive_closure(intensity) ); } } } 复制代码
现在耗时的计算只在一个地方被调用,并只会在需要结果的时候执行该代码。
闭包类型推断和标注
闭包不要求像 fn
函数那样在参数和返回值上注明类型。函数中需要类型标注是因为他们是暴露给用户的显式接口的一部分。严格的定义这些接口对于保证所有人都认同函数使用和返回值的类型来说是很重要的。但是闭包并不用于这样暴露在外的接口:他们储存在变量中并被使用,不用命名他们或暴露给库的用户调用。
类似于变量,可以选择增加类型标注。
let expensive_closure = |num: u32| -> u32 { println!("计算中..."); 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 ; 复制代码
第一行展示了一个函数定义,而第二行展示了一个完整标注的闭包定义。第三行闭包定义中省略了类型标注,而第四行去掉了可选的大括号,因为闭包体只有一行。这些都是有效的闭包定义,并在调用时产生相同的行为。
闭包定义会为每个参数和返回值推断一个具体类型
尝试调用闭包两次,第一次使用 String
类型作为参数而第二次使用 u32
,则会得到一个错误
let example_closure = |x| x; let s = example_closure(String::from("hello")); let n = example_closure(5); 复制代码
尝试调用一个被推断为两个不同类型的闭包。
第一次使用 String
值调用 example_closure
时,编译器推断 x
和此闭包返回值的类型为 String
。接着这些类型被锁定进闭包 example_closure
中,如果尝试对同一闭包使用不同类型则会得到类型错误。
使用带有泛型和 Fn trait 的闭包
创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。你可能见过这种模式被称 memoization
或 lazy evaluation
(惰性求值)。
为了让结构体存放闭包,我们需要指定闭包的类型,因为结构体定义需要知道其每一个字段的类型。每一个闭包实例有其自己独有的匿名类型:也就是说,即便两个闭包有着相同的签名,他们的类型仍然可以被认为是不同。
Fn
系列trait
由标准库提供。所有的闭包都实现了trait
Fn
、FnMut
或FnOnce
中的一个。
为了满足 Fn trait bound
我们增加了代表闭包所必须的参数和返回值类型的类型。在例子中,闭包有一个 u32
的参数并返回一个 u32
,这样所指定的 trait bound
就是 Fn(u32) -> u32
。
struct Cacher<T> where T: Fn(u32) -> u32 { calculation: T, value: Option<u32>, } 复制代码
结构体 Cacher
有一个泛型 T
的字段 calculation
。T
的 trait bound
指定了 T
是一个使用 Fn
的闭包。任何我们希望储存到 Cacher
实例的 calculation
字段的闭包必须有一个 u32
参数(由 Fn
之后的括号的内容指定)并必须返回一个 u32
(由 ->
之后的内容)。
字段 value
是 Option<u32>
类型的。在执行闭包之前,value
将是 None
。如果使用 Cacher
的代码请求闭包的结果,这时会执行闭包并将结果储存在 value
字段的 Some
成员中。接着如果代码再次请求闭包的结果,这时不再执行闭包,而是会返回存放在 Some
成员中的结果。
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::new
函数获取一个泛型参数 T
,它定义于 impl
块上下文中并与 Cacher
结构体有着相同的 trait bound
。Cacher::new
返回一个在 calculation
字段中存放了指定闭包和在 value
字段中存放了 None
值的 Cacher
实例,因为我们还未执行闭包。
当调用代码需要闭包的执行结果时,不同于直接调用闭包,它会调用 value
方法。这个方法会检查 self.value
是否已经有了一个 Some
的结果值;如果有,它返回 Some
中的值并不会再次执行闭包。
如果 self.value
是 None
,则会调用 self.calculation
中储存的闭包,将结果保存到 self.value
以便将来使用,并同时返回结果值。
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!( "今天做 {} 俯卧撑!", expensive_result.value(intensity) ); println!( "接下来做 {} 仰卧起坐!", expensive_result.value(intensity) ); } else { if random_number == 3 { println!("休息一下"); } else { println!( "今天运动了 {} 分钟!", expensive_result.value(intensity) ); } } } 复制代码
不同于直接将闭包保存进一个变量,我们保存一个新的 Cacher
实例来存放闭包。接着,在每一个需要结果的地方,调用 Cacher
实例的 value
方法。可以调用 value
方法任意多次,或者一次也不调用,而慢计算最多只会运行一次。
闭包会捕获其环境
闭包还有另一个函数所没有的功能:他们可以捕获其环境并访问其被定义的作用域的变量。
有一个储存在 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
,因为它与 equal_to_x
定义于相同的作用域。
函数则不能做到同样的事,它并不能编译。
fn main() { let x = 4; fn equal_to_x(z: i32) -> bool { z == x } let y = 4; assert!(equal_to_x(y)); } 复制代码
当闭包从环境中捕获一个值,闭包会在闭包体中储存这个值以供使用。这会使用内存并产生额外的开销
闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式:
- 获取所有权
- 可变借用
- 不可变借用
这三种捕获值的方式被编码为如下三个 Fn trait
:
FnOnce
消费从周围作用域捕获的变量,闭包周围的作用域被称为其 {环境|environment}。为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的Once
部分代表了闭包不能多次获取相同变量的所有权的事实,所以它只能被调用一次。FnMut
获取可变的借用值所以可以改变其环境Fn
从其环境获取不可变的借用值
当创建一个闭包时,Rust
根据其如何使用环境中变量来推断我们希望如何引用环境。由于所有闭包都可以被调用至少一次,所以所有闭包都实现了 FnOnce
。那些并没有移动被捕获变量的所有权到闭包内的闭包也实现了 FnMut
,而不需要对被捕获的变量进行可变访问的闭包则也实现了 Fn
。
使用迭代器处理元素序列
{迭代器|iterator}是一种设计模式,它提供了一种遍历集合元素的统一接口,无需暴露集合的内部结构。
迭代器可以按照不同的顺序遍历集合元素,也可以实现惰性求值和流式处理等特性,提高代码的可读性、可维护性和可扩展性。
JS中的迭代器
在JS中,迭代器是通过Symbol.iterator
接口实现的。它可以被for...of
循环、扩展运算符、解构赋值等语法所使用,也可以被自定义的迭代器函数所使用。
下面的代码定义了一个迭代器函数,它接受一个数组作为参数,返回一个迭代器对象,可以按照顺序遍历数组元素。
const createIterator = arr => { let i = 0; return { next: () => { if (i < arr.length) { return { value: arr[i++], done: false }; } else { return { value: undefined, done: true }; } } }; }; const numbers = [1, 2, 3, 4, 5]; const iterator = createIterator(numbers); console.log(iterator.next()); // { value: 1, done: false } console.log(iterator.next()); // { value: 2, done: false } console.log(iterator.next()); // { value: 3, done: false } console.log(iterator.next()); // { value: 4, done: false } console.log(iterator.next()); // { value: 5, done: false } console.log(iterator.next()); // { value: undefined, done: true } 复制代码
在 Rust
中,迭代器是 {惰性的|lazy},这意味着在调用方法使用迭代器之前它都不会有效果。
通过调用定义于 Vec
上的 iter
方法在一个 vector v1
上创建了一个迭代器。这段代码本身没有任何用处:
let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); 复制代码
迭代器被储存在
v1_iter
变量中,而这时没有进行迭代。
一旦 for
循环开始使用 v1_iter
,接着迭代器中的每一个元素被用于循环的一次迭代,这会打印出其每一个值:
let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("值为: {}", val); } 复制代码
在标准库中没有提供迭代器的语言中,我们可能会使用一个从 0 开始的索引变量,使用这个变量索引 vector
中的值,并循环增加其值直到达到 vector
的元素数量。
迭代器为我们处理了所有这些逻辑,这减少了重复代码并消除了潜在的混乱。另外,迭代器的实现方式提供了对多种不同的序列使用相同逻辑的灵活性。
Iterator trait 和 next 方法
迭代器都实现了一个叫做 Iterator
的定义于标准库的 trait
。
pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } 复制代码
这段代码表明实现 Iterator trait
要求同时定义一个 Item
类型,这个 Item
类型被用作 next
方法的返回值类型。换句话说,Item
类型将是迭代器返回元素的类型。
next
是 Iterator
实现者被要求定义的唯一方法。next
一次返回迭代器中的一个项,封装在 Some
中,当迭代器结束时,它返回 None
。
可以直接调用迭代器的 next
方法。
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); } 复制代码
v1_iter
需要是可变的:在迭代器上调用 next
方法改变了迭代器中用来记录序列位置的状态。换句话说,代码 {消费|consume}了,或使用了迭代器。每一个 next
调用都会从迭代器中消费一个项。使用 for
循环时无需使 v1_iter
可变因为 for
循环会获取 v1_iter
的所有权并在后台使 v1_iter
可变。
消费迭代器的方法
Iterator trait
有一系列不同的由标准库提供默认实现的方法;你可以在 Iterator trait
的标准库 API
文档中找到所有这些方法。一些方法在其定义中调用了 next
方法,这也就是为什么在实现 Iterator trait
时要求实现 next
方法的原因。
这些调用 next
方法的方法被称为 {消费适配器|consuming adaptors},因为调用他们会消耗迭代器。一个消费适配器的例子是 sum
方法。这个方法获取迭代器的所有权并反复调用 next
来遍历迭代器,因而会消费迭代器。当其遍历每一个项时,它将每一个项加总到一个总和并在迭代完成时返回总和。
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
因为调用 sum
时它会获取迭代器的所有权。
产生其他迭代器的方法
Iterator trait
中定义了另一类方法,被称为 {迭代器适配器|iterator adaptors},他们允许我们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。
如下展示了一个调用迭代器适配器方法 map
的例子,该 map
方法使用闭包来调用每个元素以生成新的迭代器。 这里的闭包创建了一个新的迭代器,对其中 vector
中的每个元素都被加 1。不过这些代码会产生一个警告:
let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); 复制代码
得到的警告是:
warning: unused `std::iter::Map` which must be used: iterator adaptors are lazy and do nothing unless consumed --> src/main.rs:4:5 | 4 | v1.iter().map(|x| x + 1); | ^^^^^^^^^^^^^^^^^^^^^^^^^ 复制代码
警告提醒了我们:迭代器适配器是惰性的,而这里我们需要消费迭代器。
为了修复这个警告并消费迭代器获取有用的结果,我们使用 collect
方法。这个方法消费迭代器并将结果收集到一个数据结构中。
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]); 复制代码
map
获取一个闭包,可以指定任何希望在遍历的每个元素上执行的操作。
使用闭包获取环境
让我们展示一个通过使用 filter
迭代器适配器和捕获环境的闭包的用例。迭代器的 filter
方法获取一个使用迭代器的每一个项并返回布尔值的闭包。
- 如果闭包返回
true
,其值将会包含在filter
提供的新迭代器中。 - 如果闭包返回
false
,其值不会包含在结果迭代器中
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() } #[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_my_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
函数获取一个鞋子 vector
的所有权和一个鞋子大小作为参数。它返回一个只包含指定大小鞋子的 vector
。
shoes_in_my_size
函数体中调用了 into_iter
来创建一个获取 vector
所有权的迭代器。接着调用 filter
将这个迭代器适配成一个只含有那些闭包返回 true
的元素的新迭代器。
实现 Iterator trait 来创建自定义迭代器
可以实现 Iterator trait
来创建任何我们希望的迭代器。定义中唯一要求提供的方法就是 next
方法。一旦定义了它,就可以使用所有其他由 Iterator trait
提供的拥有默认实现的方法来创建自定义迭代器了!
让我们创建一个只会从 1 数到 5 的迭代器。首先,创建一个结构体来存放一些值,接着实现 Iterator trait
将这个结构体放入迭代器中并在此实现中使用其值。
struct Counter { count: u32, } impl Counter { fn new() -> Counter { Counter { count: 0 } } } 复制代码
Counter
结构体有一个字段 count
。这个字段存放一个 u32
值,它会记录处理 1 到 5 的迭代过程中的位置。count
是私有的因为我们希望 Counter
的实现来管理这个值。new
函数通过总是从为 0
的 count
字段开始新实例来确保我们需要的行为。
为 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 } } } 复制代码
将迭代器的关联类型Item
设置为 u32
,意味着迭代器会返回 u32
值集合。
一旦实现了 Iterator trait
,我们就有了一个迭代器!如下展示了一个测试用来演示使用 Counter
结构体的迭代器功能,通过直接调用 next
方法。
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); } 复制代码
后记
分享是一种态度。
参考资料:《Rust权威指南》
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。