在学习 Rust 时,我了解到它是一种严格的、静态类型的语言。尤其是它没有函数重载或可选参数这种特性。但是,当我看到 Axum 时,我惊讶地发现了这种代码:
1 | let app = Router::new() |
get
方法可以接收一个各种类型的函数作为参数!这是什么黑魔法?
为了搞清楚,我写了一个简单的例子:
1 | fn print_id(id: Id) { |
例子中,有一个 trigger
,它接收一个 Context
和一个函数作为参数。作为参数的函数可以接收 1 个或 2 个参数,参数类型为 Id
或 Param
类型。神奇吗?
让我们来看看到底是怎么实现的,首先是 Context
:
1 | struct Context { |
Context
可以类比为 Axum 里的 Request
,它是 print_id
和 print_all
函数里面数据的来源,这个例子中它仅包括两个字段。
接下来是第一个秘诀, FromContext
trait:
1 | trait FromContext { |
它使得我们可以创建各种 “Extractors” 来提取里面的内容,比如 Param
会提取里面的 param
字段:
1 | struct Param(String); |
第二个秘诀是 Handler
trait:
1 | trait Handler<T> { |
我们会为 Fn<T>
类型来实现这个 trait:
1 | impl<F, T> Handler<T> for F |
这样的话,我们在函数调用和它的参数之间就有了一个 “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)
:
1 | impl<T1, T2, F> Handler<(T1, T2)> for F |
译者注:通过宏,可以一次性实现不定参数的情况,例:
1 | macro_rules! all_the_tuples { |
最后实现 trigger
函数就搞定了:
1 | fn trigger<T, H>(context: Context, handler: H) |
现在,让我解释下下面代码:
1 | let context = Context::new("magic".into(), 33); |
print_id
是 Fn(Id)
类型,它实现了 Handler<Id>
,当调用 handler.call
方法时,相当于执行如下代码:
1 | print_id(Id::from_context(&context)); |
魔术揭秘!
附完整代码:
1 |
|