Skip to content

前言

前面的文章中我们讲了几个 Rust 的基础概念,本篇文章主要讲讲 Rust 的闭包,让我们来进一步去学习 Rust 中的核心知识点吧。

闭包是什么?

在Rust中,闭包是一种特殊的函数类型,允许你在代码中定义匿名函数。闭包是一种可以捕获上下文变量的函数,它可以在定义时访问定义范围内的变量,并且可以在之后被调用多次。

闭包能干什么?

  1. 灵活性: 闭包允许你在需要时直接定义和使用函数,而无需为其命名。
  2. 捕获上下文:闭包可以捕获其定义范围内的变量,使其可以在闭包内部使用,甚至在离开定义范围后仍然有效。
  3. 函数指针替代:闭包可以用作函数指针的替代品,方便地传递和使用。

闭包和函数的区别

  1. 语法:闭包的语法更为灵活,可以直接在代码中定义,而函数需要使用fn关键字进行声明。
  2. 捕获上下文: 闭包可以捕获其定义范围内的变量,函数则需要显式传递参数。
  3. 匿名性: 闭包可以是匿名的,而函数总是有一个名字。

闭包有哪些类型?

在Rust中,有三种主要的闭包类型:

  1. Fn: 闭包可以访问其闭包环境中的值,并对其进行只读操作,它是多线程安全的。
  2. FnMut: 闭包可以访问其闭包环境中的值,并对其进行读写操作,它是非多线程安全的,需要做一些限制。
  3. FnOnce: 闭包只能从其闭包环境中获取值一次,通常在闭包执行后会消费这些值。

包含关系: FnOnce 包含 FnMut、FnMut 包含 Fn。对应 Trait 的源码是这样的:

rust
pub trait Tuple {}
pub trait FnOnce<Args: Tuple> {
    type Output;
    fn call_once(self, args: Args) -> Self::Output;
}

pub trait FnMut<Args: Tuple>: FnOnce<Args> {
    fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait Fn<Args: Tuple>: FnMut<Args> {
    fn call(&self, args: Args) -> Self::Output;
}

也就是说如果你把一个闭包的类型声明为 FnMut,那么它也能同时接收 Fn 的闭包。

闭包性能怎样?

Rust 闭包的性能非常好,比函数指针都要快,别的语言使用闭包一般都需要在堆中分配内存并且进行垃圾回收,而 Rust 不会把闭包分配到堆内存中,除非你把它放到 Box 或其它堆内存容器中,Rust 闭包在被调用时会直接进行内联以此来提升执行性能(可以消除函数调用的开销)。Rust 的闭包会在运行时就确定类型,而比如 JavaScript 的闭包会在运行时才确定类型,这可以减少运行时的性能损耗。

闭包的使用例子

rust
fn main() {
    let x = 5;
    let y = 10;

    // Fn 闭包,只读访问上下文变量
    let read_only = |num: i32| -> i32 { num + x };
    // Fn Closure Result: 15
    println!("Fn Closure Result: {}", read_only(y));

    // FnMut 闭包,可读写访问上下文变量
    let mutable = |num: i32| -> i32 { x + y + num };
    // FnMut Closure Result: 30
    println!("FnMut Closure Result: {}", mutable(15));

    // FnOnce 闭包,使用 move 在闭包内获取上下文变量所有权(Ownership)
    let owned = move |num: i32| -> i32 { num * y };
    // FnOnce Closure Result: 50
    println!("FnOnce Closure Result: {}", owned(x));

    // 尝试访问已经被 FnOnce 闭包获取所有权的变量,这会导致编译错误
    // println!("Trying to use x after FnOnce: {}", x);
}

我们定义了一个使用不同类型闭包的示例:

  • Fn 闭包对上下文变量进行只读访问。
  • FnMut 闭包对上下文变量进行读写访问。
  • FnOnce 闭包获取上下文变量的所有权(Ownership),并在闭包内部消费了这个所有权。

闭包的所有权

  • 一个没有修改外部变量的非 move 闭包既能进行 Copy 也能进行 Clone,不会发生所有权转移
  • 会修改外部值的非 move 闭包既不能 Copy,也不能进行 Clone,当闭包变量赋值给另一个变量的时候,会发生所有权的转移
  • 对于 move 闭包,如果闭包内捕获的内容都能 Copy,那闭包也能 Copy,如果闭包内捕获的内容都能 Clone,那闭包也能 Clone。

闭包实战

rust
use std::collections::HashMap;

struct Request {
    method: String,
    url: String,
    body: Vec<u8>,
}

struct Response {
    status_code: u32,
    body: Vec<u8>,
}

// 用于存放 Rust 闭包,使用动态派发,即在运行时确定具体的闭包类型
type BoxedCallback = Box<dyn Fn(&Request) -> Response>;

struct Router {
    routes: HashMap<String, BoxedCallback>,
}

impl Router {
    fn new() -> Self {
        Router {
            routes: HashMap::new(),
        }
    }

    // 可以添加一个路由,接收一个闭包。
    fn add_route<C>(&mut self, url: &str, callback: C)
    where
        C: Fn(&Request) -> Response + 'static,
    {
        self.routes.insert(url.to_string(), Box::new(callback));
    }

    // 用来发送模拟请求
    fn handle_request(&self, request: &Request) -> Response {
        match self.routes.get(&request.url) {
            // 找到了对应的方法,进行调用
            Some(callback) => callback(request),
            // 找不到对应的方法,返回 404
            None => Response {
                status_code: 404,
                body: vec![],
            },
        }
    }
}

fn main() {
    // 创建一个路由管理器
    let mut router = Router::new();

    // 第二个参数传递一个闭包
    router.add_route("/login", |req| {
        println!(
            "接收到 login 请求,请求体: {:?},请求方法: {}",
            &req.body, req.method
        );
        Response {
            status_code: 200,
            body: vec![],
        }
    });

    let login_request = Request {
        body: vec![],
        method: "POST".to_string(),
        url: "/login".to_string(),
    };

    // 发送一个模拟请求,执行后控制台打印:
    // 接收到 login 请求,请求体: [],请求方法: POST
    router.handle_request(&login_request);
}

总结

闭包是Rust中强大的概念,提供了灵活、高效的函数式编程方式。通过使用不同的闭包类型,你可以根据需要选择只读、可读写或所有权转移的语义。这篇文章把 Rust 中闭包的所有重要内容都列举出来了,希望能对读者们有帮助。码字不易,内容基本都是纯手打的,很多都是自己在学习过程中记录下来的笔记和心得总结,点点赞点点关注就是对我最大的帮助~感谢

每天进步一丢丢