译:揭秘神奇的 Rust Axum 风格的函数实现(Rusts Axum style magic function params example)

原文链接:github.com/alexpusch/r...

原文作者:Alex Puschinsky

在学习 Rust 时,我了解到它是一种严格的、静态类型的语言。尤其是它没有函数重载或可选参数这种特性。但是,当我看到 Axum 时,我惊讶地发现了这种代码:

rust 复制代码
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 方法可以接收一个各种类型的函数作为参数!这是什么黑魔法?

为了搞清楚,我写了一个简单的例子:

rust 复制代码
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 和一个函数作为参数。作为参数的函数可以接收 1 个或 2 个参数,参数类型为 Id 或 Param 类型。神奇吗?

让我们来看看到底是怎么实现的,首先是 Context:

rust 复制代码
struct Context {
    param: String,
    id: u32,
}

Context 可以类比为 Axum 里的 Request,它是 print_id 和 print_all 函数里面数据的来源,这个例子中它仅包括两个字段。

接下来是第一个秘诀, FromContext trait:

rust 复制代码
trait FromContext {
    fn from_context(context: &Context) -> Self;
}

它使得我们可以创建各种 "Extractors" 来提取里面的内容,比如 Param 会提取里面的 param 字段:

rust 复制代码
struct Param(String);

impl FromContext for Param {
    fn from_context(context: &Context) -> Self {
        Param(context.param.clone())
    }
}

第二个秘诀是 Handler trait:

rust 复制代码
trait Handler<T> {
    fn call(self, context: Context);
}

我们会为 Fn<T> 类型来实现这个 trait:

rust 复制代码
impl<F, T> Handler<T> for F
where
    F: Fn(T),
    T: FromContext,
{
    fn call(self, context: Context) {
        self(T::from_context(&context));
    }
}

这样的话,我们在函数调用和它的参数之间就有了一个 "middleware"。这里我们将调用 FromContext::from_context 方法,将上下文转换为预期的函数参数,即 Param 或 Id。

译者注:执行 impl<F, T> Handler<T> for F 后,相当于为 Fn<T> 类型实现了 Handler 这个 trait,即 print_id 实现了 Handler,可以调用 call 方法,而 call 方法中的 self 就是 print_id。

我们继续添加代码,支持两个函数参数的情况。有趣的是,这个实现与参数的顺序无关--它同时支持 fn foo(p: Param, id: id) 和 fn foo(id: id, p: Param):

rust 复制代码
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));
    }
}

译者注:通过宏,可以一次性实现不定参数的情况,例:

rust 复制代码
macro_rules! all_the_tuples {
    ($name:ident) => {
        $name!([]);
        $name!([T1]);
        $name!([T1, T2]);
        $name!([T1, T2, T3]);
        // Can add more
    };
}

macro_rules! impl_handler {
    (
        [$($ty:ident),*]
    ) => {
        impl<F, $($ty,)*> Handler<($($ty,)*)> for F
        where
            F: Fn($($ty,)*),
            $( $ty: FromContext,)*
        {
            fn call(self, context: Context) {
                self($( $ty::from_context(&context), )*);
            }
        }
    };
}

all_the_tuples!(impl_handler);

最后实现 trigger 函数就搞定了:

rust 复制代码
fn trigger<T, H>(context: Context, handler: H)
where
    H: Handler<T>,
{
    handler.call(context);
}

现在,让我解释下下面代码:

rust 复制代码
let context = Context::new("magic".into(), 33);

trigger(context.clone(), print_id);

print_id 是 Fn(Id) 类型,它实现了 Handler<Id>,当调用 handler.call 方法时,相当于执行如下代码:

rust 复制代码
print_id(Id::from_context(&context));

魔术揭秘!

附完整代码:

rust 复制代码
#[derive(Clone, Debug)]
struct Context {
    param: String,
    id: u32,
}

impl Context {
    fn new(param: String, id: u32) -> Context {
        Context { param, id }
    }
}

trait FromContext {
    fn from_context(context: &Context) -> Self;
}
trait Handler<T> {
    fn call(self, context: Context);
}

struct Param(String);

impl FromContext for Param {
    fn from_context(context: &Context) -> Self {
        Param(context.param.clone())
    }
}

struct Id(u32);

impl FromContext for Id {
    fn from_context(context: &Context) -> Self {
        Id(context.id.clone())
    }
}

macro_rules! all_the_tuples {
    ($name:ident) => {
        $name!([]);
        $name!([T1]);
        $name!([T1, T2]);
        $name!([T1, T2, T3]);
    };
}

macro_rules! impl_handler {
    (
        [$($ty:ident),*]
    ) => {
        impl<F, $($ty,)*> Handler<($($ty,)*)> for F
        where
            F: Fn($($ty,)*),
            $( $ty: FromContext,)*
        {
            fn call(self, context: Context) {
                self($( $ty::from_context(&context), )*);
            }
        }
    };
}

all_the_tuples!(impl_handler);

fn trigger<T, H>(context: Context, handler: H)
where
    H: Handler<T>,
{
    handler.call(context);
}

fn print_id(id: Id) {
    println!("id is {}", id.0);
}

fn print_param(Param(param): Param) {
    println!("id is {}", param);
}

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_param);
    trigger(context.clone(), print_all);
}
相关推荐
红尘散仙9 小时前
谁说终端只能敲键盘?我用 Rust 给小说阅读器加上了鼠标
前端·后端·rust
unconditional2 天前
我拆了 sdkm 的回滚设计:每个写操作,都提前写好了翻车预案
rust·开源
数字幽灵2 天前
开源!我用 Rust 手搓了一个 Agent 原生的鸿蒙逆向分析平台
rust
Kapaseker3 天前
Rust 不用写 for 循环?看我怎么写!
rust
奶油话梅糖3 天前
打开项目 0.9 秒、内存只有 IDEA 的 1/12:OpenIDEA 用 Rust 把 IntelliJ IDEA 重造了一遍
java·rust·github·intellij-idea
Vex2une4 天前
Google Picasa 精神续作:Rust 重写的离线本地相册工具‌
rust·图片管理·picasa·马良·本地照片管理·重复图片检测·maliang
unconditional5 天前
我拆了 sdkm 的自定义 SDK 机制:所谓“内置”,就是官方提前帮你填好的表
rust·开源
Ramble_Naylor6 天前
Rust深度解读:Send 与 Sync 的本质与线程安全边界
python·算法·rust
Ramble_Naylor6 天前
Rust深度解读:异步流(Async Streams)与生成器的处理艺术
开发语言·后端·rust
RobinDevNotes6 天前
Memvid 把 Agent 长期记忆塞进一个文件
人工智能·rust·openresty