在这篇博客中,你将看到:
- 如何在 Rust 中将复杂类型进行类型擦除统一保存
- 一些简单的 TDD (Test-Driven Development,测试 驱动开发)实践
- 一些简单的 mock 测试方法
如果你在 Rust 中使用过 Actix-web、Rocket 等框架,那你应该接触过这种注册服务的方法:
App::new()
.handler(|| {})
.handler(|s: String| "hello, world!")
.handler(|db: DBConnection, id: Path<u32>| Error::new())
.start();
简单来说,App
接受几个函数指针,只要函数指针的输入参数和输出参数类型是被支持的(比如实现了 FromRequest
和 ToResponse
trait),就被注册为服务,而后接受来自 App
的请求。本系列博文将会尝试实现类似这样的一个简单系统,主要聚焦于其中的类型擦除部分。
你可能想问,这有什么难的?无非就是把这些指针都保存到一个 Box<dyn Fn>
里面罢了。emmmm,对,也不对。大致思路是这样的,但是其中涉及到很多细节的、对初学者并不显然的东西,而这正是本篇博客关注的重点。如果你对如何实现这些细节很清楚,那你可以关掉这篇文章了。
1. Start Simple
首先,让我们从最简单的一个例子出发:app 不支持任何重载,只接受类型为 Fn() -> ()
的 handler。
我们先将环境搭建起来,在命令行中输入:
> cargo new --lib type_erase_exercise
> cd type_erase_exercise
# src/lib.rs
#![allow(unused)]
mod start_simple;
# src/start_simple.rs
struct App {}
好了,这就是我们的最基础框架了。运行 cargo test
,你应该通过——当然,因为我们还没写任何实际测试。
接下来,让我们先写一个测试,再开始写功能实现——这就是 TDD,测试驱动开发,先写测试再写实现。
# src/start_simple.rs
# 文件尾
#[test]
fn test_start_simple() {
fn f1() {
println!("f1 called.");
}
fn f2() {
println!("f2 called.");
}
fn f3() {
println!("f3 called.");
}
let app = App::new()
.handler(f1)
.handler(|| println!("lambda called."))
.handler(f2)
.handler(f3);
app.dispatch();
}
嗯,看起来不错,让我们编译一下——cargo c
——编译不通过。当然,因为我们还没写实现。让我们先写一个最简单的实现:
# src/start_simple.rs
struct App {
handlers: Vec<Box<dyn Fn() -> ()>>,
}
impl App {
pub fn new() -> Self {
Self { handlers: vec![] }
}
pub fn handler(mut self, f: impl Fn() -> ()) -> Self {
self.handlers.push(Box::new(f));
self
}
pub fn dispatch(&self) {
// TODO
}
}
闭包、Fn
trait 和生命周期
再来编译一下——cargo c
——哎呀,又出错了:
error[E0310]: the parameter type `impl Fn() -> ()` may not live long enough
--> src\start_simple.rs:9:28
|
8 | pub fn handler(mut self, f: impl Fn() -> ()) -> Self {
| --------------- help: consider adding an explicit lifetime bound...: `impl Fn() -> () + 'static`
9 | self.handlers.push(Box::new(f));
| ^^^^^^^^^^^ ...so that the type `impl Fn() -> ()` will meet its required lifetime bounds
这是什么意思呢?注意到我们接受的是 Fn() -> ()
——闭包也实现了 Fn
trait,而且可以捕获外部变量,这就意味着它们可能带有生命周期。如果我们注册服务的时候传入了一个捕获了栈上变量的闭包并在退栈之后调用这个闭包——boom,内存非法访问。
例如像这样的代码:
fn create_app() -> App {
let i = 1u32;
App::new()
.handler(|| println!("i = {}", i); )
}
如果我将创建的 app
传回给上层函数再调用 start
,那它引用闭包的时候 i
应该指向哪块地址?感谢 Rust,我们在编译期就发现了这个问题!
感谢归感谢,怎么通过编译呢?本篇博文重点不是在生命周期处理,所以我们直接简单粗暴将传入参数的生命周期要求为 'static
——作为处理函数不应该捕获任何栈上的变量。
# src/start_simple.rs
pub fn handler(mut self, f: impl Fn() -> () + 'static) -> Self {
self.handlers.push(Box::new(f));
self
}
好了,现在应该通过编译了,而且我们有信心它不会导致上述的内存安全问题。
增加测试的能力!
现在让我们跑一下测试——cargo test
——测试通过。 全文结束,谢谢大家
注意到我们根本没有实现调用的逻辑,但依然通过了测试。这是正常的,因为我们在测试中没有写任何断言。对于一个请求,我们会希望所有注册到的 handler 都被调用一次。这类断言在测试中相当常见,测试中的 mock 就包括了这类断言。对于 rust 生态,我们可以用 mockall
库来实现。让我们来引入它。
因为我们只会在测试中使用它,所以我们将其放在 dev-dependencies
中。
# Cargo.toml
[dependencies]
[dev-dependencies]
mockall = "0.9.1"
我们用它来写一些断言:每一次 dispatch
调用,每个 handler
都应该被调用一次。
# src/start_simple.rs
#[test]
fn test_start_simple() {
use mockall::*;
#[automock]
pub trait Handler {
fn f1();
fn f2();
fn f3();
}
let f1_ctx = MockHandler::f1_context();
f1_ctx.expect().times(1).returning(|| {});
let f2_ctx = MockHandler::f2_context();
f2_ctx.expect().times(1).returning(|| {});
let f3_ctx = MockHandler::f3_context();
f3_ctx.expect().times(1).returning(|| {});
let app = App::new()
.handler(MockHandler::f1)
.handler(|| MockHandler::f2())
.handler(MockHandler::f3);
app.dispatch();
}
整挺好。现在我们再来测试一下—— cargo t
——
running 1 test
test start_simple::test_start_simple ... FAILED
failures:
---- start_simple::test_start_simple stdout ----
thread 'start_simple::test_start_simple' panicked at 'MockHandler::f3: Expectation(<anything>) called fewer than 1 times', src\start_simple.rs:23:5
好耶!失败了!因为我们还没写 dispatch
的逻辑,没有调用注册的 handler。现在我们补全 dispatch
的逻辑:
# src/start_simple.rs
pub fn dispatch(&self) {
for handler in self.handlers.iter() {
(handler)()
}
}
再次测试,通过!好耶!
running 1 test
test start_simple::test_start_simple ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
到此为止,我们的第一个简易玩具完成,它只是一个最简单的实现。接下来的部分,我们将会处理更复杂一点的类型擦除。
2. 真正的实现
复制一份 start_simple.rs
到 second_try.rs
并在 lib.rs
中增加 mod second_try
。
上面的例子太简单了,读者应该都能自己写出来。下面我们将实现一个更为复杂的泛型注册方法,它接受这样一类函数指针,函数的参数可以是多个或 0 个,每个入参都是支持的类型,没有返回值。
我们将“请求”定义为一个 String
,将“支持的类型定为 String
、u32
、()
。我们将尝试从请求中解析出对应的参数传递给注册的 handler,并不考虑解析失败的情况。
代码未动,测试先行
首先先修改我们的测试方法。在 TDD 中,总是先写测试,再开始编写代码。
# src/second_try.rs
/// 请求对象,内部是个字符串
struct Request {
s: String,
}
impl Request {
pub fn new(s: impl Into<String>) -> Self {
Self { s: s.into() }
}
}
#[test]
fn test_second_try() {
use mockall::*;
#[automock]
pub trait Handler {
// 测试无参数
fn f0();
// 测试获取一个 String
fn f1(s: String);
// 测试获取两个参数
fn f2(n: u32, s: String);
// 反过来也可以
fn f3(t: (), s: String, n: u32);
}
let f0_ctx = MockHandler::f0_context();
f0_ctx.expect().times(1).returning(|| {});
let f1_ctx = MockHandler::f1_context();
f1_ctx.expect().times(1).returning(|s: String| {
assert_eq!(s, "123");
});
let f2_ctx = MockHandler::f2_context();
f2_ctx.expect().times(1).returning(|n: u32, s: String| {
assert_eq!(n, 123);
assert_eq!(s, "123");
});
let f3_ctx = MockHandler::f3_context();
f3_ctx
.expect()
.times(1)
.returning(|t: (), s: String, n: u32| {
assert_eq!(s, "123");
assert_eq!(n, 123);
});
let app = App::new()
.handler(MockHandler::f0)
.handler(|s: String| MockHandler::f1(s))
.handler(MockHandler::f2)
.handler(MockHandler::f3);
app.dispatch(Request::new("123"));
}
整挺好。
现在我们开始考虑如何写实现。
在你继续向下阅读之前,笔者强烈建议读者先自己进行尝试;当遇到问题后再继续阅读。
重复上一个最小实现
显然,App
依然需要持有一个 handler 列表,但是不能再是 Box<dyn Fn() -> ()>
了,因为支持的函数可能有多个参数。由于 App
本身不带任何类型参数,所以它持有的内部变量也不能有类型参数。我们引入一个新 trait,取名叫 Service
。我们这样写 App
的数据结构:
# src/second_try.rs
struct App {
services: Vec<Box<dyn Service>>,
}
impl App {
pub fn new() -> Self {
Self { services: vec![] }
}
pub fn handler<F>(mut self, f: F) -> Self {
// TODO
self
}
pub fn dispatch(&self, req: &Request) {
// TODO
}
}
trait Service {
// TODO
}
Service
trait 并不提示实现了它的实例如何保存内部数据结构——这很重要。利用这一点,我们就可以擦除实际承载 Service
trait 逻辑的类型,而用一个统一的 Service
trait object 保存。
我们将 Service
trait 这样定义:它接受一个 Request
类型的实例借用,内部处理请求,不做任何返回。
# src/second_try.rs
trait Service {
fn handle_request(&self, req: &Request);
}
因为我们要支持各种类型的函数闭包,所以我们应该要对各种类型的函数指针都实现 Service
trait,这样才能将这些函数指针保存在内部 services
数组中。
首先对 Fn() -> ()
实现吧,我们把剩下的逻辑一起写完:
# src/second_try.rs
struct App {
services: Vec<Box<dyn Service>>,
}
impl App {
pub fn new() -> Self {
Self { services: vec![] }
}
pub fn handler<F>(mut self, f: F) -> Self
where
F: Service + 'static,
{
self.services.push(Box::new(f));
self
}
pub fn dispatch(&self, req: Request) {
for f in self.services.iter() {
f.handle_request(&req);
}
}
}
trait Service {
fn handle_request(&self, req: &Request);
}
impl<F> Service for F
where
F: Fn() -> (),
{
fn handle_request(&self, req: &Request) {
(self)()
}
}
我们先注释掉测试中的部分代码,让一部分代码先跑起来:
# src/second_try.rs
#[test]
fn test_second_try() {
use mockall::*;
#[automock]
pub trait Handler {
// 测试无参数
fn f0();
// 测试获取一个 String
fn f1(s: String);
// 测试获取两个参数
fn f2(n: u32, s: String);
// 反过来也可以
fn f3(t: (), s: String, n: u32);
}
let f0_ctx = MockHandler::f0_context();
f0_ctx.expect().times(1).returning(|| {});
// let f1_ctx = MockHandler::f1_context();
// f1_ctx.expect().times(1).returning(|s: String| {
// assert_eq!(s, "123");
// });
// let f2_ctx = MockHandler::f2_context();
// f2_ctx.expect().times(1).returning(|n: u32, s: String| {
// assert_eq!(n, 123);
// assert_eq!(s, "123");
// });
// let f3_ctx = MockHandler::f3_context();
// f3_ctx
// .expect()
// .times(1)
// .returning(|t: (), s: String, n: u32| {
// assert_eq!(s, "123");
// assert_eq!(n, 123);
// });
let app = App::new().handler(MockHandler::f0);
// .handler(|s: String| MockHandler::f1(s))
// .handler(MockHandler::f2)
// .handler(MockHandler::f3);
app.dispatch(Request::new("123"));
}
cargo t
——很好,通过了!到这里为止我们基本上重复实现了上一个部分的代码,但是使用了一种更抽象的方式。
E0207
很好,接下来我们将 Service
trait 扩展到实现一个参数的函数——顺便增加一个 FromRequest
trait 并对我们支持的类型实现。
# src/second_try.rs
trait Service {
fn handle_request(&self, req: &Request);
}
impl<F> Service for F
where
F: Fn() -> (),
{
fn handle_request(&self, req: &Request) {
(self)()
}
}
impl<F, T> Service for F
where
F: Fn(T) -> (),
T: FromRequest,
{
fn handle_request(&self, req: Request) {
let t = T::from_request(&req);
(self)(t)
}
}
现在取消注释 f1
相关的测试并尝试编译——编译失败了( E0207
):
error[E0207]: the type parameter `T` is not constrained by the impl trait, self type, or predicates
--> src\second_try.rs:36:9
|
36 | impl<F, T> Service for F
| ^ unconstrained type parameter
这是什么意思呢?简单解释就是,对于带有泛型参数的 impl
,rust 限制其中的类型参数 至少要满足以下规则的一条:
-
此参数出现在被
impl
的类型中,如impl<T> Foo<T>
-
对于 trait impl,此类型参数出现在 被 impl 的类型中,如
impl <T> Trait<T> for Foo
。 -
它作为关联类型出现在类型限制中,如
impl<T, U> Trait for T where T: AnotherTrait<Item=U>
那显然,我们这样的写法使得参数 T
并不满足以上任何一条。让我们一一考虑解决办法:
- 让
T
(函数参数)出现在F
(函数类型)中——不太可能。 - 让
T
(函数参数)出现在Service
中——Service
要作为dyn Service
出现在App
中,类型已经抹去,不行。 - 让
T
(函数参数)出现在F
的某个 bound trait 中作为关联类型出现——好像可以?我们可以尝试一下(剧透:不行)。
E0207 的第一次尝试
我们按照第三个方法尝试一下,这样修改代码:
# src/second_try.rs
trait Service {
fn handle_request(&self, req: &Request);
}
impl<F> Service for F
where
F: Fn() -> () + Handler<Input = ()>,
{
fn handle_request(&self, req: &Request) {
(self)()
}
}
impl<F, T> Service for F
where
F: Fn(T) -> () + Handler<Input = (T,)>,
T: FromRequest,
{
fn handle_request(&self, req: Request) {
let t = T::from_request(&req);
(self)(t)
}
}
trait Handler {
type Input;
}
看起来很合理吧?尝试编译一下——又报错了:
error[E0119]: conflicting implementations of trait `second_try::Service`:
--> src\second_try.rs:36:1
|
28 | / impl<F> Service for F
29 | | where
30 | | F: Fn() -> () + Handler<Input = ()>,
31 | | {
... |
34 | | }
35 | | }
| |_- first implementation here
36 | / impl<F, T> Service for F
37 | | where
38 | | F: Fn(T) -> () + Handler<Input = (T,)>,
39 | | T: FromRequest,
... |
44 | | }
45 | | }
| |_^ conflicting implementation
编译器尝试告诉你,你可能对某个类型实现了两次 Service
trait!你可能会想,这咋可能呢?一个是 Fn()->()
,另一个是 Fn(T)->()
呀?!
emmmm,确实有可能。如果你定义一个类型并手动对其分别实现 Fn()->()
和 Fn(T)->()
,它就可能满足这个要求!(虽然手动 impl Fn
是 unstable 的,但是是有可能的)。Rust 编译器又一次找到了可能有歧义的地方。
将函数的参数类型提取到泛型参数上
三种解决的方法都失败了,这怎么办呢……
让我们再次看看三个条件。
第一条,让 T
(函数参数)出现在 F
(函数类型)中——不太可能——等等,为什么一定要对 F
实现 Service
trait 呢?如果我们将 F
包裹一下,用 PhantomData
引入参数 T
,将 T
提取到函数指针的类型参数上,不就可以了吗?
让我们尝试一下。
# src/second_try.rs
use std::marker::PhantomData;
trait Service {
fn handle_request(&self, req: &Request);
}
// T -> (T1, T2,)
struct FunctionWrapper<F, T> {
f: F,
_t: PhantomData<T>,
}
impl<F> FunctionWrapper<F, ()>
where
F: Fn() -> () + 'static,
{
pub fn new(f: F) -> Self {
Self { f, _t: PhantomData }
}
pub fn call(&self, t: ()) {
(self.f)()
}
}
impl<F, T> FunctionWrapper<F, (T,)>
where
F: Fn(T) -> () + 'static,
T: FromRequest,
{
pub fn new(f: F) -> Self {
Self { f, _t: PhantomData }
}
pub fn call(&self, t: (T,)) {
(self.f)(t.0)
}
}
...
impl<F> Service for FunctionWrapper<F, ()>
where
F: Fn() -> () + 'static,
{
fn handle_request(&self, req: &Request) {
self.call(())
}
}
impl<F, T> Service for FunctionWrapper<F, (T,)>
where
F: Fn(T) -> () + 'static,
T: FromRequest,
{
fn handle_request(&self, req: &Request) {
let t = T::from_request(&req);
self.call((t,))
}
}
...
这样的确可以解决!接下来我们挨个对不同参数个数的函数指针实现 impl FunctionWrapper
和 impl<F,T> Serverice for FunctionWrapper<F, (T)>
就行了。
少写代码,多抽象
不过这样写虽然没什么大问题,但是由于 Fn
这个 trait 的定义问题,我们不能使用 Fn<(T1, T2), Output = ()>
的形式(E0658),只能手动对各种参数个数分别实现 Fn(T1, T2, ...) -> () + 'static
。
如果不想到处都要写一堆 Fn(T1, T2, ...) -> () + 'static'
,我们可以先抽象出一个 trait 出来:
/// `Handler<(T1, T2)>`基本上等价于 `F(T1, T2)->() + 'static`。
trait Handler<T>: 'static {
fn call(&self, params: T);
}
这样我们的代码可以简洁很多:
# src/second_try.rs
use std::marker::PhantomData;
struct App {
services: Vec<Box<dyn Service>>,
}
impl App {
pub fn new() -> Self {
Self { services: vec![] }
}
pub fn handler<F, T>(mut self, f: F) -> Self
where
F: Handler<T>,
T: FromRequest + 'static,
{
let f = FunctionWrapper::new(f);
self.services.push(Box::new(f));
self
}
pub fn dispatch(&self, req: Request) {
for f in self.services.iter() {
f.handle_request(&req);
}
}
}
trait Service {
fn handle_request(&self, req: &Request);
}
/// `Handler<(T1, T2)>`基本上等价于 `F(T1, T2)->()`。
trait Handler<T>: 'static {
fn call(&self, params: T);
}
#[rustfmt::skip]
mod _impl_handler{
use super::*;
impl<F> Handler<()> for F where F: Fn() -> () + 'static {
fn call(&self, params: ()) {
(self)()
}
}
impl<F, T> Handler<(T, )> for F where F: Fn(T) -> () + 'static {
fn call(&self, params: (T, )) {
(self)(params.0)
}
}
impl<F, T1, T2> Handler<(T1, T2)> for F where F: Fn(T1, T2) -> () + 'static {
fn call(&self, params: (T1, T2)) {
(self)(params.0, params.1)
}
}
...
}
/// 这里将函数指针的 T 提到类型参数中
struct FunctionWrapper<F, T> {
f: F,
_t: PhantomData<T>,
}
/// 我们只需要实现一次 `impl FunctionWrapper` 就可以了,不需要对不同参数个数的函数重复实现
impl<F, T> FunctionWrapper<F, T>
where
F: Handler<T>,
T: FromRequest,
{
pub fn new(f: F) -> Self {
Self { f, _t: PhantomData }
}
}
/// 将 `Service` 逻辑实现给函数指针,同样只需要实现一次
impl<F, T> Service for FunctionWrapper<F, T>
where
F: Handler<T>,
T: FromRequest,
{
fn handle_request(&self, req: &Request) {
// 在这里从请求中提取参数
let params = T::from_request(req);
self.f.call(params)
}
}
struct Request {
s: String,
}
trait FromRequest {
fn from_request(req: &Request) -> Self;
}
impl FromRequest for String {
fn from_request(req: &Request) -> Self {
req.s.clone()
}
}
impl FromRequest for u32 {
fn from_request(req: &Request) -> Self {
req.s.parse().unwrap()
}
}
impl FromRequest for () {
fn from_request(req: &Request) -> Self {
()
}
}
impl Request {
pub fn new(s: impl Into<String>) -> Self {
Self { s: s.into() }
}
}
我们还差最后一个拼图:自动将 &Request
提取到 ()
, (T, )
, (T1, T2)
, (T1, T2, T3)
……
# src/second_try.rs
struct Request {
s: String,
}
trait FromRequest {
fn from_request(req: &Request) -> Self;
}
#[rustfmt::skip]
mod _impl_from_request {
use super::*;
impl FromRequest for String {
fn from_request(req: &Request) -> Self {
req.s.clone()
}
}
impl FromRequest for u32 {
fn from_request(req: &Request) -> Self {
req.s.parse().unwrap()
}
}
impl FromRequest for () {
fn from_request(req: &Request) -> Self {
()
}
}
// propagate
impl <T1> FromRequest for (T1, ) where T1: FromRequest {
fn from_request(req: &Request) -> Self {
(T1::from_request(req), )
}
}
impl <T1, T2> FromRequest for (T1, T2) where
T1: FromRequest,
T2: FromRequest,
{
fn from_request(req: &Request) -> Self {
(T1::from_request(req), T2::from_request(req), )
}
}
impl <T1, T2, T3> FromRequest for (T1, T2, T3) where
T1: FromRequest,
T2: FromRequest,
T3: FromRequest,
{
fn from_request(req: &Request) -> Self {
(T1::from_request(req), T2::from_request(req), T3::from_request(req), )
}
}
}
好了,现在取消测试部分全部的注释——测试通过!
减少重复——宏生成代码
更进一步地优化代码结构,注意到我写了两个 mod _impl_xxxx
,里面有很多都是机械性的操作。我们可以使用宏来生成代码消除这部分重复:
#[rustfmt::skip]
mod _impl_handler {
use super::*;
// delegate
impl<F> Handler<()> for F where F: Fn() -> () + 'static {
fn call(&self, params: ()) {
(self)()
}
}
macro_rules! f {
(($($Ts:ident),*), ($($Ns:tt),*)) => {
impl<F, $($Ts,)*> Handler<( $($Ts, )* )> for F
where
F: Fn( $($Ts,)* ) -> () + 'static
{
fn call(&self, params: ( $($Ts,)* )) {
(self)(
$(params.$Ns, )*
)
}
}
};
}
f!((T1), (0));
f!((T1, T2), (0, 1));
f!((T1, T2, T3), (0, 1, 2));
f!((T1, T2, T3, T4), (0, 1, 2, 3));
f!((T1, T2, T3, T4, T5), (0, 1, 2, 3, 4));
f!((T1, T2, T3, T4, T5, T6), (0, 1, 2, 3, 4, 5));
f!((T1, T2, T3, T4, T5, T6, T7), (0, 1, 2, 3, 4, 5, 6));
f!((T1, T2, T3, T4, T5, T6, T7, T8), (0, 1, 2, 3, 4, 5, 6, 7));
f!((T1, T2, T3, T4, T5, T6, T7, T8, T9), (0, 1, 2, 3, 4, 5, 6, 7, 8));
f!((T1, T2, T3, T4, T5, T6, T7, T8, T9, T10), (0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
}
3. 总结
最终的代码可以在 GitHub repo 找到。
总结一下,
- 我们实现了一个类型擦除的系统,其通过一个
Service
trait 将不同类型的函数指针类型进行擦除,保存在内部一个数组中; - 在擦除的过程中我们利用一个包装类型
FunctionWrapper<F, T>
将函数指针的参数类型提取出来成为直接泛型参数,从而绕过了 Rust E0207 规则; - 我们利用
Handler<T>
trait 简化代码,避开了写一大堆Fn(T1, T2, ...) + 'static
的问题; - 我们利用宏生成代码,用几行代码生成了几十行代码,使得 handler 的参数可以高达 10 个,而且很容易继续扩展。
4. 展望
在下一篇(如果有)博客中,我们会继续使用当前的代码进行改进,使得其支持异步或支持错误处理。
异步麻烦的地方就在于函数的签名是
F: Fn(T1, T2, ...) -> Fut, Fut: Future<Output>
Ext Link: https://gwy15.com/blog/%E7%B1%BB%E5%9E%8B%E6%93%A6%E9%99%A4%E6%95%99%E7%A8%8B-Part-I.-%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84%E5%90%8C%E6%AD%A5%E7%A4%BA%E8%8C%83
评论区
写评论好文👍,学习了。
Handle的作用是方便我们的serive trait统一实现handle函数,如下:
注意到这里直接使用
self.f.call(params)
统一处理request了没有,这个call
方法就是handle
trait提供的。如果你没有handle
trait,则实现如下:如此才能保证代码的统一性。
--
👇
yar999:
Handler<T>
应该不仅仅是简化代码的作用吧, 我按照不使用Handler<T>
trait 的思路,尝试发现 APP 的 handler 函数那里不能统一处理了.....请问如果不用
Handler<T>
trait 统一Fn() -> ()
和Fn(T) -> ()
, APP 的 handler 函数那里该怎么写? 只能用多个不同的handler名字来处理多0个或多个参数的情况?Handler<T>
应该不仅仅是简化代码的作用吧, 我按照不使用Handler<T>
trait 的思路,尝试发现 APP 的 handler 函数那里不能统一处理了.....请问如果不用
Handler<T>
trait 统一Fn() -> ()
和Fn(T) -> ()
, APP 的 handler 函数那里该怎么写? 只能用多个不同的handler名字来处理多0个或多个参数的情况?好文
好文,学习了!
新手跟着走了一遍,写的真的很详细。注册服务可以说是一个非常常见的做法了,很多地方用得上。
感谢