Skip to main content

72 posts tagged with "Rust"

View All Tags

Rust 编程中,模块是用于分割和组织代码的一种重要方式。随着项目规模的扩大,代码复杂度会迅速上升,使用模块可以帮助我们将代码逻辑进行划分,使项目的维护和扩展更加轻松。

本文将全面介绍 如何在 Rust 中创建和管理模块,包括如何创建模块、如何使用模块、以及如何导入模块中的内容。

什么是模块?

模块(Module) 是 Rust 中用来分割和组织代码的基本单元。它可以包含 函数结构体枚举trait 等,甚至模块中还可以嵌套其他模块。通过模块,代码可以变得更有条理、提高可读性,同时方便重用和管理。

模块在 Rust 中的使用类似于其他编程语言的包(package)或命名空间(namespace),但是在 Rust 中的表现形式更加灵活和强大。

模块在大型项目中尤其重要,因为它们帮助开发者清晰地划分项目逻辑,减少耦合,提高代码的可维护性和可读性。

如何在 Rust 中创建模块?

在 Rust 中,可以使用多种方式创建模块,下面我们将介绍三种常见的创建模块的方式:

  1. 使用 mod 关键字直接在文件中声明模块
  2. 将模块定义为单独的文件,文件名即为模块名。
  3. 使用 mod.rs 文件定义目录为一个模块,目录名即为模块名。

1. 使用 mod 关键字创建模块

通过在代码中直接使用 mod 关键字,可以创建一个简单的模块:

mod my_module {
pub fn my_function() {
println!("Hello, world!");
}
}

在上面的示例中,我们定义了一个名为 my_module 的模块,包含了一个名为 my_function 的函数。使用 mod 是最直接的模块定义方式。

2. 使用单独的文件作为模块

在项目中,还可以通过创建一个新的 .rs 文件来定义模块,例如:

// src/my_module.rs
pub fn my_function() {
println!("Hello, world!");
}

在主文件中引用该模块时,只需使用 mod 关键字声明模块:

mod my_module;

fn main() {
my_module::my_function();
}

这样可以使模块代码与其他代码隔离开,增强代码可读性。

3. 使用包含 mod.rs 文件的目录定义模块

还可以通过在某个目录中创建一个 mod.rs 文件来定义该目录为一个模块。目录名即为模块名,例如:

// src/my_module/mod.rs
pub fn my_function() {
println!("Hello, world!");
}

这种方式适用于包含多个文件的大型模块,将不同的逻辑拆分到多个文件中可以提高模块的可维护性。

如何使用模块?

1. 声明模块

在使用模块之前,需要在代码中声明模块。可以在项目的主文件(如 main.rslib.rs)中使用 mod 关键字来声明模块,例如:

mod my_module;

这样 Rust 编译器就知道需要将 my_module 模块包含进来。

2. 导入模块中的内容

为了在其他部分使用模块中的函数、结构体或枚举等,可以通过 use 关键字 来导入模块中的内容:

use my_module::my_function;

fn main() {
my_function();
}

在导入模块中的内容时,需要注意以下几点:

  • 如果要使用模块中的函数、结构体、枚举等内容,需要在模块中使用 pub 关键字 声明它们为公有的。否则,模块内容默认为私有,无法在其他模块中访问。
  • 可以使用 pub use 关键字层层导出模块中的内容,这样可以让其他模块更方便地使用这些内容。

3. 模块路径导入方式

Rust 支持两种模块路径导入方式:绝对路径导入相对路径导入

绝对路径导入

  • 项目内模块:使用 crate 关键字从根模块开始导入内容。
  • 第三方依赖模块:使用 crate_name 后跟 :: 来导入特定的模块。
use crate::my_module::my_function; // 项目内模块
use serde::Serialize; // 第三方依赖模块

相对路径导入

  • 父模块内容:使用 super 关键字导入父模块中的内容。
  • 当前模块内容:使用 self 关键字表示当前模块,也可以省略 self

相对路径导入通常用于测试模块或在子模块中访问父模块的内容。

mod parent_module {
pub mod child_module {
pub fn child_function() {
println!("I'm a child function");
}
}

pub fn parent_function() {
// 使用 super 访问父模块中的内容
super::child_module::child_function();
}
}

Rust 中模块的使用最佳实践

在大型 Rust 项目中,模块的组织和管理是至关重要的。以下是一些关于 Rust 模块使用的最佳实践:

1. 合理拆分模块

当项目代码超过一屏幕时,就应该考虑将其拆分为多个模块。使用模块可以帮助逻辑清晰、降低代码的耦合度,同时提高代码的可读性和维护性。

2. 使用 mod.rs 文件

在大型项目中,使用包含 mod.rs 文件的目录来管理模块可以帮助将不同的逻辑模块化,并以目录的形式组织代码,使项目结构更加清晰。

3. 使用 pub use 导出公共接口

为了让模块中的内容更容易被其他模块访问,可以使用 pub use 关键字将模块中的内容层层导出。例如:

// src/my_module.rs
pub fn internal_function() {
println!("Internal function");
}

// src/lib.rs
mod my_module;
pub use my_module::internal_function;

这样可以避免外部模块使用非常长的路径来引用内容,从而提高代码的简洁性和可读性。

结论

Rust 中,模块的创建与使用主要分为以下几步:

  1. 创建模块

    • 使用 mod my_module { ... } 关键字直接创建模块。
    • 创建一个单独的 .rs 文件作为模块。
    • 使用包含 mod.rs 文件的目录来定义模块。
  2. 使用模块

    • 声明模块,使用 mod my_module;
    • 导入模块中的内容,使用 use my_module::my_function; 或使用 pub use 逐层导出模块内容。
  3. 模块路径导入

    • 绝对路径导入:使用 crate 或第三方库名称来导入模块。
    • 相对路径导入:使用 superself 关键字进行相对路径导入。
模块的正确使用不仅能让代码更加清晰易读,还能显著提升代码的可维护性和复用性。

通过合理地使用模块,开发者可以轻松管理 Rust 项目中的代码结构,减少代码重复,使项目在扩展和维护方面更加灵活。

鱼雪

在Rust的错误处理生态系统中,从标准库的std::error::Erroranyhowthiserrorsnafu, 每个库都在用法和功能上进行了不同程度的改进和演变。 下面是对这些库的改进和设计的详细说明。

标准库的std::error::Error

特点

  • 基础特性:定义了一个通用的错误trait,所有错误类型都可以实现这个trait。
  • 手动实现:需要手动实现DisplayError trait,比较繁琐。

用法示例

use std::fmt;

#[derive(Debug)]
struct MyError {
details: String,
}

impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.details)
}
}

impl std::error::Error for MyError {}

小结

要使用Rust标准库的Error trait实现自定义错误,那么需要做到以下两点:

  • 实现std::fmt::Display trait,以便将错误信息显示给用户。
  • 实现std::error::Error trait,以便将错误信息传递给调用者。

anyhow

该库提供了 anyhow::Error,一种基于特征对象(trait object)的错误类型, 用于在 Rust 应用程序中轻松地进行惯用错误处理。

改进

  • 简化错误处理:提供了一个基于trait对象的通用错误类型anyhow::Error,简化了错误的传播和处理。
  • 上下文信息:可以为错误添加上下文信息,帮助调试。
  • 自动回溯:自动捕获和打印回溯信息(在Rust 1.65及以上)。

用法示例

use anyhow::{Context, Result};

fn read_file(path: &str) -> Result<String> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read file at path: {}", path))?;
Ok(content)
}

小结

anyhow库通过提供anyhow::Error类型,简化了Rust中的错误处理。 它提供了一种简单的方式来处理和传播错误,同时支持添加上下文信息和自动回溯信息。

  • 在anyhow中,经常使用的就是anyhow::Resultanyhow::anyhow
  • 可以使用?符号进行错误传播,同时可以解包未出错的值

thiserror

这个库为标准库的 std::error::Error trait 提供了一个方便的派生宏。

改进

  • 派生宏:通过派生宏简化了自定义错误类型的定义。
  • 自动实现:自动实现DisplayError trait,减少了手动编码的负担。
  • 与其他错误类型集成:可以轻松地将其他错误类型转换为自定义错误类型。

用法示例

use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
#[error("IO error")]
Io(#[from] std::io::Error),
#[error("Parse error")]
Parse(#[from] std::num::ParseIntError),
}

snafu

SNAFU 是一个可以轻松生成错误并向底层错误添加信息的库, 特别是当相同的底层错误类型可能发生在不同的上下文中时。

改进

  • 错误类型生成:通过宏生成错误类型和相关的上下文信息。
  • 上下文支持:更强大的上下文信息支持,提供了详细的错误信息。
  • 源错误追踪:内置对源错误的追踪和显示。

用法示例

use snafu::{Snafu, ResultExt};

#[derive(Debug, Snafu)]
enum MyError {
#[snafu(display("Failed to open file {}: {}", filename, source))]
OpenFile { filename: String, source: std::io::Error },
#[snafu(display("Failed to parse integer: {}", source))]
ParseInt { source: std::num::ParseIntError },
}

fn read_file(path: &str) -> Result<String, MyError> {
let content = std::fs::read_to_string(path).context(OpenFile { filename: path.to_string() })?;
Ok(content)
}

总结

从Rust标准库的std::error::Erroranyhowthiserrorsnafu,经历了一系列的改进和设计:

  • 从手动到自动:从标准库需要手动实现错误处理,到thiserrorsnafu利用宏自动生成代码,减少了开发者的负担。
  • 上下文信息anyhowsnafu增强了对上下文信息的支持,使调试更加容易。
  • 错误传播简化anyhow通过统一的错误类型简化了错误传播,而snafu提供了强大的错误上下文支持。
  • 回溯信息anyhow自动捕获回溯信息,为调试提供了更多有用的信息。

这些改进和设计使得Rust的错误处理更加简洁、高效和易于维护。

参考

鱼雪

thiserror 库为 Rust 的 std::error::Error trait 提供了一个方便的派生宏,使得定义错误类型变得更加简单和高效。下面是对 thiserror 中不同属性的用法和含义的总结,以及它们的使用情况。

Thiserror 思维导图

#[error("...")]

  • 用法: 用于为错误类型或枚举的每个变体提供一个显示格式(Display)。
  • 含义: 定义了当错误被打印时显示的消息格式。
  • 适用情况: 任何需要向用户展示错误信息的场景。

#[from]

  • 用法: 用于自动实现从源错误类型到当前错误类型的 From trait。
  • 含义: 允许一个错误类型自动转换("from")另一个错误类型。
  • 适用情况: 当你想要从一个特定的错误类型自动转换到你定义的错误类型时。这通常用在错误链的上下文中,允许底层错误被包装成更高层的抽象错误。

#[source]

  • 用法: 标记一个字段作为源错误(即导致当前错误的底层错误)。
  • 含义: 该字段会被 Error trait 的 source() 方法返回,用于错误链的追踪。
  • 适用情况: 当你的错误是由另一个错误导致的,并且你想要保留这种因果关系时。这对于调试和错误报告非常有用。

#[backtrace]

  • 用法: 标记一个字段为回溯(backtrace)信息。
  • 含义: 允许捕获和存储错误发生时的调用栈信息。
  • 适用情况: 在需要调试或详细了解错误发生上下文时非常有用。这通常用于复杂系统中,通过回溯可以更容易地定位问题源头。

#[error(transparent)]

  • 用法: 使得当前错误类型在显示和源链处理上透明地代理到内部的错误类型。
  • 含义: 当前错误类型的 Displaysource 方法将直接委托给它包装的错误类型。
  • 适用情况: 当你定义的错误类型仅仅是对另一个错误类型的简单封装,而你不希望添加任何额外信息或行为时。这在定义通用或透明的错误包装时特别有用。

通过这些属性,thiserror 库大大简化了 Rust 中错误处理的复杂性,使得定义丰富而又具有表达力的错误类型变得非常简单。 无论是简单的直接转换,还是更复杂的错误链处理和调试信息捕获,thiserror 都提供了强大而灵活的工具,以支持各种不同的使用场景。 相关信息可以在其官方文档GitHub 仓库中找到。

鱼雪

anyhow库为Rust应用程序提供了一种基于trait对象的错误类型anyhow::Error,以便于进行简便和惯用的错误处理。

Anyhow思维导图

主要特性

  • 简化错误传播:通过使用?操作符,可以轻松地传播实现了std::error::Error trait的任何错误。
  • 上下文添加:允许为错误添加上下文,帮助调试时理解错误发生的具体环节。这是通过Context trait和相关方法(如.context().with_context())实现的。
  • 错误下转型:支持将anyhow::Error下转型为具体的错误类型,以便进行更精确的错误处理或信息获取。
  • 自动捕获回溯信息:在Rust版本≥1.65时,如果底层错误类型没有提供自己的回溯信息,anyhow会自动捕获并打印错误的回溯信息。通过环境变量可以控制回溯信息的显示。
  • 与任何错误类型兼容anyhow可以与任何实现了std::error::Error的错误类型一起工作,不需要特定的derive宏来实现相关trait。
  • 宏支持:提供了几个宏来简化错误处理,例如anyhow!用于创建一个即时的错误消息,bail!用于提前返回一个错误,以及ensure!用于在条件不满足时返回错误。

使用场景

  • 函数返回类型:对于可能失败的函数,推荐使用Result<T, anyhow::Error>(或等价的anyhow::Result<T>)作为返回类型。
  • 错误传播:在函数内部,使用?来简化错误的传播。
  • 添加错误上下文:在可能导致调试困难的低级错误上添加上下文信息,以提供更多关于错误发生时上下文的信息。
  • 处理特定错误:通过错误下转型来处理特定类型的错误。
  • 自定义错误类型:虽然anyhow不直接提供derive宏,但可以与如thiserror库结合使用,来定义和实现自定义错误类型。
  • 即时错误消息:通过anyhow!bail!宏来快速创建和返回错误。

适用性

由于其灵活性和简便性,anyhow库适用于大多数Rust应用程序中的错误处理。它特别适合那些需要简单、直接且灵活处理各种可能错误的应用程序。 对于需要在库中暴露具体错误类型的情况,可能需要结合使用如thiserror之类的库来提供更精细的错误定义和处理。

鱼雪

axum::Router结构体

pub struct Router<S = ()> { /* private fields */}

用于组合处理程序和服务的路由器类型。

实现

impl<S> Router<S>
where
S: Clone + Send + Sync + 'static,

新建路由器

pub fn new() -> Self

创建一个新的路由器,除非您添加额外的路由,否则将对所有请求响应404未找到。

添加另一个路由到路由器

pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self
  • path: 是由/分割的路径段字符串。每个段可能是静态的、捕获的或者是通配符。
  • method_router: 是一个MethodRouter,它将请求方法映射到处理程序。 method_router通常会是类似于get的方法路由器中的处理程序。

静态路径

例如:

  • /
  • /foo
  • /foo/bar

如果传入的请求路径完全匹配,则将调用相应的服务。

捕获

例如:

  • /:key
  • /foo/:key
  • /users/:id/tweets

路径可以包含类似于/:key的段,它匹配任何单个段,并将存储在key处捕获的值。 捕获的值可以是零长度,除了无效路径//

捕获可以使用Path进行提取。

MatchedPath可以用于提取匹配路径,而不是实际路径。

通配符

路径可以以/*key结尾,匹配所有段并捕获的段存储在key中。

例如:

  • /*key
  • /users/*path
  • /:id/:repo/*tree

请注意,/*key 不匹配空段。因此:

  • /*key 不匹配 /,但匹配 /a/a/ 等。
  • /x/*key 不匹配 /x/x/,但匹配 /x/a/x/a/ 等。

还可以使用 Path 来提取通配符捕获。 请注意,不包括前导斜杠,即对于路由 /foo/*rest 和路径 /foo/bar/bazrest 的值将是 bar/baz

接受多种方法

要接受同一路由的多个方法,您可以同时添加所有处理程序。

use axum::{Router, routing::{get, delete}, extract::Path};

let app = Router::new().route(
"/",
get(get_root).post(post_root).delete(delete_root),
);

async fn get_root() {}
async fn post_root() {}
async fn delete_root() {}

或者你也可以一一添加:

let app = Router::new()
.route("/", get(get_root))
.route("/", post(post_root))
.route("/", delete(delete_root));

更多例子

use axum::{Router, routing::{get, delete}, extract::Path};

let app = Router::new()
.route("/", get(root))
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(show_user))
.route("/api/:version/users/:id/action", delete(do_users_action))
.route("/assets/*path", get(serve_asset));

async fn root() {}

async fn list_users() {}

async fn create_user() {}

async fn show_user(Path(id): Path<u64>) {}

async fn do_users_action(Path((version, id)): Path<(String, u64)>) {}

async fn serve_asset(Path(path): Path<String>) {}

Panics

如果路径与另一个路由重叠,则会发生panic

use axum::{routing::get, Router};

let app = Router::new()
.route("/", get(|| async {}))
.route("/", get(|| async {}));

静态路由 /foo 和动态路由 /:key 不被视为重叠,并且 /foo 将优先。 如果路径为空,也会引发 panic。

路由服务

添加另一个路由到路由器调用一个服务

pub fn route_service<T>(self, path: &str, service: T) -> Self
where
T: Service<Request, Error=Infallible> + Clone + Send + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,

示例:

use axum::{
Router,
body::Body,
routing::{any_service, get_service},
extract::Request,
http::StatusCode,
error_handling::HandleErrorLayer,
};
use tower_http::services::ServeFile;
use http::Response;
use std::{convert::Infallible, io};
use tower::service_fn;

let app = Router::new()
.route(
"/",
any_service(service_fn(|_: Request| async {
let res = Response::new(Body::from("Hi from `GET /`"));
}))
)
.route_service(
"/foo",
service_fn(|req: Request| async move {
let body = Body::from(format!("Hi from `{}` /foo", req.method()))
let res = Response::new(body);
Ok::<_, Infallible>(res)
})
)
.route_service(
"/static/Cargo.toml",
ServeFile::new("Cargo.toml"),
);

以这种方式路由到任意服务会对背压(Service::poll_ready)产生复杂性。 有关更多详细信息,请参阅服务路由和背压模块。

由于相同的原因而出现panic,或者尝试将路由到Router时也会发生panic。

use axum::{routing::get, Router};

let app = Router::new().route_service(
"/",
Router::new().route("/foo", get(|| async {})),
);

使用Router::nest替换

在某个路径上嵌套一个路由器。

这样可以将应用程序分解成更小的部分,并将它们组合在一起。

pub fn nest(self, path: &str, router: Router<S>) -> Self

示例:

use axum::{
routing::{get, post},
Router,
};

let user_routes = Router::new().route("/:id", get(|| async {}));
let team_routes = Router::new().route("/", post(|| async {}));

let api_routes = Router::new()
.nest("/users", user_routes)
.nest("/teams", team_routes);

let app = Router::new().nest("/api", api_routes);

// Our app now accepts
// - GET /api/users/:id
// - POST /api/teams

URI如何变化

请注意,嵌套路由将无法看到原始请求URI,而是会剥去匹配的前缀。 这对于像静态文件服务之类的服务工作是必要的。 如果需要原始请求URI,请使·OriginalUri

外部路由的捕获

在使用嵌套动态路由时要小心,因为嵌套还会从外部路由中捕获:

use axum::{
extract::Path,
routing::get,
Router,
};
use std::collections::HashMap;

async fn users_get(Path(params): Path<HashMap<String, String>>) {
// Both `version` and `id` were captured even though `users_api` only
// explicitly captures `id`.
let version = params.get("version");
let id = params.get("id");
}

let users_api = Router::new().route("/users/:id", get(users_get));

let app = Router::new().nest("/:version/api", users_api);

与通配符路由的区别

嵌套路由类似于通配符路由。 不同之处在于通配符路由仍然可以看到整个 URI,而嵌套路由将会去掉前缀:

use axum::{routing::get, http::Uri, Router};

let nested_router = Router::new()
.route("/", get(|uri: Uri| async {
// `uri` will _not_ contain `/bar`
}));

let app = Router::new()
.route("/foo/*rest", get(|uri: Uri| async {
// `uri` will contain `/foo`
}))
.nest("/bar", nested_router);

后备方案

如果嵌套路由器没有自己的回退,则将从外部路由器继承回退:

use axum::{routing::get, http::StatusCode, handler::Handler, Router};

async fn fallback() -> (StatusCode, &'static str) {
(StatusCode::NOT_FOUND, "Not Found")
}

let api_routes = Router::new().route("/users", get(|| async {}));

let app = Router::new()
.nest("/api", api_routes)
.fallback(fallback);

在这里,像 GET /api/not-found 这样的请求将进入 api_routes, 但由于它没有匹配的路由,也没有自己的回退,它将调用外部路由器的回退,即回退功能。 如果嵌套路由器有自己的回退,则外部回退将不会被继承:

use axum::{
routing::get,
http::StatusCode,
handler::Handler,
Json,
Router,
};

async fn fallback() -> (StatusCode, &'static str) {
(StatusCode::NOT_FOUND, "Not Found")
}

async fn api_fallback() -> (StatusCode, Json<serde_json::Value>) {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "status": "Not Found" })),
)
}

let api_routes = Router::new()
.route("/users", get(|| async {}))
.fallback(api_fallback);

let app = Router::new()
.nest("/api", api_routes)
.fallback(fallback);

在这里,像 GET /api/not-found 这样的请求将转到 api_fallback

用状态嵌套路由器

当使用此方法将Router组合时,每个Router必须具有相同类型的状态。 如果您的路由器具有不同类型,您可以使用Router::with_state来提供状态并使类型匹配:

use axum::{
Router,
routing::get,
extract::State,
};

#[derive(Clone)]
struct InnerState {}

#[derive(Clone)]
struct OuterState {}

async fn inner_handler(state: State<InnerState>) {}

let inner_router = Router::new()
.route("/bar", get(inner_handler))
.with_state(InnerState {});

async fn outer_handler(state: State<OuterState>) {}

let app = Router::new()
.route("/", get(outer_handler))
.nest("/foo", inner_router)
.with_state(OuterState {});

请注意,内部路由器仍将继承外部路由器的后备机制。

恐慌

  • 如果路由与另一个路由重叠。有关详细信息,请参阅Router::route
  • 如果路由包含通配符(*)。
  • 如果path为空。

nest类似,但接受任何服务

pub fn nest_service<T>(self, path: &str, service: T) -> Self
where
T: Service<Request, Error=Infallible> + Clone + Send + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,

将两个路由器的路径(path)和回退(fallbacks)合并到一个路由器

pub fn merge<R>(self, other: R) -> Self
where
R: Into<Router<S>>,

这对于将应用程序分成更小的部分并将它们组合成一个非常有用。

use axum::{
routing::get,
Router,
};

let user_routes = Router::new()
.route("/users", get(users_list))
.route("/users/:id", get(users_show));

let team_routes = Router::new()
.route("/teams", get(teams_list));

let app = Router::new()
.merge(user_routes)
.merge(team_routes);

// 也可以执行 `user_routes.merge(team_routes)`

// 我们的应用程序现在接受
// - GET /users
// - GET /users/:id
// - GET /teams

合并路由器的状态

使用此方法合并 Router 时,每个 Router 必须具有相同类型的状态。 如果您的 routers 具有不同类型,可以使用 Router::with_state 来提供状态并使类型匹配:

use axum::{
Router,
routing::get,
extract::State,
};

#[derive(Clone)]
struct InnerState {}

#[derive(Clone)]
struct OuterState {}

async fn inner_handler(state: State<InnerState>) {}

let inner_router = Router::new()
.route("/bar", get(inner_handler))
.with_state(InnerState {});

async fn outer_handler(state: State<OuterState>) {}

let app = Router::new()
.route("/", get(outer_handler))
.merge(inner_router)
.with_state(OuterState {});

合并具有回退的路由器

使用此方法合并 Router 时,后备(fallbacks)也会合并。但是只能有一个路由器有后备

tower::Layer 应用于路由器中的所有路由。

pub fn layer<L>(self, layer: L) -> Router<S>
where
L: Layer<Route> + Clone + Send + 'static,
L::Service: Service<Request> + Clone + Send + 'static,
<L::Service as Service<Request>>::Response: IntoResponse + 'static,
<L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
<L::Service as Service<Request>>::Future: Send + 'static,

这可以用于为一组路由的请求添加额外的处理。

注意,中间件只应用于现有路由。

因此,您必须首先添加您的路由(和/或回退(fallbacks)),然后调用层(layer)。 在调用层之后添加的额外路由将不会添加中间件。

如果要将中间件添加到单个处理程序,可以使用 MethodRouter::layerHandler::layer

示例

添加tower_http::trace::TraceLayer:

use axum::{
routing::get,
Router,
};
use tower_http::trace::TraceLayer;

let app = Router::new()
.route("/foo", get(|| async {}))
.route("/bar", get(|| async {}))
.layer(TraceLayer::new_for_http());

如果您需要编写自己的中间件,请参阅“编写中间件”以获取不同的选项。

如果您只想在某些路由上使用中间件,可以使用Router::merge

use axum::{
routing::get,
Router,
};
use tower_http::{trace::TraceLayer, compression::CompressionLayer};

let with_tracing = Router::new()
.route("/foo", get(|| async {}))
.layer(TraceLayer::new_for_http());

let with_compression = Router::new()
.route("/bar", get(|| async {}))
.layer(CompressionLayer::new());

let app = Router::new()
.merge(with_tracing)
.merge(with_compression);

多中间件

当应用多个中间件时,建议使用tower::ServiceBuilder。有关更多详细信息,请参阅中间件。

路由之后运行

使用此方法添加的中间件将在路由之后运行,因此无法用于重写请求URI。 有关更多详细信息和解决方法,请参见“在中间件中重写请求URI”。

错误处理

请参阅有关错误处理影响中间件的详细信息。

向路由器应用一个 tower::Layer,只有当请求匹配路由时才会运行

pub fn route_layer<L>(self, layer: L) -> Self
where
L: Layer<Route> + Clone + Send + 'static,
L::Service: Service<Request> + Clone + Send + 'static,
<L::Service as Service<Request>>::Response: IntoResponse + 'static,
<L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
<L::Service as Service<Request>>::Future: Send + 'static,

请注意,中间件仅应用于现有路由。 因此,您必须首先添加您的路由(和/或回调),然后在之后调用 route_layer。 调用 route_layer 后添加的额外路由将不会添加中间件。

这与 Router::layer 类似,不同之处在于只有当请求匹配路由时中间件才会运行。 这对于提前返回的中间件非常有用(例如授权), 否则可能会将 404 Not Found 转换为 401 Unauthorized。

示例

use axum::{
routing::get,
Router,
};
use tower_http::validate_request::ValidateRequestHeaderLayer;

let app = Router::new()
.route("/foo", get(|| async {}))
.route_layer(ValidateRequestHeaderLayer::bearer("password"));

// `GET /foo` 使用有效令牌将接收 `200 OK`
// `GET /foo` 使用无效令牌将接收 `401 未经授权`
// `GET /not-found` 使用无效令牌将接收 `404 未找到`

向路由器添加一个回退处理程序

pub fn fallback<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static

如果没有任何路由匹配传入的请求,将调用此服务。

use axum::{
routing::get,
Router,
handler::Handler,
response::IntoResponse,
http::{StatusCode, Uri},
};

let app = Router::new()
.route("/foo", get(|| async { "foo" }))
.fallback(fallback);

async fn fallback(uri: Uri) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, format!("No route for {uri}"))
}

仅在路由器中没有匹配任何内容的路由时才适用回退。 如果处理程序被请求匹配但返回 404,则不会调用回退。

处理所有没有其他路由的请求

如果没有其他路由,使用Router::new().fallback(...)来接受所有请求, 无论路径或方法如何,这并不是最佳选择

use axum::Router;

async fn handler() {}

let app = Router::new().fallback(handler);

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();

直接运行处理程序会更快,因为它避免了路由的开销:

use axum::handler::HandlerWithoutStateExt;

async fn handler() {}

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, handler.into_make_service()).await.unwrap();

向路由器添加一个回退服务

pub fn fallback_service<T>(self, service: T) -> Self
where
T: Service<Request, Error=Infallible> + Clone + Send + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,

查看Router::fallback以获取更多详细信息。

为路由器提供状态

pub fn with_state<S2>(self, state: S) -> Router<S2>
use axum::{Router, routing::get, extract::State};

#[derive(Clone)]
struct AppState {}

let routes = Router::new()
.route("/", get(|State(state): State<AppState>| async {
// use state
}))
.with_state(AppState {});

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, routes).await.unwrap();

从函数中返回带状态的路由器

在从函数返回 Router 时,通常建议不直接设置状态

use axum::{Router, routing::get, extract::State};

#[derive(Clone)]
struct AppState {}

// 不要在这里调用 `Router::with_state`
fn routes() -> Router<AppState> {
Router::new()
.route("/", get(|_: State<AppState>| async {}))
}

// 在运行服务器之前执行
let routes = routes().with_state(AppState {});

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, routes).await.unwrap();

如果确实需要提供状态,并且您没有将路由嵌套/合并到另一个路由器中,则返回不带任何类型参数的 Router:

// 不要返回 `Router<AppState>`
fn routes(state: AppState) -> Router {
Router::new()
.route("/", get(|_: State<AppState>| async {}))
.with_state(state)
}

let routes = routes(AppState {});

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, routes).await.unwrap();

这是因为我们只能在 Router<()> 上调用 Router::into_make_service, 而不能在 Router<AppState> 上调用。有关原因的更多详细信息,请参见下文。

请注意,状态默认为(),所以RouterRouter<()>是一样的。

如果您需要嵌套/合并路由器,建议在结果路由器上使用通用状态类型:

fn routes<S>(state: AppState) -> Router<S> {
Router::new()
.route("/", get(|_: State<AppState>| async {}))
.with_state(state)
}

let routes = Router::new().nest("/api", routes(AppState {}));

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, routes).await.unwrap();

状态是路由器内的全局状态

传递给此方法的状态将用于该路由器接收的所有请求。 这意味着它不适合保存从请求中派生的状态,比如在中间件中提取的授权数据。 请改用 Extension 来存储此类数据。

Router<S>中的S代表什么

Router<S>表示一个缺少类型为S的状态以处理请求的路由器。 它并不意味着具有类型为S的状态的路由器。

例如:

// 需要`AppState`来处理请求的路由器
let router: Router<AppState> = Router::new()
.route("/", get(|_: State<AppState>| async {}));

// 一旦我们调用 `Router::with_state` 方法,路由器就不再缺少状态了,因为我们刚刚提供了它
//
// 因此,路由器类型变为`Router<()>`,即一个不缺少任何状态的路由器。
let router: Router<()> = router.with_state(AppState {});

// 只有 `Router<()>` 具有 `into_make_service` 方法。
//
// 因为它仍然缺少 `AppState`,所以不能在 `Router<AppState>` 上调用 `into_make_service`。
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, router).await.unwrap();

或许有点反直觉,Router::with_state 并不总是返回一个 Router<()>。 相反,您可以选择新的缺失状态类型是什么:

let router: Router<AppState> = Router::new()
.route("/", get(|_: State<AppState>| async {}));

// 当我们调用`with_state`时,我们可以选择下一个丢失的状态类型是什么。在这里我们选择`String`
let string_router: Router<String> = router.with_state(AppState {});

// 这允许我们添加使用`String`作为状态类型的新路由
let string_router = string_router
.route("/needs-string", get(|_: State<String>| async {}));

// 提供`String`,并选择 `()` 作为新的缺失状态。
let final_router: Router<()> = string_router.with_state("foo".to_owned());

// 既然我们有一个`Router<()>`,我们可以运行它。
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, final_router).await.unwrap();

为什么在调用with_state后返回Router<AppState>不起作用?

// 这不会起作用,因为我们正在返回 `Router<AppState>`
// 即,我们在说我们仍然缺少一个 `AppState`
fn routes(state: AppState) -> Router<AppState> {
Router::new()
.route("/", get(|_: State<AppState>| async {}))
.with_state(state)
}

let app = routes(AppState {});

// 我们只能在 `Router<()>` 上调用 `Router::into_make_service` 方法,
// 而 `app` 是 `Router<AppState>`
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();

由于我们提供了所需的所有状态,因此请返回 Router<()>

// 我们已经提供了所有必要的状态,因此返回 `Router<()>`。
fn routes(state: AppState) -> Router<()> {
Router::new()
.route("/", get(|_: State<AppState>| async {}))
.with_state(state)
}

let app = routes(AppState {});

// 我们现在可以调用 `Router::into_make_service`。
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();

关于性能的说明

如果您需要一个实现 Service 但不需要任何状态的 Router(也许您正在制作一个在内部使用 axum 的库), 那么建议在开始提供请求之前调用该方法:

use axum::{Router, routing::get};

let app = Router::new()
.route("/", get(|| async { /* ... */ }))
// 即使我们不需要任何状态,也要调用`with_state(())`。
.with_state(());

这不是必需的,但它让 axum 有机会更新路由器中的一些内部内容,可能会影响性能并减少分配。

note

请注意,Router::into_make_serviceRouter::into_make_service_with_connect_info 会自动执行此操作。

将路由器转换为带有固定请求体类型的借用服务,以辅助类型推断

pub fn as_service<B>(&mut self) -> RouterAsService<'_, B, S>

在某些情况下,在 Router 上调用 tower::ServiceExt 的方法时,可能会出现类似以下内容的类型推断错误

let response = router.ready().await?.call(request).await?;
^^^^^ cannot infer type for type parameter `B`

这是因为 Router 使用 impl<B> Service<Request<B>> for Router<()> 实现了 Service

例如:

use axum::{
Router,
routing::get,
http::Request,
body::Body,
};
use tower::{Service, ServiceExt};

let mut router = Router::new().route("/", get(|| async {}));
let request = Request::new(Body::empty());
let response = router.ready().await?.call(request).await?;

调用 Router::as_service 可以解决此问题。

use axum::{
Router,
routing::get,
http::Request,
body::Body,
};
use tower::{Service, ServiceExt};

let mut router = Router::new().route("/", get(|| async {}));
let request = Request::new(Body::empty());
let response = router.as_service().ready().await?.call(request).await?;

这主要是在测试时调用路由时使用的。当通过 Router::into_make_service 正常运行路由时,这是不必要的。

将路由器转换为具有固定请求正文类型的所有服务,以帮助类型推断

pub fn into_service<B>(self) -> RouterIntoService<B, S>

这与 Router::as_service 相同,只是它返回一个拥有的服务。有关更多详细信息,请参见该方法。

impl Router

pub fn into_make_service(self) -> IntoMakeService<Self>

将此路由器转换为一个 MakeService,它是一个响应是另一个服务的服务。

use axum::{
routing::get,
Router,
};

let app = Router::new().route("/", get(|| async { "Hi!" }));

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();

仅在tokio特性上可用

pub fn into_make_service_with_connect_info<C>(
self
) -> IntoMakeServiceWithConnectInfo<Self, C>

将这个路由器转换为一个MakeService,它将把C的相关ConnectInfo存储在一个请求扩展中, 以便ConnectInfo可以提取它。

这使得可以提取类似客户端远程地址的信息。

提取std::net::SocketAddr 是开箱即用的。

use axum::{
extract::ConnectInfo,
routing::get,
Router,
};
use std::net::SocketAddr;

let app = Router::new().route("/", get(handler));

async fn handler(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String {
format!("Hello {addr}")
}

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.unwrap();

您可以这样实现自定义连接(Connected):

use axum::{
extract::connect_info::{ConnectInfo, Connected},
routing::get,
serve::IncomingStream,
Router,
};

let app = Router::new().route("/", get(handler));

async fn handler(
ConnectInfo(my_connect_info): ConnectInfo<MyConnectInfo>,
) -> String {
format!("Hello {my_connect_info:?}")
}

#[derive(Clone, Debug)]
struct MyConnectInfo {
// ...
}

impl Connected<IncomingStream<'_>> for MyConnectInfo {
fn connect_info(target: IncomingStream<'_>) -> Self {
MyConnectInfo {
// ...
}
}
}

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<MyConnectInfo>()).await.unwrap();

Trait实现

impl<S> Clone for Router<S>

  • fn clone(&self) -> Self: 返回一个克隆值。
  • fn clone_from(&mut self, source: &Self): 从源(source)执行复制赋值。

impl<S> Debug for Router<S>

  • fn fmt(&self, f: &mut Formatter<'_>) -> Result: 使用给定的格式器格式化值

impl<S> Default for Router<S> where S: Clone + Send + Sync + 'static

  • fn default() -> Self: 返回一个默认值。

impl Service<IncomingStream<'_>> for Router<()>

  • 仅在 tokio crate 功能和 (crate 功能 http1http2) 中可用。

type Response = Router

  • 由服务提供的响应。

type Error = Infallible

  • 由服务生成的错误

type Future = Ready<Result<<Router as Service<IncomingStream<'_>>>::Response, <Router as Service<IncomingStream<'_>>>::Error>>

  • 功能响应值

fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>

  • 当服务能够处理请求时,返回 Poll::Ready(Ok(()))

fn call(&mut self, _req: IncomingStream<'_>) -> Self::Future

  • 处理请求并异步返回响应。

impl<B> Service<Request<B>> for Router<()>
where
B: HttpBody<Data = Bytes> + Send + 'static,
B::Error: Into<BoxError>,

type Response = Response<Body>

  • 由服务提供的响应。

type Error = Infallible

  • 由服务生成的错误

type Future = RouteFuture<Infallible>

  • 功能响应值

fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>>

  • 当服务能够处理请求时,返回 Poll::Ready(Ok(()))

fn call(&mut self, req: Request<B>) -> Self::Future

  • 异步处理请求并返回响应

自动特性实现

  • impl<S> Freeze for Router<S>
  • impl<S = ()> !RefUnwindSafe for Router<S>
  • impl<S> Send for Router<S>
  • impl<S> Sync for Router<S>
  • impl<S> Unpin for Router<S>
  • impl<S = ()> !UnwindSafe for Router<S>

通用实现

impl<T> Any for T
where
T: 'static + ?Sized,

impl<T> Borrow<T> for T
where
T: ?Sized,

impl<T> BorrowMut<T> for T
where
T: ?Sized,

impl<T> From<T> for T

impl<T> FromRef<T> for T
where
T: Clone,

impl<T> Instrument for T

impl<T, U> Into<U> for T
where
U: From<T>,

impl<M, S, Target, Request> MakeService<Target, Request> for M
where
M: Service<Target, Response = S>,
S: Service<Request>,

impl<T> PolicyExt for T
where
T: ?Sized,

impl<T> Same for T

impl<S, R> ServiceExt<R> for S
where
S: Service<R>,

impl<T, Request> ServiceExt<Request> for T
where
T: Service<Request> + ?Sized,

impl<T> ToOwned for T
where
T: Clone,

impl<T, U> TryFrom<U> for T
where
U: Into<T>,

impl<T, U> TryInto<U> for T
where
U: TryFrom<T>,

impl<V, T> VZip<V> for T
where
V: MultiLane<T>,

impl<T> WithSubscriber for T
鱼雪

目录

  • 高级功能
  • 兼容性
  • Hello World
  • 路由
  • 处理程序
  • 提取器
  • 响应
  • 错误处理
  • 中间件
  • 与处理程序共享状态
  • Axum集成
  • 必需依赖
  • 示例
  • 功能标志

高级功能

  • 使用无宏(macro-free)的API路由(router)请求(request)到处理程序(handler)
  • 使用提取器声明式解析请求
  • 简单且可预测的错误处理模型
  • 生成局域最少样板的响应
  • 充分利用towertower-http生态系统的中间件、服务和使用程序

特别是,最后一点是区分axum与其它框架的地方。 axum没有自己的中间件系统,而是使用tower::Service。 这意味着axum获得超时追踪压缩授权等功能, 而且是免费的。 它还能让您与使用hypertonic编写的应用程序共享中间件。

兼容性

axum旨在与tokiohyper协同工作。 至少目前来看,并不追求运行时和传输层独立性。

Hello World

use axum::{
routing::get,
Router,
};

#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(|| async { "Hello World!" }));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
note

使用#[tokio::main]需要您启用tokio的宏和rt-multi-thread功能, 或者只需启用所有功能(cargo add tokio --features macro, rt-multi-thread)

路由

路由器用于设置哪些路径指向那些服务。

use axum::{Router, routing::get};

let app = Router::new
.route("/", get(root))
.route("/foo", get(get_foo).post(post_foo))
.route("/foo/bar", get(foo_bar));

async fn root() {}
async fn get_foo() {}
async fn post_foo() {}
async fn foo_bar() {}

处理程序

axum中,处理程序是一个异步函数,它接受零个或多个提取器作为参数, 并返回可以转换为响应的东西。 处理程序是您的应用程序逻辑所在的地方,并且axum应用程序是通过在处理程序 之间进行路由而构建的。

提取器

提取器是实现了FromRequestFromRequestsParts接口的类型。 提取器是您拆解传入请求以获取处理程序所需部分的方式。

use axum::extract::{Path, Query, Json};
use std::collections::HashMap;

// `Path`会提供路径参数并对其进行反序列化
async fn path(Path(user_id): Path<u32>) {}

// `Query`为您提供查询参数并将其反序列化
async fn query(Query(params): Query<HashMap<String, String>>) {}

// 将请求正文缓冲并将其反序列化为JSON到`serde_json::Value`
// `Json`支持任何实现`serde::Deserialize`的类型
async fn json(Json(payload): Json<serde_json:Value>) {}

响应

处理程序返回的可以实现IntoResponse接口的任何内容

use axum::{
body::Body,
routing::get,
response::Json,
Router,
};
use serde_json::{Value, json};

async fn plain_text() -> &'static str {
"foo"
}

async fn json() -> Json<Value> {
Json(json!({ "data": 42 }))
}

let app = Router::new()
.route("/plain_text", get(plain_text))
.route("/json", get(json));

错误处理

axum旨在拥有一个简单且可预测的错误处理模型。 这意味着将错误转换为响应变得简单, 并且可以保证所有错误都得到处理。

参见error_handling

处理程序之间共享状态

处理程序之间共享状态是很常见的。 例如,可能需要共享数据库连接池或其他服务的客户端。 实现这一点的三种常见方式是:

  • 使用State提取器
  • 使用请求扩展
  • 使用闭包捕获
  1. 使用State提取器
use axum::{
extract::State,
routing::get,
Router,
};
use std::sync::Arc;

struct AppState {
//...
}

let shared_state = Arc::new(AppState { /* ... */ });

let app = Router.new()
.route("/", get(handler))
.with_state(shared_state);

async fn hander(
State(state): State<Arc<AppState>>,
) {
// ...
}

如果可能的话,您应该更倾向于使用State,因为它更具有类型安全性。 不足之处是,它比请求扩展更少的动态性。

  1. 使用请求扩展

在处理程序中提取状态的另一种方式是使用Extension作为中间层和提取器。

use axum::{
extract::Extension,
routing::get,
Router,
};
use std::sync::Arc;

struct AppState {
// ...
}

let shared_state = Arc::new(AppState { /* ... */ });

let app = Router::new()
.route("/", get(handler))
.layer(Extension(shared_state));

async fn hander(
Extension(state): Extension<Arc<AppState>>
) {
// ...
}

这种方法的缺点是,如果你尝试提取一个不存在的扩展, 你将会得到运行时错误(500内部服务器错误响应)

  1. 使用闭包捕获

状态也可以通过闭包捕获直接传递给处理程序。

use axum::{
Json,
extract::{Extension, Path},
routing::{get, post},
Router,
};
use std::sync::Arc;
use serde::Deserialize;

struct AppState {
// ...
}

let shared_state = Arc::new(AppState { /* ... */ });

let app = Router::new()
.route(
"/users",
post({
let shared_state = Arc::clone(&shared_state);
move |body| create_user(body, shared_state)
}),
)
.route(
"/users/:id",
get({
let shared_state = Arc::clone(&shared_state);
move |path| get_user(path, shared_state)
}),
);

async fn get_user(Path(user_id): Path<String>, state: Arc<AppState>) {
// ...
}

async fn create_user(Json(payload): Json<CreateUserPayload>, state: Arc<AppState>) {
// ...
}

#[derive(Deserialize)]
struct CreateUserPayload {
// ...
}

这种方法的缺点在于,它比使用StateExtensions要冗长一些。

为axum构建集成

系统提供FromRequest, FromRequestPartsIntoResponse实现的库 作者应该依赖于axum-core包,而不是axumaxum-core包含核心类型和特性,并且不太可能出现破坏性变化。

必需依赖

要使用axum,你还需要引入一些依赖:

[dependencies]
axum = "<latest-version>"
tokio = { version = "<latest-version>", features = ["full"] }
tower = "<latest-version>"

为了开始使用,完整功能对于 tokio 并不是必需的,但却是最简单的方法。 Tower 也不是绝对必要的,但在测试时很有帮助。 请参考存储库中的测试示例,了解有关测试 axum 应用程序的更多信息。

特性标志

axum使用一组功能标志来减少编译和可选依赖项的数量

以下可选项特性可用:

名称描述是否默认
http1启用hyper的http1特性
http2启用hyper的http2特性
json启用Json类型和一些类似的便利功能
macro启动可选工具宏
matched-path启用了对每个请求的路由路径进行捕获,并使用MatchedPath提取器
multipart启用Multipart解析multipart/form-data请求
original-uri启用捕获每个请求的原始URI和OriginalUri提取器
tokio启用tokio作为依赖和axum::serve,SSE和extract::connect_info类型
tower-log启用tower的日志特性
tracing从内置提取器记录日志
ws通过extract::ws启用Websocket支持
form启用表单提取器
query启用查询提取器

模块

  • body: HTTP请求体工具
  • error_handling: 错误处理模型和工具
  • extract: 从请求为类型和特型提取数据
  • handler: 可以用来处理请求的异步函数
  • middleware: 写中间件的工具
  • response: 生成响应的类型和特型
  • routing: 在Service和处理之间的路由
  • serve: 提供服务

结构体

  • Error: 在使用axum时可能发生的错误
  • Extension: 提取器和扩展响应
  • Form: URL编码的提取器和响应。
  • Json: JSON提取器 / 响应。
  • Router: 用于组合处理程序和服务的路由器类型。

Traits(特型)

  • RequestExt: 扩展特性,为Request添加额外方法
  • RequestPartExt: 扩展特性,为Parts添加额外的方法
  • ServiceExt: 想任何服务添加附加方法的扩展特性

函数

  • serve: tokio和(http1或http2),使用提供的监听器提供服务

属性宏

  • debug_handler: 宏在应用处理函数时生成更好的错误消息。
鱼雪

模式匹配是Rust语言的一项神奇功能,请阅读文本了解更多。

什么是模式匹配

在许多编程语言中,模式匹配都是一个强大而通用的功能, Rust中也有这个功能。 它是我最喜欢的语言功能之一, 我最早是在OCaml中发现它的,它也是函数式编程的支柱之一。

模式匹配的核心是允许开发人员根据一系列模式检查给定值, 并根据匹配结果执行相应的代码。 这意味着模式匹配更注重数据的形状, 而不是数据本身。

在Rust中的模式匹配语法

在Rust中,模式匹配是使用match关键字完成的。 基本语法如下所示:

match value {
pattern1 => { },
pattern2 => { },
...
_ => { },
}

在花括号之间,您可以找到该值可以具有的不同形状, 在=>之后,您可以找到模式匹配时执行的代码。 代码可以是单个表达式用大括号括起来的代码块

_模式是一个包罗万象的模式, 如果前面的模式都不匹配,则匹配任何内容。

模式匹配是一项强大的功能, 因为Rust会检查模式的详尽性,即它将确保所有模式都得到处理, 并且如果您忘记处理某个模式,它会警告您。

模式匹配案例

  1. 匹配数字
let number = 3;

match number {
1 => println!("One"),
2 => println!("Two"),
3 => println!("Three"),
_ => println!("Something else"),
}

功能

  • 本例检查数字的值,并打印数字对应的单词(如果数字在13之间)
  • 通用模式_,是任何未具体匹配的数字(如3以外的数字)的默认情况。 如果缺少了它,Rust就会提示错误,并告诉您该模式并不详尽。

优势

  • 与多个if-else语句相比,此处使用模式匹配简化了逻辑。 它更加简洁和可读,特别是对于固定范围的值。
  1. 匹配字符串
let day = "thursday";

match day {
"monday" => println!("first day of the week"),
"tuesday" => println!("second day of the week"),
"wednessday" => println!("third day of the week"),
"thursday" => println!("fourth day of the week"),
"friday" => println!("fifth day of the week"),
"saturday" => println!("sixth day of the week"),
"sunday" => println!("seventh day of the week"),
_ => println!("this is not a valid day"),
}

功能

  • 这会将字符串day与代表一周中每一天的七种可能性进行匹配, 并根据匹配执行不同的代码。 最后的包罗万象的模式将捕获不是有效日期的字符串。

优势

  • 模式匹配有利于处理特定的已知字符串。 它比使用一些列if-else语句更清晰、更直接。
  1. 使用可选项
let some_option: Option<i32> = Some(5);

match some_option {
Some(number) => println!("Number is {}", number),
None => println!("No number"),
}

功能

  • 这个例子处理 Option<i32> 类型,该类型可能包含整数(Some) 或不包含任何内容(None)。
  • match语句要么打印数字(如果存在),要么打印一条消息说没有数字。
  • 这种处理值缺失的方法比在其他语言中使用undefinednil更安全, 因为None清楚地表明该值不存在,但代码按预期工作,而不是发生了另一种问题。

优势

  • 模式匹配非常适合Option类型,因为它以安全、简洁的方式优雅地处理 两种情况(SomeNone),同时保持代码的确定性。
  1. 匹配枚举
enum Direction {
Up,
Down,
Left,
Right,
}

let dir = Direction::Up;
match dir {
Direction::Up => println!("Going up!"),
Direction::Down => println!("Going down!"),
Direction::Left => println!("Going left!"),
Direction::Right => println!("Going right!"),
}

功能

  • 枚举定义:

    WebEvent有五个变体,每种变体代表不同类型的事件。 PageLoadPageUnload等变体包含一个StringKeyPress包含一个u32MouseClick包含两个xy坐标结构中的i64值。

  • 模式匹配:

    match语句,每个臂对应不同的WebEvent变体。 对于PageLoadPageUnload,它会打印URL。 对于KeyPress,它会打印按键代码。 对于MouseClick,它会析构结构以获取xy坐标并打印出来。 对于ScreenRefresh(屏幕刷新),它不携带额外数据,只打印一条信息。

优势

  • 对带有值的枚举进行模式匹配,可以简洁地处理封装在每个枚举变量中的不同类型的数据。 与使用嵌套的if-else语句或其他方法相比,通过直接解构每个变体, 代码变得简洁、更易读。这种方法还能确保处理所有可能的情况(枚举的变体), 从而使代码更加健壮和详尽。
  1. 复杂模式
let pair = (0, -2);

match pair {
(0, y) => println!("Y axis: {}", y),
(x, 0) => println!("X axis: {}", x),
_ => println!("Somewhere on the plane"),
}

功能

  • 此示例处理包含两个整数的元组对。
  • 匹配检查两个整数中的任何一个是否为零,并识别该对位于哪个轴上, 或者以其他方式确认它位于平面上的某个位置。

优势

  • 像这样的复杂模式匹配对于解构和处理各种数据类型非常有用。 它比嵌套的if-else语句更高效、更易读,尤其是在处理元组等多组数据结构时。
let array = [1, 2, 3];

match array {
[0, ..] => println!("Array starts with 0"),
[1, 2, 3] => println!("Array contains 1, 2, 3"),
[_, _, _] => println!("Array has three elements"),
}

功能

  • 前缀匹配:[0, ..]使用..模式来匹配以0开头的任何数组。如果数组以0开头(如[0,4,5]), 则会打印Arraystarts with 0
  • 完全匹配:[1, 2, 3]匹配恰好包含1,2,3的数组。如果数组是[1,2,3], 则打印Array contains 1,2,3
  • 长度匹配:[_,_,_]匹配任何具有三个元素的数组,而不管它们的值是多少。 如果数组有三个元素,如[7,8,9]则打印数组有三个元素。由于在进行模式匹配之前, 数组的长度是已知的,因此它或多或少起到了一网打尽的作用。

优势

  • 数组上的模式匹配可以成为验证数组或其包含的元素的形状的强大工具, 而无需诉诸更复杂的遍历数组的方法(如foldmap)

限制和考虑因素

Rust 对数组的模式匹配是有限的,因为我们需要在编译时知道数组的大小。 模式必须考虑数组的长度,这与其他数据类型(如向量)相比会有限制,因为后者的长度可以是动态的。 对于小数组和固定大小的数组, 模式匹配可以为处理基于数组的逻辑提供一种简洁、可读性强的方法, 但对于较长的数组,使用这种方法就会变得更加麻烦。

为什么更喜欢模式匹配而不是if条件

模式匹配是一项强大的功能,因为它不仅仅是检查相等性, 还可以重组数据类型,如元组或枚举,从而直接提取值。

与一系列 if 语句相比,这使得代码更加简洁、可读性更强、更不容易出错, 尤其是在处理复杂的数据结构时。

模式匹配还能确保处理所有可能的情况,无论是特定情况还是默认情况,从而使代码更加健壮。

总之,Rust 中的模式匹配为处理条件逻辑提供了一种清晰、简洁和强大的方法。

在使用 Rust 的各种数据类型(如枚举和选项)时,它的作用尤为突出, 让开发人员可以编写出更可读、更易维护的代码。

无论是处理简单的值还是复杂的数据结构,模式匹配都能大大简化 Rust 代码。

最后的话

Rust 中的模式匹配为处理不同类型的数据和条件提供了一种结构化和优雅的方法。

它能够针对不同的数据类型和结构进行重组和匹配, 因此在很多情况下,它比传统的条件语句更受欢迎。

这不仅使代码更具可读性和可维护性,还能确保以安全、稳健的方式全面处理所有可能的情况。

鱼雪

在Rust中,迭代器是一种尤为重要的数据类型, 被用于遍历集合中的元素。 Rust中的大多数集合类型都可以转换成迭代器, 从而可以对它们进行遍历。 包括:数组(Array)、向量(Vec)、散列表(HashMap)

使用迭代器可以使代码更加简洁、优雅,并且支持 过滤映射折叠 等强大的操作

迭代器的基本概念

在Rust中,迭代器是一种实现了Iterator trait的类型。 该trait定义了在集合中遍历元素的一系列行为。 通过实现Iterator trait,可以将一个类型转换为一个迭代器, 从而实现迭代器等操作。

Iterator Trait

Iterator trait定义了迭代器的核心行为。 它包括next方法和其它几个方法。 next方法为集合中的下一个元素返回一个**Option值**, 直到所有元素都遍历完毕,此时返回None

除了next方法,Iterator trait还定义了许多其他有用的方法, 例如mapfilter等,允许对迭代器元素进行操作和转换

pub trait Iterator {
type Item;

fn next(&mut self) -> Option<Self::Item>;
}

Animal案例

让我们探索Animal结构体的迭代器的实现。 Animal类型实现了Iterator trait, 从而可以对其属性进行迭代。 以下是Animal类型的定义:

#[derive(Debug)]
struct Animal {
name: String,
age: u32,
kind: String,
i: i32,
}

我们为Animal实现Iterator trait,以使用for循环迭代其属性:

impl Iterator for Animal {
type Item = String;

fn next(&mut self) -> Option<Self:Item> {
let next_attribute = match self.i {
0 => Some(self.name.clone()),
1 => Some(self.age.to_string()),
2 => Some(self.kind.clone()),
_ => None,
};
self.i += 1;
next_attribute
}
}

现在我们已经将类型转换为迭代器,
我们可以从迭代器中调用各种方法。
例如,可以使用`for`循环来迭代`Animal`的每个属性:

```rust
fn main() {
let mut animal = Animal {
name: "Tome".to_string(),
age: 15,
kind: "cat".to_string(),
i: 0,
}
}

在上述代码中,我们定义了一个Animal的迭代器, 包括一个名为i的内部状态变量。 代码成功打印了Animal的所有信息。

让我们通过定义Animal向量并迭代打印每个Animal的属性来继续我们的探索:

fn print_all_attrs(animals: Vec<Animal>) {
for mut animal in animals {
println!("Name: {}", animal.next().unwrap());
println!("Age : {}", animal.next().unwrap());
println!("Kind: {}", animal.next().unwrap());
}
}

fn main() {
let animals = vec![Animal {
name: "Tom".to_string(),
age: 15,
kind: "cat".to_string(),
i: 0,
}];
print_all_attrs(animals);
}

在这段代码中,我们使用for循环来迭代所有Animal对象并逐一打印它们的属性。

迭代器的常见用途

map方法

map方法是Iterator trait中的一个关键方法。 它允许我们转换迭代器中的每个元素并返回一个新的迭代器。 例如:

fn main() {
let animals = vec![
Animal {
name: "Tom".to_string(),
age: 14,
kind: "cat".to_string(),
i: 0,
},
Animal {
name: "Jerry".to_string(),
age: 7,
kind: "mouse".to_string(),
i: 1,
},
];

let list: Vec<String> = animals
.into_iter()
.map(|ani| ani.name.clone())
.collect();
println!("{:?}", list);
}

在上面的代码中,我们定义了一个带有两个Animal对象的向量, 并使用map方法从每个Animal中提取name属性, 返回一个新的迭代器。 然后,collect()方法将其转换为向量。

filter方法

假设我们想找到三岁或以上的Animal。 我们可以使用过滤器来实现这一点:

fn main() {
let animals = vec![
Animal {
name: "Tom".to_string(),
age: 33,
kind: "cat".to_string(),
i: 3,
},
];

let filtered_animals: Vec<Animal> = animals
.into_iter()
.filter(|animal| animal.age >= 3)
.collect();
println!("{:?}", filtered_animals);
}

在上面的代码中,我们使用filter方法选择年龄为三岁或以上的Animal, 返回一个新的Animal向量。

enumerate方法

enumerate方法将迭代器中的每个元素与其索引配对, 返回一个新的迭代器。例如:

fn main() {
let animals = vec![
Animal {
name: "Tom".to_string(),
age: 33,
kind: "cat".to_string(),
i: 3,
},
Animal {
name: "Jay".to_string(),
age: 22,
kind: "mouse".to_string(),
i: 4,
},
];

for (i, animal) in animals.iter().enumerate() {
println!("{}: {:?}", i, animal);
}
}

在上面代码中,我们使用enumerate方法将每个Animal与其索引配对, 并使用for循环打印结果。

flat_map方法

flat_map方法不太常见但很有用。 它展平嵌套迭代器到单个迭代器中。例如:

fn main() {
let cat = Animal {
name: "Tom".to_string(),
age: 21,
kind: "cat".to_string(),
i: 3,
};

let mouse = Animal {
name: "Jerry".to_string(),
age: 3,
kind: "mouse".to_string(),
i: 2,
};

let animals = vec![vec![cat], vec![mouse]];

let list: Vec<Animal> = animals
.iter()
.flat_map(|x| x.iter().cloned())
.collect();
println!("{:?}", list);
}

在上面代码中,我么定义了一个2D向量Animals,并使用flat_map方法 将其展平为1D迭代器,将其转换回向量。

zip方法

要同时迭代两个向量,我们可以使用zip方法进行配对:

fn main() {
let names = vec!["Tom", "Jerry", "Bob"];
let ages = vec![3, 4, 5];

for (name, age) in names.iter().zip(ages.iter()) {
println!("{} is {} years old", name, age);
}
}

在上面代码中,我们使用zip方法将姓名和年龄向量中的元素配对, 并在for循环中打印每对。

fold方法

fold方法在Rust中时必不可少的; 它接受一个初始值和一个闭包, 迭代元素, 并将它们合并为一个值。例如:

fn main() {
let cat = Animal {
name: "Tom".to_string(),
age: 13,
kind: "cat".to_string(),
i: 0,
};
let mouse = Animal {
name: "Jerry".to_string(),
age: 7,
kind: "mouse".to_string(),
i: 1,
};

let animals = vec![cat, mouse];

let sum = animals
.iter()
.fold(0, |t, ani| t + ani.age);
println!("{}", sum);
}

在上面代码中,我们定义了一个带有两个Animal对象的向量, 并使用fold方法累加年龄属性, 返回结果总和。

总结

在Rust中,迭代器是遍历集合元素和支持各种操作的重要数据类型。 在本文中,我们探讨了迭代器的基本概念和常用方法, 并使用Animal示例演示了相应的代码。 希望读者能够扎实地理解Rust迭代器,并在实际编程中有效地应用它们。

鱼雪