Technology Sharing

Rust Programming Language Learning - Functional Language Features: Iterators and Closures

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Rust Closures are anonymous functions that can be stored in a variable or passed as arguments to other functions. Closures can be created in one place and then executed in different contexts. Unlike functions, closures allow you to capture values ​​in the scope in which they are defined.

Iterators are responsible for traversing each item in a sequence and the logic of deciding when the sequence ends. When using iterators, we don't need to reimplement this logic.

1. Closure

A closure is a function-like structure that can be stored in a variable.

1.1 Closures capture their environment

In Rust, closures are anonymous functions that can capture variables from the outer environment. The capture behavior of a closure depends on the types of the variables and how the closure uses those variables.

By Value

When a closure captures a variable by value, it takes ownership of that variable. This means that after the closure is created, the original variable cannot be used anymore.

fn main() {
    let text = "Hello".to_string();
    // 使用 move 来显式地表示闭包将获取 text 的所有权
    let closure = move || println!("{}", text);
    // 这里 text 不能被使用,因为其所有权已经被闭包获取
    // println!("{}", text); // 这将导致编译错误
    closure(); // 打印 "Hello"
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Capture by Reference

When a closure captures a variable by reference, it borrows that variable. This means that the original variable is still available, but the closure can only borrow it and cannot take ownership.

fn main() {
    let text = "Hello";
    // 闭包通过引用捕获 text
    let closure = || println!("{}", text);
    // text 仍然可用,因为它没有被移动
    println!("{}", text); // 打印 "Hello"
    closure(); // 再次打印 "Hello"
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Mutable Capture

A closure can capture a mutable reference, allowing it to modify the value of the original variable.

fn main() {
    let mut count = 0;
    // 闭包通过可变引用捕获 count
    let mut closure = || {
        count += 1; // 修改 count 的值
        println!("Count: {}", count);
    };
    closure(); // 打印 "Count: 1"
    closure(); // 打印 "Count: 2"
    // count 的值现在是 2
    println!("Final count: {}", count);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

If you remove the closure mut Modification, compilation failed.

fn main() {
    let mut count = 0;
    // 闭包通过可变引用捕获 count
    let closure = || {
        count += 1; // 修改 count 的值
        println!("Count: {}", count);
    };
    closure(); // 打印 "Count: 1"
    closure(); // 打印 "Count: 2"
    // count 的值现在是 2
    println!("Final count: {}", count);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

Error message: Due to borrowing mutable count,transfer closure Requires mutable binding.

   Compiling playground v0.0.1 (/playground)
error[E0596]: cannot borrow `closure` as mutable, as it is not declared as mutable
 --> src/main.rs:4:9
  |
4 |     let closure = || {
  |         ^^^^^^^ not mutable
5 |         count += 1; // 修改 count 的值
  |         ----- calling `closure` requires mutable binding due to mutable borrow of `count`
...
8 |     closure(); // 打印 "Count: 1"
  |     ------- cannot borrow as mutable
9 |     closure(); // 打印 "Count: 2"
  |     ------- cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
4 |     let mut closure = || {
  |         +++

For more information about this error, try `rustc --explain E0596`.
error: could not compile `playground` (bin "playground") due to 1 previous error
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

Closures as parameters

Closures can be used asParameter passingTo other functions, capture variables in the environment.

fn main() {
    // 创建一个整数变量
    let number = 10;

    // 创建一个闭包,它接受一个 i32 类型的参数并返回其平方
    // 这里使用 || 表示这是一个闭包
    let square = || number * number;

    // 定义一个函数,它接受一个闭包作为参数并调用它
    // 闭包作为参数需要指定其类型,这里使用 || -> i32 表示闭包没有参数并返回 i32 类型的值
    fn call_closure<F>(f: F)
    where
        F: Fn() -> i32, // 使用 trait bound 指定闭包的签名
    {
        // 调用闭包并打印结果
        let result = f();
        println!("The result is: {}", result);
    }

    // 调用 `call_closure` 函数,并将闭包 `square` 作为参数传递
    // 由于闭包 `square` 没有参数,我们可以直接传递
    call_closure(square);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

In this example:

  1. We define a closure square, which captures by reference main In the functionnumber variable and calculate its square.
  2. We define a call_closure A function that accepts aFn() -> i32 The closure that takes a trait bound as a parameter, which means that the closure takes no parameters and returns ai32 The value of type.
  3. We call call_closure function, andsquare The closure is passed as a parameter.square Closures have no parameters, they can be passed directly as parameterscall_closure
  4. call_closure The function calls the closure and prints out the result.

1.2 Closure type inference and annotation

There are more differences between functions and closures. Closures do not always require fn Functions are written in the same way that types are annotated on their parameters and return values. Type annotations are needed on functions because they are part of the explicit interface exposed to users. Having a well-defined interface is important to ensure that everyone has a consistent understanding of the types of functions used and returned. Closures, by contrast, do not have such an exposed interface: they are stored in variables and used without naming them or exposing them to users of the library.

Closures are usually short and relevant only in a narrow range of contexts rather than in arbitrary situations. In these limited contexts, the compiler can reliably infer the types of the parameters and return value, similar to how it can infer the types of most variables (there are also rare cases where the compiler needs a closure type annotation).

Similar to variables, we can add type annotations if we want to increase explicitness and clarity, with the downside of making the code more verbose (versus strictly necessary).

fn main() {
    let a = 100;
    let add_one = |x: i32| -> i32 { x + 1 };
    let b = add_one(a);
    println!("{}", b); 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

With type annotations, closure syntax becomes more function-like. Here's a vertical comparison of a function definition that adds one to its parameter and the closure syntax for the same behavior. Some whitespace has been added to align the parts. This shows how similar closure syntax is to function syntax, except for the use of vertical bars and some optional syntax:

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  ;
  • 1
  • 2
  • 3
  • 4

So the above example can be simplified to:

fn main() {
    let a = 100;
    let add_one = |x| x + 1;
    let b = add_one(a);
    println!("{}", b); 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

The compiler infers a concrete type for each parameter and return value in a closure definition.

Let’s look at another example:

fn main() {
    let a = 100i32;
    let a1 = 100f32;
    let closure = |x| x;
    let b = closure(a);
    let b1 = closure(a1);
    println!("{}", b); 
    println!("{}", b1); 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Note that this closure definition does not have any type annotations, so we can call this closure with any type. However, if we try to call the closure twice, the first time with i32 and the second time with f32, we will get an error:Expected, because the closure was called before with an argument of type 'i32'.

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
 --> src/main.rs:6:22
  |
6 |     let b1 = closure(a1);
  |              ------- ^^ expected `i32`, found `f32`
  |              |
  |              arguments to this function are incorrect
  |
note: expected because the closure was earlier called with an argument of type `i32`
 --> src/main.rs:5:21
  |
5 |     let b = closure(a);
  |             ------- ^ expected because this argument is of type `i32`
  |             |
  |             in this closure call
note: closure parameter defined here
 --> src/main.rs:4:20
  |
4 |     let closure = |x| x;
  |                    ^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` (bin "playground") due to 1 previous error
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

1.3 Moving captured values ​​out of closures and the Fn trait

Once a closure captures a reference to or ownership of a value in the environment in which it is defined (thus affecting what gets moved into the closure, if any), the code in the closure body defines what happens to that reference or value later when the closure is evaluated (thus affecting what gets moved out of the closure, if any). The closure body can do any of the following: move a captured value out of the closure, modify a captured value, neither move nor modify the value, or never capture a value from the environment in the first place.

The way a closure captures and processes values ​​from its environment affects the traits that the closure implements. Traits are how functions and structs specify the types of closures they can use. Depending on how the closure body processes values, the closure automatically and incrementally implements one, two, or three traits. Fn trait。

  1. FnOnce Applies to closures that can be called once. All closures implement at least this trait, since all closures can be called. A closure that moves captured values ​​out of its body only implementsFnOnce trait, this is because it can only be called once.
  2. FnMut Suitable for closures that do not move captured values ​​out of the closure body, but may modify the captured values. Such closures can be called multiple times.
  3. Fn Applicable to closures that neither move captured values ​​out of the closure body nor modify captured values, and of course also include closures that do not capture values ​​from the environment. Such closures can be called multiple times without changing their environment, which is very important in scenarios where closures are called multiple times concurrently.

Here is an example showing how to use it in a function FnOnce As a generic constraint, and to ensure that the closure is only called once:

fn call_once<F, T>(f: F) -> T
where
    F: FnOnce() -> T, // 约束 F 为 FnOnce trait,意味着它接受一个空参数并返回 T 类型
{
    f() // 调用闭包并返回结果
}

fn main() {
    // 创建一个闭包,它捕获了 `value` 的所有权
    let value = 42;
    let consume = move || {
        let result = value; // 移动 `value`
        println!("The value is: {}", result);
        result // 返回结果
    };

    // 调用 `call_once` 函数,传入闭包
    let result = call_once(consume);
    println!("Result of the closure: {}", result);

    // 尝试再次使用 `consume` 将会导致编译错误,因为它已经消耗了 `value`
    // call_once(consume);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

operation result

The value is: 42
Result of the closure: 42
  • 1
  • 2

In this example, we define a generic function call_once, which accepts a type F Parametersf,in F Must be achievedFnOnce trait. This meansf is a closure that takes empty arguments and returns a typeT the result of.

exist main In the function, we create a closureconsume, which captures value Then, we callcall_once Function, passed inconsume Closure.call_once The function calls the closure and returns its result.consume The closure has been consumedvalueTry calling again call_once Passing the same closure will result in a compilation error.

Here is a sample using FnMut Example of a trait.

fn apply_mut<F, T>(func: &mut F, num: i32) -> T
where
    F: FnMut(i32) -> T, // F 是一个可变闭包,接受一个 i32 类型的参数并返回类型为 T 的结果
{
    func(num) // 调用闭包并返回结果
}

fn main() {
    let mut count = 0;

    // 创建一个闭包,它接受一个 i32 类型的参数并将其加到 count 上
    let mut increment = |num: i32| -> i32 {
        count += num;
        count
    };

    // 使用 apply_mut 函数和 increment 闭包的引用
    let result: i32 = apply_mut(&mut increment, 5);
    println!("Result after applying increment: {}", result);

    // 再次使用 apply_mut 函数和 increment 闭包的引用
    let result: i32 = apply_mut(&mut increment, 10);
    println!("Result after applying increment again: {}", result);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

operation result

Result after applying increment: 5
Result after applying increment again: 15
  • 1
  • 2

In this example,apply_mut The function accepts aF Mutable references to types&mut F as a parameter (so that,increment The closure will not be moved and can be used multiple times) and ai32 Type Parametersnumwhere The clause specifiesF Must be a realizedFnMut A closure that accepts ai32 Type of parameter and returns a type ofT the result of.main A mutable closure is created in the functionincrement, which modifies a captured variable count, then use apply_mut function to call this closure. Each time it is calledapply_mut Will useincrement closure, andincrement Closures will be modifiedcount The value of .

In Rust,Fn The trait indicates that the closure does not take ownership of the variables it captures and does not mutate them. The following example shows how to define a closure that acceptsFn A function that takes a closure as a parameter and uses it in concurrent scenarios.

use std::thread;

// 定义一个函数,它接受一个实现了 Fn(i32) -> i32 的闭包,并调用它
fn call_once<F, T>(func: F) -> T
where
    F: Fn(i32) -> T, // 指定 F 是一个接受 i32 并返回 T 的 Fn 闭包
{
    let result = func(42); // 调用闭包,传入一个 i32 类型的值
    result // 返回闭包的执行结果
}

fn main() {
    // 定义一个简单的 Fn 闭包,它接受一个 i32 类型的参数并返回两倍的该值
    let double = |x: i32| -> i32 {
        x * 2
    };

    // 创建多个线程,每个线程都使用相同的闭包
    let handles: Vec<_> = (0..5).map(|i| {
        let func = double; // 闭包可以被复制,因为它是 Fn 类型的
        thread::spawn(move || {
            let result = call_once(func); // 调用 call_once 函数,并传入闭包
            println!("Thread {} result: {}", i, result); // 打印结果
        })
    }).collect();

    // 等待所有线程完成
    for handle in handles {
        handle.join().unwrap();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

Not every execution is in this order. Which thread runs first depends on the system's current scheduling.

operation result

Thread 1 result: 84
Thread 2 result: 84
Thread 0 result: 84
Thread 4 result: 84
Thread 3 result: 84
  • 1
  • 2
  • 3
  • 4
  • 5

In this example:

  1. We defined a call_once Function that accepts a generic parameterF,and F Must meetFn(i32) -> T This means thatF is a closure that accepts ai32 Type of parameter and returns a type ofT the result of.
  2. exist main In the function, we define a simple closuredouble, which accepts a i32 Type Parametersx and returnsx * 2 the result of.
  3. We use map Five threads are created, each of which copiesdouble Closure, and call in a new threadcall_once function, passing the closure as a parametercall_once
  4. In each thread,call_once The function is called, the closure is executed, and the result is printed.
  5. Finally, we use join Method waits for all threads to complete.

2. Iterator

Iterators allow you to do something with the items in a sequence. The following mainly introduces the use of iterators to process element sequences and the performance comparison of loops VS iterators. In Rust, iterators are lazy, which means that no operation is performed until a method that consumes the iterator is called.

2.1 Iterator trait and next method

Iterators all implement a Iterator is defined in the standard library as a trait. The trait definition looks like this:

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
    // 此处省略了方法的默认实现
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

type Item andSelf::Item, which define the associated type of the trait. But for now, just know that this code shows that the implementation Iterator The trait requires that aItem Type, thisItem Type is used asnext The return type of the method. In other words,Item The type will be the type of elements returned by the iterator.

next yesIterator The only method implementors are required to define.next Returns one item in the iterator at a time, wrapped inSome When the iterator is finished, it returnsNone

2.2 Methods for consuming iterators

Iterator The trait has a number of different methods with default implementations provided by the standard library; you canIterator All of these methods can be found in the standard library API documentation for the trait. Some methods callnext method, which is why in the implementationIterator Trait requires implementationnext The reason for the method.

These calls next Methods that call iterators are called consuming adaptors because calling them consumes the iterator.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    // 使用into_iter()将Vec转换为消费迭代器
    let numbers_iter = numbers.iter();
    let mut sum: i32 = numbers_iter
        // 使用sum适配器计算迭代器中所有元素的总和
        .sum();

    // 打印总和
    println!("The sum is: {}", sum);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

If you use it again numbers_iter It will report an error.sum method takes ownership of the iterator and calls it repeatedlynext to iterate over the iterator, thereby consuming the iterator. As it iterates over each item, it adds each item to a sum and returns the sum when the iteration is complete.sum No longer allowed to usenumbers_iter, because calling sum It takes ownership of the iterator.

2.3 Methods for generating other iterators

Iterator Another class of methods defined in the trait are called iterator adaptors, which allow us to change the current iterator into a different type of iterator. Multiple iterator adapters can be chained together. However, since all iterators are lazy, a consumer adapter method must be called in order to get the result of the iterator adapter call.

fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];
    let v2: Vec<_> = v1.iter().map(|x| x * x).collect();
    assert_eq!(v2, vec![1, 4, 9]);
}
  • 1
  • 2
  • 3
  • 4
  • 5

The collect method consumes iterators and collects the results into a data structure. map Takes a closure that specifies any actions you wish to perform on each element in the traversal.

2.4 Using closures that capture their environment

Many iterator adapters accept closures as arguments, and typically the closure specified as an argument to an iterator adapter will be a closure that captures its environment.

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

    // 使用into_iter()将Vec转换为消费迭代器
    let filtered_and_squared: Vec<i32> = numbers.into_iter()
        // 使用filter适配器过滤元素,接受一个闭包作为参数
        // 闭包捕获其环境,这里指numbers的元素
        .filter(|&x| x % 2 == 0) // 保留偶数
        // 使用map适配器对过滤后的元素进行变换,也接受一个闭包
        .map(|x| x * x) // 对每个元素进行平方
        // 使用collect适配器将结果收集到一个新的Vec中
        .collect();

    // 打印过滤和平方后的结果
    println!("The filtered and squared numbers are: {:?}", filtered_and_squared);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

operation result

The filtered and squared numbers are: [4, 16]
  • 1

In this example,filter andmap are iterator adapters that accept closures as parameters. These closures can capture their environment, i.e. the elements in the iterator.filter In the adapter, closure|&x| x % 2 == 0 It is used to check whether the element is an even number, if so, the element will be retained.map In the adapter, closure|x| x * x is used to square each element. Since these adapters are consuming, they consume the original iterator and cannot be used again. Finally,collect The adapter collects the processed elements into a newVec middle.

2.5 Loop VS Iterator Performance Comparison

Iterators, as a high-level abstraction, are compiled into code that is roughly consistent with the low-level code written by hand. Iterators are one of Rust's zero-cost abstractions, which means that the abstraction does not introduce runtime overhead.

fn main() {
    let numbers1 = (0..1000000).collect::<Vec<i64>>();
    let numbers2 = (0..1000000).collect::<Vec<i64>>();

    let mut sum1 = 0i64;
    let mut sum2 = 0i64;

    // 测量for循环的性能
    let start = std::time::Instant::now();
    for val in numbers1 {
        sum1 += val;
    }
    let loop_duration = start.elapsed();

    // 测量迭代器的性能
    let start = std::time::Instant::now();
    for val in numbers2.iter() {
        sum2 += val;
    }
    let iterator_duration = start.elapsed();

    println!("Iterator took: {:?}", iterator_duration);
    println!("For loop took: {:?}", loop_duration);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

operation result

Iterator took: 12.796012ms
For loop took: 11.559512ms

Iterator took: 12.817732ms
For loop took: 11.687655ms

Iterator took: 12.75484ms
For loop took: 11.89468ms

Iterator took: 12.812022ms
For loop took: 11.785106ms

Iterator took: 12.78293ms
For loop took: 11.528941ms
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

From this example you can see that iterators are still slightly slower.

Reference Links

  1. Rust official website:https://www.rust-lang.org/zh-CN
  2. Rust official documentation:https://doc.rust-lang.org/
  3. Rust Play:https://play.rust-lang.org/
  4. The Rust Programming Language