跳到主要内容

77 篇博文 含有标签「Rust」

查看所有标签

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 有机会更新路由器中的一些内部内容,可能会影响性能并减少分配。

备注

请注意,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();
}
备注

使用#[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迭代器,并在实际编程中有效地应用它们。

鱼雪

Rust 的所有权系统是编译器用来处理内存,确保程序内存使用安全性的关键方面。 与一些具有垃圾回收或显式内存分配和释放的语言不同,Rust 采用了一种独特的方法。 它通过一组规则来管理内存,这些规则由编译器检查,确保程序保持内存安全。

以下是 Rust 所有权系统的三个关键规则:

  1. Rust 中的每个值都有一个所有者。
  2. 同一时间只能有一个所有者。
  3. 当所有者超出范围时,该值将被丢弃,内存将被释放。

例如:

fn main() {
// 规则 1:Rust 中的每个值都有一个所有者
let mut s = String::from("Hello");

// 规则 2:同一时间只能有一个所有者
let s1 = s; // s1 现在拥有字符串;s 不再有效

// 规则 3:当所有者超出范围时,该值将被丢弃
// 当 s1 超出范围时,值 "Hello" 将被丢弃
}

这些规则确保 Rust 程序有效地管理内存,防止与内存错误相关的常见陷阱。 理解所有权对于编写健壮且安全的 Rust 代码至关重要。

所有权和函数

Rust 的所有权概念不仅适用于变量,还适用于将值传递给函数。 当将值传递给函数时,它要么移动,要么复制,就像在变量赋值期间一样。 让我们通过一些带有注释的示例来探讨这个问题。

fn main() {
let s = String::from("welcome"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数中,此处不再有效
let x = 8; // x 进入作用域
makes_copy(x); // x 将移动到函数中,但 i32 是 Copy 类型,所以在此之后仍然可以使用 x
} // 此处,首先是 x 超出范围,然后是 s。因为 s 的值已被移动,所以不会发生特殊的事情。

fn takes_ownership(s1: String) {
// s1 进入作用域
println!("{}", s1);
} // 此处,s1 超出范围,其内存被释放。

fn makes_copy(y: i32) {
// y 进入作用域
println!("{}", y);
} // 此处,y 超出范围。不会发生特殊的事情。

如果我们尝试在调用 takes_ownership 后使用 s, Rust 将引发编译时错误,确保我们的代码正确性。

返回值和作用域

函数还可以通过返回值传递所有权。考虑以下带有注释的示例:

fn main() {
let s1 = gives_ownership(); // gives_ownership 将其返回值移动到 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到 takes_and_gives_back,该函数还将其返回值移动到 s3
} // 此处,s3 超出范围并被丢弃。s2 被移动,因此什么都不会发生。s1 超出范围并被丢弃。

fn gives_ownership() -> String {
// gives_ownership 将其返回值移动到调用它的函数中
let s4 = String::from("welcome"); // s4 进入作用域
s4 // 返回 s4 并移动到调用它的函数中
}

fn takes_and_gives_back(s5: String) -> String {
// s5 进入作用域
s5 // 返回 s5 并移动到调用它的函数中
}

所有权模式保持一致:将值分配给另一个变量将其移动。 如果具有堆数据的变量超出范围,除非所有权已被转移,否则该值将被清理。

在 Rust 中的借用和引用

在 Rust 中,借用和引用是使多个变量能够与和操作数据而不获取所有权的基本概念,提供对程序中内存使用的更强大的控制。

借用

借用是一种在不声称所有权的情况下临时使用值的行为。 这允许多个变量读取和访问数据而不更改它。 借用通过引用实现,引用是对数据的不可变指针。 让我们通过一个简化的例子来了解这个概念:

fn main() {
let s = String::from("hello");
let len = s.len();
println!("String length: {}", len);
let mut s1 = s;
// s1[0] = 'h'; // 取消注释此行会导致错误,因为 s1 现在拥有数据
println!("String after modifying: {}", s1);
}

在这个例子中,String 值 slen 函数和变量 s1 借用。 但是,只有 len 函数以不可变方式借用该值,而 s1 以可变方式借用该值。 这意味着 s1 可以修改 s 的值,但 len 不能。

引用

在 Rust 中,引用是对数据的不可变指针。它允许您访问和读取数据而不进行修改。 由 & 符号表示的引用必须遵循特定的借用规则:

  1. 引用的作用域不能超过所有者的作用域。
  2. 不能有对同一值的多个可变引用。

以下是演示 Rust 中引用的简化示例:

fn main() {
let s = String::from("hello");
let len = s.len();
println!("String length: {}", len);
let s1 = &s;
let len1 = s1.len();
println!("String length through reference: {}", len1);
}

在这个例子中,len 函数采用对 String 值 s 的不可变引用。 类似地,变量 len1 采用对 s 的不可变引用。 这允许多个变量访问 s 的值而不进行修改。

总之,在 Rust 中,借用和引用使多个变量能够与和操作数据而不声称所有权, 为程序中内存使用提供了更大的灵活性和控制。

参考

Rust 所有权的官方文档

鱼雪

在系统编程不断发展的领域中,Zig和Rust两种语言因其独特的方法和能力而脱颖而出。 两者都提供引人注目的功能,但面向不同的需求和偏好。 本文深入详细比较了Zig和Rust,突出它们的优势、用例和主要差异。

Zig: 注重简单性和控制

简单性和可读性:Zig的简单语法,类似于C,强调可读性和易于维护。 其设计避免了隐藏的控制流和内存分配,使代码透明且易于理解。

  • 性能:Zig被设计为高性能,并提供对底层细节的高度控制。
  • 编译时执行:Zig强调在编译时执行,从而减少运行时开销并优化性能。
  • 控制和底层能力:作为系统编程的理想选择,Zig在需要直接管理系统资源的场景中表现出色。
  • 内存安全:Zig通过编译时检查和显式错误处理来实现内存安全,依赖程序员进行手动内存管理。 与Rust、C、C++一样,Zig不使用垃圾收集器。为了实现像Rust那样的内存安全,Zig提供促进内存安全的机制, 例如:
    1. 严格的编译时检查
    2. 用于处理可能为null值的可选类型
    3. 带有Error类型的显式错误处理
    4. 具有内建分配器的增强型内存分配
  • 互操作性:Zig具有出色的C互操作性,可以直接使用C库而无需包装器。
  • 错误处理:Zig使用错误类型、错误联合和延迟语句进行错误处理。
  • 社区和生态系统:作为相对较新的语言,Zig的社区和生态系统较小,但正在不断发展。
  • 元编程能力:Zig的编译时元编程通过减少样板代码的需求和启用代码优化,提高了代码的灵活性和生产力。
  • 用例:Zig的简单性和直接的C互操作性使其在嵌入C项目或进行低级系统编程, 需要控制和清晰度至关重要的情况下特别有优势。 例如,在构建操作系统、设备驱动程序和嵌入式系统的系统编程中非常理想。 它还在命令行工具中很有用,用于创建高效快速的命令行界面,构建系统脚本或优化现有工具的性能。 Zig以其元编程能力和对简单性的关注而闻名,Bun是一个在Zig中开发的JavaScript运行时环境的著名示例。

Rust: 安全、并发和高性能

  • 无垃圾收集的内存安全:Rust的所有权和借用规则确保内存安全,消除了垃圾收集器的需求。
  • 并发和并行性:内置对安全有效多线程的支持使Rust成为需要并发的应用程序的理想选择。
  • 编译时间:Rust的安全措施可能导致比其他语言更长的编译时间。为了在运行时防止问题,它在编译时检查代码。
  • 互操作性:Rust依赖于外部函数接口(FFI)和类似的倡议,使其能够与C和C++集成。
  • 社区和生态系统:Rust拥有强大的工具和库生态系统。其包管理器Cargo显着简化了依赖管理和与外部库的集成。
  • 错误处理:Rust的Result和Option类型增加了错误处理的表达性,提高了代码的可靠性和可读性。
  • 跨平台兼容性和零成本抽象:Rust提倡跨平台开发。Rust通过将所有代码编译成机器指令,没有解释器或垃圾收集器,实现零成本抽象。这样,Rust确保任何抽象都不会带来额外的运行时成本。
  • 用例:Rust的先进并发特性在Web服务器和数据库系统中得到有效利用,其中安全有效地管理多个线程至关重要。其所有权模型确保线程安全,使其成为高性能并发应用程序的强大选择,例如Servo浏览器引擎或TiKV分布式键值存储。
  • 尽管像任何语言一样,Rust有其优点和缺点,但它仍然是开发人员中的热门选择。在2023年Stack Overflow开发人员调查中,Rust第8年蝉联“最受欢迎和令人敬佩的编程语言”的榜首,超过80%的受访者表示他们明年仍然想要使用它。
  • 在系统编程中,Rust用于构建操作系统、设备驱动程序和嵌入式系统等任务。
  • 后端和前端Web开发人员也使用Rust与流行的框架(如Rocket或Actix)进行后端开发,以及使用WebAssembly或Tauri进行前端开发。
  • Rust还用于网络和网络服务,例如网络协议、代理、负载均衡器、VPN软件等。

Rust vs. Zig: 性能

客观地说,在Rust和Zig之间没有更高性能的语言。Rust在特定应用中可能优于Zig,而Zig在其他应用中可能优于Rust。

让我们仔细检查每种语言和编译器基准的比较中的性能:

此基准项目包含用几种编程语言编写的同时运行的程序。 它们的运行结果然后被测量并以表格形式呈现,供您查看每种编程语言在任务中的性能如何。

Rust Vs Zig

在上图中,我们有使用Rust和Zig编写的mandelbrot和nbody程序。比较表中的测量结果从性能更好到性能较差进行排列。

您会注意到,在某些情况下,Zig的性能优于Rust,在其他情况下,Rust的性能优于Zig。 两者都是高性能的语言,因此在项目中选择任何一种选项都应该能够满足您的需求。

结论:为工作选择合适的工具

  • 用例适应性:如果您的项目需要在内存安全和并发方面提供强大的保证,Rust可能是更好的选择。对于更注重C互操作性和简单性的项目,Zig可能更合适。
  • 社区和支持:Rust更大的生态系统可能是需要广泛的外部库和社区支持的项目的决定性因素。
  • 学习曲线:由于其类似于C的语法,Zig可能在初期更容易掌握,而Rust的学习曲线可能会因其强大的安全功能和并发支持而得到证明,具体取决于项目需求。
  • 性能优化:Zig和Rust都以高度优化的代码、手动内存管理、直接CPU访问和编译时评估而闻名。
  • 低级控制:两者都提供对系统资源的更多控制,使它们成为低级任务和系统编程的理想选择。

最终,选择Zig还是Rust应该基于项目的具体需求、团队对语言的熟悉程度以及您需要的支持和库的种类。 这两种语言在系统编程领域都是强大的工具,可以根据不同情境做出正确选择。 由于Rust存在的时间更长,因此在功能和稳定性方面可能被认为更加成熟。

参考链接

Zig vs Rust: A Comprehensive Comparison for Modern Developers

Zig官网

Rust官网

编程语言和编译器基准测试

鱼雪

CandleRust 的极简 ML 框架,重点关注性能(包括 GPU 支持)和易用性。 今天我们来使用Candle完成一个深度学习的Hello World案例:手写数字识别。 我们使用最简单的线性模型来训练一个自己的手写数字识别模型,作为Candle框架的 最简单入门案例。

环境

  • Rust: 1.75.0-nightly
  • candle-core: 0.3.0
  • candle-nn: 0.3.0
  • candle-datasets: 0.3.0
提示

candle-nn当前版本中依赖了Rust nightly

Cargo.toml内容如下

  1. rand: 随机数
  2. anyhow: 处理异常
  3. clap: 解析命令行参数
[package]
name = "linear_mnist"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
candle-core = { git = "https://github.com/huggingface/candle.git", version = "0.3.0" }
candle-nn = { git = "https://github.com/huggingface/candle.git", version = "0.3.0" }
rand = "0.8.5"
anyhow = "1"
clap = { version = "4.4.4", features = ["derive"] }
candle-datasets = { git = "https://github.com/huggingface/candle.git", version = "0.3.0" }

创建项目并安装Candle相关模块

  1. 使用cargo new创建linear_mnist项目
  2. 进入项目目录
  3. 安装candle三个模块
    • candle-core
    • candle-nn
    • candle-datasets
  4. 安装其他依赖库
    • rand
    • anyhow
    • clap

具体操作如下:

cargo new linear_mnist
cd linear_mnist

cargo add --git https://github.com/huggingface/candle.git candle-core
cargo add --git https://github.com/huggingface/candle.git candle-nn
cargo add --git https://github.com/huggingface/candle.git candle-datasets

代码

导入相关依赖

  1. 导入clap::Parser解析命令行参数
  2. 导入candle_core的相关依赖
    • Device: 数据计算时放置的设备
    • Result: 处理异常
    • Tensor: 张量数据类型
    • D: 是一个enum,包含Minus1Minus2
    • DType: 数据类型enum结构,包含支持的数据类型
  3. 导入candle-nn的相关依赖
    • loss: 损失函数相关操作
    • ops: 函数操作,如log_softmax
    • Linear: 线性模型
    • Module: 由于Linear的依赖
    • Optimizer: 优化器
    • VarBuilder: 构建变量
    • VarMap: 用于存储模型变量
use clap::{ Parser };
use candle_core::{ Device, Result, Tensor, D, DType };
use candle_nn::{ loss, ops, Linear, Module, Optimizer, VarBuilder, VarMap };

定义相关配置

  1. 定义图像维度数量和标签数量的常量
  2. 定义命令行参数解析,并添加指令宏#[derive(Parser)],可以使用clap::Parser解析命令行参数
    • learning_rate: 学习率
    • epochs: 模型训练迭代次数
    • save_model_path: 训练好的模型保存路径
    • load_model_path: 加载预训练模型路径
    • local_mnist: 本地MNIST数据集目录
  3. 定义训练参数结构体TrainingArgs
  4. 定义线性模型结构体LinearModel

具体代码如下:

const IMAGE_DIM: usize = 784;
const LABELS: usize = 10;

#[derive(Parser)]
struct Args {
#[arg(long)]
learning_rate: Option<f64>,

#[arg(long, default_value_t = 10)]
epochs: usize,

#[arg(long)]
save_model_path: Option<String>,

#[arg(long)]
load_model_path: Option<String>,

#[arg(long)]
local_mnist: Option<String>,
}

struct TrainingArgs {
learning_rate: f64,
load_path: Option<String>,
save_path: Option<String>,
epochs: usize,
}

struct LinearModel {
linear: Linear,
}

定义模型

  1. 定义Model trait
  2. LinearModel实现Model trait
    • new: 初始化模型
    • forward: 模型结构,前向传播
  3. linear_z是具体创建Linear模型
    • 创建模型张量变量并调用candle-nn::Linear创建线性模型返回

具体代码如下:

trait Model: Sized {
fn new(vs: VarBuilder) -> Result<Self>;
fn forward(&self, xs: &Tensor) -> Result<Tensor>;
}

impl Model for LinearModel {
fn new(vs: VarBuilder) -> Result<Self> {
let linear: Linear = linear_z(IMAGE_DIM, LABELS, vs)?;
Ok(Self { linear })
}

fn forward(&self, xs: &Tensor) -> Result<Tensor> {
self.linear.forward(xs)
}
}

fn linear_z(in_dim: usize, out_dim: usize, vs: VarBuilder) -> Result<Linear> {
let ws: Tensor = vs.get_with_hints((out_dim, in_dim), "weight", candle_nn::init::ZERO)?;
let bs: Tensor = vs.get_with_hints(out_dim, "bias", candle_nn::init::ZERO)?;
Ok(Linear::new(ws, Some(bs)))
}

定义模型训练函数

  1. 输入参数
    • m: 数据集
    • args: 训练参数TrainingArgs
  2. 获取或设置模型运算的设备Device::Cpu
  3. 从数据集m中获取训练数据和标签,测试数据和标签
  4. 创建varmap用来存储模型参数
  5. 创建vs变量构造,存储模型参数,并将其传入到Model::new
  6. 如果命令行传入load_model_path,则会加载预训练模型
  7. 创建优化器SGD
  8. 根据epochs迭代训练模型
    • 前向传播得到logits
    • 计算概率log_softmax
    • 计算损失函数值
    • 反向传播sgd.backward_step()
    • 输入测试数据得到测试数据准确率test_accuracy
    • 每个epoch花费的时间epoch_duration
  9. 如果命令传入save_model_path,则会保存模型参数
    • 确保存放模型的目录已经建立

具体代码如下:

fn train<M: Model>(
m: candle_datasets::vision::Dataset,
args: &TrainingArgs) -> anyhow::Result<()> {

let dev = Device::Cpu;

let train_labels = m.train_labels;
let train_images = m.train_images.to_device(&dev)?;
let train_labels = train_labels.to_dtype(DType::U32)?.to_device(&dev)?;
let test_images = m.test_images.to_device(&dev)?;
let test_labels = m.test_labels.to_dtype(DType::U32)?.to_device(&dev)?;

let mut varmap = VarMap::new();
let vs = VarBuilder::from_varmap(&varmap, DType::F32, &dev);
let model = M::new(vs.clone())?;

// Load Pre-trained Model Parameters
if let Some(load_path) = &args.load_path {
println!("Loading model from {}", load_path);
let _ = varmap.load(load_path);
}

// Create Optimizer
let mut sgd = candle_nn::SGD::new(varmap.all_vars(), args.learning_rate)?;

// Iterate training model
for epoch in 1..=args.epochs {
let start_time = std::time::Instant::now();
let logits = model.forward(&train_images)?;
let log_sm = ops::log_softmax(&logits, D::Minus1)?;
let loss = loss::nll(&log_sm, &train_labels)?;

sgd.backward_step(&loss)?;

let test_logits = model.forward(&test_images)?;
let sum_ok = test_logits
.argmax(D::Minus1)?
.eq(&test_labels)?
.to_dtype(DType::F32)?
.sum_all()?
.to_scalar::<f32>()?;
let test_accuracy = sum_ok / test_labels.dims1()? as f32;
let end_time = std::time::Instant::now();
let epoch_duration = end_time.duration_since(start_time);
println!("Epoch: {epoch:4} Train Loss: {:8.5} Test Acc: {:5.2}% Epoch duration: {:.2} second.",
loss.to_scalar::<f32>()?, test_accuracy * 100., epoch_duration.as_secs_f64());
}

// Save Model Parameters
if let Some(save_path) = &args.save_path {
println!("Saving trained weight in {save_path}");
varmap.save(save_path)?
}
Ok(())
}

main函数

  1. 解析命令行参数Args
  2. 根据local_mnist命令行参数指定的目录加载MNIST数据集
  3. 设置学习率
  4. 创建模型训练参数TrainingArgs类型变量training_args并填充设置好的参数
  5. 调用模型训练函数train::<LinearModel>(m, &training_args),传入数据集模型训练参数
fn main() ->anyhow::Result<()> {
let args: Args = Args::parse();
let m: candle_datasets::vision::Dataset = if let Some(directory) = args.local_mnist {
candle_datasets::vision::mnist::load_dir(directory)?
} else {
candle_datasets::vision::mnist::load()?
};

println!("Train Images: {:?}", m.train_images.shape());
println!("Train Labels: {:?}", m.train_labels.shape());
println!("Test Images: {:?}", m.test_images.shape());
println!("Test Labels: {:?}", m.test_labels.shape());

let default_learning_rate: f64 = 0.1;

let training_args = TrainingArgs {
epochs: args.epochs,
learning_rate: args.learning_rate.unwrap_or(default_learning_rate),
load_path: args.load_model_path,
save_path: args.save_model_path,
};

train::<LinearModel>(m, &training_args)
}

训练

  1. 如果saved_model不存在,则需要先创建该目录

  2. 目录结构如下

linear_mnist
├── Cargo.lock
├── Cargo.toml
├── dataset
│   ├── t10k-images-idx3-ubyte
│   ├── t10k-labels-idx1-ubyte
│   ├── train-images-idx3-ubyte
│   └── train-labels-idx1-ubyte
├── saved_model
│   └── minst.safetensors
└── src
└── main.rs
  1. 训练并保存模型参数

HuggingFace Candle 训练手写数字识别

  1. 加载预训练模型继续训练

HuggingFace Candle加载预训练模型

  1. 完整代码地址

Candel Linear Model Training MNIST Classification on Github

参考代码

Candle MNIST Training

鱼雪