1. 场景
当我们需要用特征对象的时候,大部分情况下 Rust 编译器可以自动进行类型推断。但是在少数场景下,我们可能需要显式声明特征对象,其中一个例子就是引用 + 关联类型同时出现。
直接来看一个例子,为了更加形象地解释,我们用服务器端程序的场景来命名出现的类型和特征。首先,我们需要一个 Listener
的 trait:
trait Listener {
type Conn;
fn accept(&self) -> Self::Conn;
}
Listener
需要有一个关联类型 Conn
,作为其 accept()
方法返回的对象,例如一个 TCP 的 Listener
需要返回一个类似于 TcpConnection
的东西,这些都是非常直观的。
那么接下来就来到了我们的重点,如果我们想构建一个 TlsListener
呢?众所周知,TLS 必须要在其他的连接(通常是 TCP )基础上进行构建,那么我们的 TLS 监听器应当有一个现成的 Listener
作为成员,并在构造时获取这个监听器的所有权,即:
struct TlsListener<Ln>
where
Ln: Listener,
{
ln: Ln,
}
impl<Ln> TlsListener<Ln>
where
Ln: Listener,
{
fn new(base: Ln) -> Self {
Self {
ln: base
}
}
}
现在,我们应该为 TlsListener
实现 Listener
trait 了,其基本框架如下所示:
impl<Ln> Listener for TlsListener<Ln>
where
Ln: Listener,
Ln::Conn: Unpin + 'static,
{
type Conn = ();
fn accept(&self) {
// accept logic here...
}
}
为了贴近现实场景,我们为 Ln::Conn
设置了两个 trait bound,对于真实的异步服务器场景,这个 bound 中还应当包括 Sync
和 Send
。不过这些都不重要了,因为到了这一步,编译错误就准备出现了,我们不必再纠结 accept
方法的逻辑。为了展示这个编译错误,只需要引入两个测试函数即可:
fn test(_t: Pin<Box<dyn Unpin + 'static>>) {}
fn test_ref(_t: &Pin<Box<dyn Unpin + 'static>>) {}
测试的内容都很清晰,唯一区别就是一个是引用而另一个不是。现在我们展示多种可能的写法,并用注释标注出编译不能通过的样例(环境是 rustc 1.78.0 2024-04-29):
impl<Ln> Listener for TlsListener<Ln>
where
Ln: Listener,
Ln::Conn: Unpin + 'static,
{
type Conn = ();
fn accept(&self) {
// ok
let conn = self.ln.accept();
test(Box::pin(conn));
// ok
let conn0 = self.ln.accept();
let boxed_conn0 = Box::new(conn0);
test(Pin::new(boxed_conn0));
// not ok
let conn1 = self.ln.accept();
test_ref(&Box::pin(conn1));
// ok
let conn2: <Ln as Listener>::Conn = self.ln.accept();
let boxed_conn2 = Box::new(conn2);
test_ref(&Pin::new(boxed_conn2));
// ok
let conn3 = self.ln.accept();
let boxed_conn3: Box<dyn Unpin + 'static> = Box::new(conn3);
test_ref(&Pin::new(boxed_conn3));
}
}
test
和 test_ref
都是仅用于测试编译类型的函数。在以上的代码中,我们将能通过编译的写法标记为 ok ,反之则是 not ok。对于无法通过编译的部分,报错如下:
error[E0308]: mismatched types
--> src/main.rs:94:18
|
94 | test_ref(&Box::pin(conn1));
| -------- ^^^^^^^^^^^^^^^^ expected `&Pin<Box<dyn Unpin>>`, found `&Pin<Box<<Ln as Listener>::Conn>>`
| |
| arguments to this function are incorrect
|
= note: expected reference `&Pin<Box<(dyn Unpin + 'static)>>`
found reference `&Pin<Box<<Ln as Listener>::Conn>>`
note: function defined here
--> src/main.rs:108:4
|
108 | fn test_ref(_: &Pin<Box<dyn Unpin + 'static>>) {}
| ^^^^^^^^ ----------------------------------
help: consider constraining the associated type `<Ln as Listener>::Conn` to `(dyn Unpin + 'static)`
|
77 | Ln: Listener<Conn = (dyn Unpin + 'static)>,
| ++++++++++++++++++++++++++++++
For more information about this error, try `rustc --explain E0308`.
可以看到,当我们的测试函数接受的对象类型为引用时,编译期便不能在 Box::pin
中自动将我们的变量转换为 dyn Unpin + 'static
的 trait object 形式,而是留在了关联类型 <Ln as Listener>::Conn
。此时我们只有像 conn3
那样,显式地将 Box
的泛型指定为 trait object,才可以通过编译。
此时 rust 编译器给出的帮助也是基本不可用的:
"consider constraining the associated type
<Ln as Listener>::Conn
to(dyn Unpin + 'static)
如果我们真的将泛型的 trait bound 写成 Ln: Listener<Conn = (dyn Unpin + 'static)>,
那么会出现动态分发类型大小不可知的问题,还需要再取一个 &
或是 Box
将其转换为特征对象。而这样修改的话,我们还需要改变 Listener
trait 本身,要求所有的 Listener
trait 返回的 Conn
都是动态分发的,这下就让简单问题复杂化了。
2. 深挖一下
在 rust users forum 上发帖求助了一下,回帖的大佬的原文是:
Some bad order of inference is going on. You can't coerce a &Box
or a &Pin<Box > to a &Box or &Pin<Box > because of too much indirection. So the compiler decided you had created a &Pin<Box > before it tried to coerce it.
看起来 rust 编译器在进行类型转换时,如果需要被转换的类型被“包”了太多层,就不能推理出正确的类型了。这个包裹可以是 Pin
Box
之类的范畴,也能是引用。
后面我会开始翻译该大佬写的有关特征对象的系列文章。相信在完全消化了那些文章后,能更加深入的理解这个问题。