跳到主要内容

52 篇博文 含有标签「Rust」

查看所有标签

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

鱼雪

Rust 是一种由 Mozilla 领导开发的系统编程语言。它设计的目标是提供内存安全、并发性和高性能。Rust 具有以下一些主要特性:

  • 内存安全
  • 所有权系统
  • 并发性
  • 零成本抽象
  • 模式匹配
  • 没有运行时开销
  • 活跃的社区

基本数据类型

  • 很大程度上,Rust是围绕其类型设计的。
  • Rust对高性能代码的支持,源自它能让开发者原则最适合当前场景的数据表示法,在简单性与成本之间进行合理的权衡。
  • Rust的内存和线程安全保障也依赖于Rust类型系统的健全性。
  • Rust的灵活性则源自于其泛型类型和特型。

Rust有以下两个特性

  • 基于已明确写出的类型,Rust的类型推断会帮你推断出剩下的大部分类型
  • 函数可以是泛型的:单个函数就可以处理许多不同类型的值

固定宽度的数值类型

  1. Rust类型系统的根基是一组固定宽度的数值类型,选用这些类型是为了匹配几乎所有现代处理器都已直接在硬件中实现的类型。 固定宽度的数值类型可能会溢出或丢失精度。
  2. char既不是u8,也不是u32(尽管它确实有32位长)
  3. usize类型和isize类型类似于CC++中的size_tptrdiff_t。它们的精度与目标机器上地址空间的大小保持一致, 即在32位架构上是32位长,在64位架构上则是64位长。
  4. Rust要求数组索引是usize值。
  5. 可以使用as运算符将一种整型转换成另一种整型。
  6. Rust几乎不会执行任何隐式的数值转换,随时可以使用as运算符写出显式转换。

布尔类型

  • Rust非常严格
    • ifwhile这样的控制结构要求它们的条件必须是bool表达式
    • 短路逻辑运算符&&||也是如此

字符

  1. Rust的字符类型char会以32位值表示单个Unicode字符
  2. Rust会对单独的字符使用char类型,但对字符串文本流使用UTF-8编码
  3. u8是唯一能通过as运算符转换为char的类型

指针类型

  1. Rust有多种表示内存地址的类型,这是Rust和大多数具有垃圾回收功能的语言之间一个重大差异。
  2. Rust语言旨在帮你将内存分配保持在最低限度。

引用

  1. 将引用视为Rust中的基本指针类型
  2. Rust引用有两种形式
    • &T: 一个不可变的共享引用
    • &mut T: 一个可变的、独占的引用
  3. Rust利用共享引用和可变引用之间的"二选一"机制来强制执行"单个写入者或多个读取者"规则
  4. 你独占读写一个值,或者让任意数量的读取者共享,但二者只能选择其一
  5. 这种由编译期检查强制执行的"二选一"规则是Rust安全保障的核心

数组、向量和切片

  1. Rust用3种类型来表示内存中的值序列

    • 类型[T;N]表示N个值的数组,每个值的类型为TN在编译期已经确定,不能追加新元素或缩小数组
    • 类型Vec<T>可称为T的向量,它是一个动态分配且可增长的T类型的值序列
    • 类型&[T]&mut [T]可称为T的共享切片和T的可变切片
  2. 在数组上看到的那些使用方法都是作为切片而非数组的方法提供的

    • 包括:遍历、搜索、排序、填充、过滤等
    • Rust在搜索各种方法时会隐式地将对数组的引用转换为切片,因此可以直接在数组上调用任何切片的方法
    • sort方法实际上是在切片上定义的
    • len方法也是切片的方法之一
  3. 向量

    • 使用collect时,通常要指定类型,因为它可以构建出不同种类的集合,而不仅仅是向量
    • 与数组一样,可以对向量使用切片的方法
      • reverse方法实际上是在切片上定义的,此调用会隐式地从向量中借用一个&mut [&str]切片并在其上调用reverse
    • Vec是Rust的基本数据类型,它几乎可以用在任何需要动态大小的列表的地方
    • 如果事先知道向量所需的元素数量,就可以调用Vec::with_capacity而不是Vec::new来创建一个向量,它的缓冲区足够大,可以从一开始就容纳所有元素
    • 许多库函数会寻求使用Vec::with_capacity而非Vec::new的机会
  4. 切片

    • 切片数组向量中的一个区域
    • 对切片的引用是一个胖指针
      • 一个双字值
      • 包括指向切片第一个元素的指针切片中元素的数量

字符串类型

  1. 字符串字面量
    • 原始字符串用小写字母r进行标记,原始字符串不识别任何转义序列
    • 可以再原始字符串的开头和结尾添加#做标记
  2. 字符串
    • 带有b前缀的字符串字面量都是字节串,这样的字节串是u8值(字节)的切片而不是Unicode文本

      let method = b"POST";  // method类型是&[u8; 4]
      assert_eq!(method, &[b'P', b'O', b'S', b'T']);
    • 字节串不能包含任意Unicode字符,它们只能使用ASCII\xHH转义序列

  3. 内存中的字符串
    • Rust字符串是Unicode字符序列,但并不以char数组的形式存储在内存中,而是用了UTF-8形式(可变宽度编码)
    • String&str.len()方法会返回其长度
    • &mut str类型确实存在,但它没什么用
    • &mut str上唯一可用的操作是make_ascii_uppercasemake_ascii_lowercase,根据定义,它们会就地修改文本并且只影响单字节字符
  4. String
    • &str非常像&[T],是指向某些数据的胖指针
    • String则类似于Vec<T>
    • String有几种方法
      • .to_string()方法会将&str转换为String,这会复制此字符串
      • .to_owned()方法会做同样的事情,也会以同样的方式使用
      • format!()宏的工作方式与println!()类似,但它会返回一个新的String
      • .concat().json(sep),字符串数组、切片、向量都有这两个方法,它们会从许多字符串中形成一个新的String
  5. 其它类似字符串的类型
    • 对于Unicode文本,坚持使用String&str
    • 当使用文件名,请改用std::path::PathBuf&Path
    • 当处理根本不是UTF-8编码的二进制数据时,请使用Vec<u8>&[u8]
    • 当使用操作系统提供的原生形式环境变量名命令行参数时,请使用OsString&OsStr
    • 当和使用null结尾字符串的C语言库进行互操作时,请使用std::ffi::CString&CStr

所有权与移动

  1. 谈及内存管理,我们希望编程语言具备两个特点

    • 能在我们选定的时机及时释放,这使得我们能控制程序的内存消耗
    • 对象被释放后,绝不希望继续使用使用指向它的指针,这是未定义行为,会导致崩溃和安全漏洞
  2. 几乎所有主流编程语言都只能在两个阵营"二选一"

    • 安全优先:通过垃圾回收机制,在所有指向对象的可达指针都消失后,自动释放对象
    • 控制优先:让开发者自己负责释放内存,程序的内存消耗完全掌握在开发者受众。
  3. Rust通过限制程序使用指针的方式出人意料的打破了这种困局

    • 在运行期,指针仅仅是内存中的地址,和在C与C++中一样
    • 而不一样的是,Rust编译器已然证明你的代码在安全地使用它们
  4. 所有权

    • 每个值都有唯一的拥有者
    • 拥有者及其拥有的哪些值形成了一棵
      • 值的拥有者是值的父节点
      • 值拥有的值是子节点
      • 每棵树的总根都是一个变量
      • 当该变量超出作用域时,整棵树都将随之消失
    • Rust的单一拥有者规则将禁止任何可能让它们排列得比树结构更复杂的可能性
    • Rust程序中的每一个值都是某棵树的成员,树根是某个变量
  5. 为了处理某些场景,Rust从几个方面扩展了这种简单的思想

    • 可以将值从一个拥有者转移给另一个拥有者,
      • 允许你构建、重新排列和拆除树形结构
    • 像整型、浮点数和字符这样的非常简单的类型,不受所有权规则的约束
      • 这些称为Copy类型
    • 标准库提供了引用计数指针类型RcArc,它们允许在某些限制下有多个拥有者
    • 可以对值进行"借用"(borrow),以获得值的引用。
      • 这种引用是非拥有型指针,有着受限的生命周期
  6. 移动

    • 对大多数类型来说,像为变量复制、将其传给函数或从函数返回的操作都不会复制值,而是移动值。
    • 源会把值的所有权转移给目标并变回未初始化状态,改由目标变量来控制值的生命周期
      • 将参数传给函数会将所有权转移给函数的参数
      • 从函数返回一个值会将所有权转给调用者
      • 构建元组会将值转给元组
    • 值移动涉及字符串、向量和其他占用大量内存且复制成本高昂的类型
    • 移动让这些类型的所有权清晰且赋值开销极低
  7. Copy类型:关于移动的例外情况

    • 对于Copy类型的值进行赋值会复制这个值,而不会移动它
    • 赋值的源仍会保持已初始化和可用状态,并且具有与之前相同的值
    • 标准的Copy类型包括:
      • 整数类型、浮点类型、char类型、bool类型,以及其他类型
    • 任何在丢弃值时需要做一些特殊操作的类型都不能是Copy类型
    • 如果结构体的所有字段都是Copy类型,那么可以通过属性#[derive(Copy,Clone)]放置在此定义之上来创建Copy类型
    • Rust的一个原则是:
      • 各种开销对程序员来说应该是显而易见的
      • 基本操作必须保持简单,而潜在的昂贵操作应该是显式的
  8. RcArc:共享所有权

    • RcArc非常相似,唯一的区别是Arc可以安全地在线程之间直接共享
    • 普通Rc会使用更快的非线程安全代码来更新引用计数
    • 克隆一个Rc<T>值并不会复制T,相反,它只会创建另一个指向它的指针并通过递增引用计数
    • 通常的所有权规则适用于Rc指针本身,当丢弃最后一个Rc时,Rust也会丢弃T
      • 资源交给Rc管理,Rc不存在后则一切都不存在了
    • Rust的内存和线程安全保证的基石是:
      • 确保不会有任何值时既共享可变
      • Rust假Rc指针的引用目标通常都可以共享,因此就不能是可变的
    • 弱引用指针std::rc::Weak来避免建立Rc指针循环。
  9. 小结

    • 所有权:每个值有唯一的所有者
    • 移动:涉及特殊操作的赋值会发生所有权转移
    • Copy:简单类型的赋值操作会复制值
    • Rc和Arc:共享值的所有权
    • 移动和引用计数指针是缓解所有权严格性问题的两种途径
    • 第三种途径是借用对值的引用

引用

  • 迄今为止,我们所看到的所有指针类型都是拥有型指针,意味着当拥有者被丢弃时,它的引用目标也会随之消失
    • 如:简单的Box<T>堆指针,String值,Vec值内部的指针
  • Rust还有一种名为引用(reference)非拥有型指针,这种指针对引用目标生命周期毫无影响
  • 引用生命周期决不能超出其引用目标
  • Rust把创建对某个值的引用的操作称为**借用(borrow)**那个值:凡是借用,终须归还
  • 引用本身确实没什么特别之处-说到底,它们只是地址而已
    • 但用以让引用保持安全的规则,对Rust来说是一种创新
  1. 对值的引用

    • 引用能让在不影响其所有权的情况下访问值
      • 共享引用:允许你读取但不能修改其引用目标,可以同时拥有任意数量对特定值的共享引用。
        • 类型写成:&T
        • 共享引用是Copy类型
      • 可变引用:允许你读取和修改值,一旦一个值拥有了可变引用,就无法对该值创建其它任何引用。
        • 类型写成: &mut T
        • 可变引用不是Copy类型
    • 可以将共享引用和可变引用之间的区别视为在编译期强制执行多重读取单一写入
      • 这条规则不仅适用于引用,也适用于所引用值的拥有者
      • 只要存在一个值的共享引用,即使是它的拥有者也不能修改它,该值会被锁定
    • 事实证明:让共享和修改保证完全分离对内存安全至关重要
  2. 由于引用在Rust中随处可见,因此**.运算符**就会按需对其左操作数隐式解引用

    struct Anime { name: &'static str, bechdel_pass: bool};
    let aria = Anime { name: "Aria: The Animation", bechdel_pass: true};
    let anime_ref = &aria;
    assert_eq!(anime_ref.name, "Aria: The Animation");
    • 在Rust中使用&运算符*运算符来创建引用(借用)和追踪引用(解引用),不过.运算符不需要做这种转换,它会隐式借用和解引用
  3. 对引用进行引用

    struct Point {x: i32, y: i32};
    let point = Point {x: 1000, y: 729};
    let r: &Point = &point;
    let rr: &&Point = &r;
    let rrr: &&&Point = &rr;
    • .运算符会追踪尽可能多层次的引用来找到它的目标
  4. 比较引用

    • 就像.运算符一样,Rust的比较运算符也能看穿任意数量的引用
    • 比较运算符操作数(包括引用型操作数)必须是完全相同类型
      • 比如如上代码中rrr类型不匹配
    • 两个引用是否指向同一块内存,可以使用std::ptr::eq,会对两者作为地址进行比较
  5. 引用永不为空

    • 在Rust中,如果需要用一个值来表示对某个可能不存在事物的引用,请使用Option<&T>
    • 在机器级别,Rust会将None表示为空指针,将Some(r)表示为非零地址,因此Option<&T>与C/C++中的可空指针一样高效,但更安全
      • 它的类型要求你在使用之前必须检查它是否为None
  6. 对切片和特型对象的引用

    • Rust还包括两种胖指针,即携带某个值地址的双字值,以及要正确使用该值所需的某些额外信息
      • 对切片的引用就是一个胖指针,携带着此切片的起始地址及其长度
      • 另一种胖指针是特型对象,即对实现了指定型的值的引用
  7. Rust的可变与共享规则

    • 共享访问是只读访问
      • 对于共享借用,这条路径是只读的
    • 可变访问是独占访问
      • 对于可变借用,这条路径是完全不可访问的
      • 通过要求可变访问必须是独占的,Rust避免了一大类日常错误
    • 在编写并发代码时,共享引用和可变引用的互斥性确实证明了其价值

表达式

  • Rust中完全面向表达式的控制流
    • if表达式可用于初始化变量
    • match表达式可以作为参数传给函数或宏
    • 块是一种最通用的表达式,一个块生成一个值
    • if let表达式其实只有一个模式的match表达式的简写形式
    • match表达式的所有分支都必须具有相同的类型
    • while循环for循环的值总是()
    • loop表达式就能生成一个值
      • loop中所有break表达式也必须生成具有相同类型的值,这样该类型就会成为这个loop本身的类型
    • ..运算符会生成一个范围(range),即具有两个字段(start和end)的简单结构体
    • break表达式会退出所在循环
  • 几个重要的自动转换
    • &String类型的值会自动转换为&str类型,无须强制转换
    • &Vec<i32>类型的值会自动转换为&[i32]
    • &Box<Chessboard>类型的值自动转换为&Chessboard
    • 以上这些被称为隐式解引用,因为它们适用于所有实现了内置特型Deref的类型
      • Deref隐式转换的目的是使智能指针类型的行为尽可能像其底层值
      • 多亏了DerefBox<Chessboard>的用法基本上和普通Chessboard的用法一样

crate与模块

  • crate
    • 每个crate都是既完整又内聚的单元
    • 包含单个库或可执行程序的所有源代码
  • crate是关于项目间代码共享的,而模块是关于项目内代码组织的
    • 它们(crate与module)扮演着Rust命名空间的角色,是构成Rust程序或库的函数、类型、常量等的容器
  • 函数标记为pub(crate)
    • 那么就意味着它可以再这个crate的任何地方使用
    • 但不会作为外部接口的一部分公开
  • 预导入(prelude)
    • 它们通过收集几乎所有用户都需要的常用导入,减少了样板代码的编写
    • 把一个模块命名为prelude只是一种约定,旨在告诉用户应该使用*导入它
  • 模块
    • 目录可以作为一个模块,目录名为模块名
      • 目录中包含mod.rs,表示是一个模块,类似Python语言的__init__.py
    • 文件可以作为一个模块,文件名为模块名
    • 自定义文件中的模块
      • 使用mod关键字
    • ::运算符用于访问模块中的各项特型
    • 关键字supercrate在路径中有着特殊的含义
      • super指的是父模块
      • crate指的是当前模块所在的crate
    • as关键字可以给导入的内容重命名
    • 绝对路径
      • ::开头,总会引用外部crate
    • src/lib.rs中代码构成了库的根模块

结构体

  • Rust有三种结构体类型

    • 具名字段型结构体
    • 元组型结构体
    • 单元型结构体
  • 具名字段型结构体

    • Rust中的约定是
      • 所有类型(包括结构体)的名称都将每个单词的第一个字母大写,称为大驼峰式
      • 字段和方法是小写的,单词之间用下划线分隔,称为蛇形格式
    • 结构体默认情况下是私有的
      • 仅在声明它们的模块及其子模块中可见
      • 即使一个结构体声明为pub,它的字段也可以是私有的
      • 其它模块可以使用此结构体及其任何公共的关联函数,但不能按名称访问私有字段或使用结构体表达式创建新值
  • 用impl定义方法

    • impl块只是fn定义的集合,每个定义都会成为块顶部命名的结构体类型上的一个方法
    • impl块中定义的函数称为关联函数,因为它们是与特定类型相关联的
    • Rust会将调用关联函数的结构体值作为第一个参数传给方法
      • 该参数必须具有特殊名称self
      • 由于self的类型显然就是在impl块顶部命名的类型或对该类型的引用
      • self的不同形式
        • self
        • &self
        • &mut self
  • BoxRcArc形式传入self

    • 方法的self参数也可以是Box<Self>类型,Rc<Self>类型或Arc<Self>类型
    • Rust会自动从BoxRcArc等指针类型中借入引用
      • 因此&self&mut self几乎总是方法签名里的正确选择
  • 类型关联函数

    • impl块还可以定义根本不以self参数的函数
    • 这些函数仍然是关联函数,因为它们在impl块中
    • 但它们不是方法,因为它们不接受self参数
    • 为了将它们与方法区分开来,我们称其为类型关联函数
  • 泛型结构体

    • Rust结构体可以是泛型的
      • 在泛型结构体定义中,尖括号(<>)中的类型名称叫作类型参数
      • Self参数定义为我们要为其添加方法的任意类型。
      • 调用关联函数时,使用::<>(比目鱼)表示法显式地提供类型参数
  • 带生命周期参数的泛型结构体

    • 如果结构体类型包括引用,则必须为这些引用的生命周期命名
  • 带常量参数的泛型结构体

    • 常量泛型参数可以是任意整数类型charbool
    • 不允许使用浮点数、枚举和其他类型
    • 如果结构体还接受其他种类的泛型参数
      • 生命周期参数必须排在第一位,然后是类型,接下来是任何const值
  • 内部可变性

    • 我们需要一个不可变值中的一丁点儿可变数据,称为内部可变性
    • Cell<T>是一个包含类型T的单个私有值的结构体
    • Cell唯一的特殊之处在,即使你对Cell本身没有mut访问权限,也可以获取和设置这个私有字段
    • Cell只是改变不变性规则的一种安全方式
      • 一丝不多,一毫不少
      • Cell不允许在共享值上调用mut方法
      • .get()方法会返回Cell中值的副本,因此它仅在T实现了Copy特型时才有效
    • Cell很容易使用
      • 虽然不得不调用.get().set().borrow().borrow_mut()略显尴尬
      • 但这就是我们为违反规则而付出的代价
    • Cell不是线程安全的
    • Cell不用,RefCell支持借用对其T值的引用

枚举与模式

  • Rust模式有点像针对所有数据的正则表达式
  • 枚举可以是泛型的,如Option<T>,Result<T>
  • 只能用一种安全的方式来访问枚举中的数据,即使用模式
  • match表达式中,模式就是出现在=>符号前面的部分
  • 表达式会生成值,模式会消耗值
  • 模式匹配可以和枚举协同工作,甚至可以测试它们包含的数据,这让match成了C的switch语句的强大而灵活的替代品
  • match也可用来匹配其他类型
    • 字面量、变量、通配符等
    • 字面量也可以用作模式,包括布尔值、字符,甚至字符串
    • 不关心匹配的值,那么可以用单个下划线_作为模式,这就是通配符模式
  • 元组型模式匹配元组
  • 结构体型模式使用花括号,就像结构体表达式一样
  • 数组型模式与切片模式
    • 切片模式不仅匹配值,还匹配长度
  • 引用型模式
    • ref模式会借用己匹配值的一部分
    • &模式会匹配引用`
    • 匹配不可复制的值会移动该值

特型与泛型

  • Rust通过两个相关联的特性来支持多态
    • 特型泛型
  • 特型
    • 是Rust体系中的接口抽象基类
    • 是一种语言特性,我们可以说某类型支持或不支持某个特型
    • 大多数情况下,特型代表着一种能力,即一个类型能做什么
    • 特型本身必须在作用域内
      • CloneIterator的各个方法没有导入就能工作,因为默认它们始终在作用域中,是标准库一部分
      • 预导入主要就是一些精心挑选的特型
  • 泛型
    • 是Rust中多态的另一种形式
  • 界限
    • 对类型T可能的类型范围做了限制
  • 泛型和特型的紧密相关
    • 泛型函数会在限界中使用特型来阐明它能针对哪些类型的参数进行调用
  1. 为什么向类型添加特型不需要额外的内存,以及如何在不需要虚方法调用开销的情况下使用特型

  2. 特型对象

    • 在Rust中使用特型编写多态代码有两种方式:
      • 特型对象
      • 泛型
    • 特型类型引用叫作特型对象
    • Rust通常无法在编译期间知道引用目标的类型,因此特性对象要包含一些关于引用目标类型的额外信息
    • 特型对象的内存布局
      • 特型对象是一个胖指针
      • 指向值的指针指向表示该值类型虚表的指针组成
      • 在Rust中,虚表只会在编译期生成一次,并由同一类型的所有对象共享
      • 当调用特型对象的方法时,该语言会自动使用虚表来确定调用哪个实现
      • 在C++中,虚表指针vptr是作为结构体的一部分存储,而Rust是胖指针方案
      • Rust中,结构体本身只包含自己的字段
        • 这样一来,每个结构体就可以实现几十个特型而不必包含几十个vptr
      • Rust在需要时会自动将普通引用转换为特型对象
      • 创建特型对象的唯一方法
        • &mut dyn train_name
        • &mut dyn WriteBox<dyn Write>也是一个胖指针,包含写入器本身的地址和虚表的地址
  3. 特型对象还是泛型代码的选择相当微妙

    • 由于这两个特性都基于特型
    • 当需要一些混合类型值的集合时,特型对象是正确的选择
  4. 特型对象相比,泛型具有三个重要优势

    • 第一个优势是速度,没有dyn则不会有动态派发,没有运行期开销
    • 第二个优势在于并不是每个特性都能支持特型对象
    • 第三个优势是它很容易同时指定具有多个特型和泛型参数限界,特型对象不能这样
  5. 定义与实现特型

    • 定义特型很简单,给它一个名字并列出特型方法的类型签名即可
    • 要实现特型,请使用语法impl TraitName for Type
    • 特型impl代码中定义一切都必须是真正属于此特型的
    • 自定义函数不能放在特型的impl代码中,只能包含在单独的impl块
    • 特型的impl代码中的一切都必须是真正属于此特型
    • 特型中有未实现的函数,也有已实现的函数
  6. 特型中的Self

    • 特型可以用关键字Self作为类型
    • 使用了Self类型的特型特型对象不兼容
    • 特型对象实际上是为最简单的特型类型而设计的
    • 特型的高级特性很有用,但它们不能与特型对象共存
      • 因为一旦有了特型对象,就会失去Rust对你的程序进行类型检查时所必须的类型信息
  7. impl Trait

    • 是一种静态派发形式
    • Rust不允许特型方法使用impl Trait作为返回值
    • 只有最简单的泛型函数中才能把impl Trait参数用作类型,参数的类型之间不能存在关系
  8. 类型限界的另一个优点

    • 当遇到编译期错误时,至少编译期可以告诉你问题出在哪里
    • 限界就这么写在代码和文档中,你可以查看Rust中泛型函数的签名
    • 并准确了解它能接受的参数类型
    • 而使用模版则做不到这些
  9. 以特型为基础

    • 特型泛型在所有这些主题中都扮演着核心角色
鱼雪