Rust Axum 风格的魔术函数参数示例
Rust 是一种严格的静态类型语言。具体来说,它没有函数重载或可选的函数参数。
直到我在 Axum 中惊讶地看到这样的东西:
let app = Router::new()
.route("/users", get(get_users))
.route("/products", get(get_product));
async fn get_users(Query(params): Query<Params>) -> impl IntoResponse {
let users = /* ... */
Json(users)
}
async fn get_product(State(db): State<Db>, Json(payload): Json<Payload>) -> String {
let product = /* ... */
product.to_string()
}
该 get
方法可以接收指向各种类型的函数指针!这是什么黑魔法?
我想我必须创建一个简化版本来以便了解这个实现方式。
fn print_id(id: Id) {
println!("id is {}", id.0);
}
// Param(param) is just pattern matching
fn print_all(Param(param): Param, Id(id): Id) {
println!("param is {param}, id is {id}");
}
pub fn main() {
let context = Context::new("magic".into(), 33);
trigger(context.clone(), print_id);
trigger(context.clone(), print_all);
}
在示例中,我们有一个 trigger
函数接收 Context
对象和函数指针。函数指针可能会接收 Id
or Param
类型的 1 或 2 个参数。魔法?
组件
让我们看一下实现此目的的组件
Context
struct Context {
param: String,
id: u32,
}
在Axums 的情况下,Context
是接收状态 Request
。这是我们的函数想要接收的“部分”的来源。在这个简化的示例中,它包含两个数据字段
FromContext Trait
trait FromContext {
fn from_context(context: &Context) -> Self;
}
第一个技巧是 FromContext
Trait。它将允许我们创建从 Context
对象中提取必要数据的“提取器”。例如
pub struct Param(pub String);
impl FromContext for Param {
fn from_context(context: &Context) -> Self {
Param(context.param.clone())
}
}
这个特征将允许我们持有一个 Context
, 但调用一个期望 Param
的函数. 稍后会详细介绍
Handler Trait
trait Handler<T> {
fn call(self, context: Context);
}
第二个技巧是 Handler trait。我们将实现闭包类型的 Fn(T)
特征。是的,我们可以实现闭包类型的特征。这个实现将允许我们在函数调用和它的参数之间有一个“中间件”。 在这里,我们将调用该方法 FromContext::from_context
,将上下文转换为预期的函数参数,即 Param
或 Id
.
impl<F, T> Handler<T> for F
where
F: Fn(T),
T: FromContext,
{
fn call(self, context: Context) {
(self)(T::from_context(&context));
}
}
为了支持多个函数参数,我们将继续为具有 2、3、4 等数量参数的闭包类型实现 Handler
。这里有趣的一点是,此实现与参数的顺序无关 - 它将支持 fn foo(p: Param, id: Id)
和 fn foo(id: Id, p: Param)
!
impl<T1, T2, F> Handler<(T1, T2)> for F
where
F: Fn(T1, T2),
T1: FromContext,
T2: FromContext,
{
fn call(self, context: Context) {
(self)(T1::from_context(&context), T2::from_context(&context));
}
}
将一切整合在一起
该 trigger
函数的实现现在很简单
pub fn trigger<T, H>(context: Context, handler: H)
where
H: Handler<T>,
{
handler.call(context);
}
让我们检查一下此调用会发生什么情况
let context = Context::new("magic".into(), 33);
trigger(context.clone(), print_id);
print_id
Fn(Id)
的类型具有 的Handler<Id>
实现。- 调用
Handler::call
该方法,我们Id::from_context(context)
从中返回结构体的Id
实例。 print_id
使用它期望的参数调用。
魔法揭开了神秘面纱。