rust 笔记2: 一个特殊的场景之编译器不能推断出特征对象的类型

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 中还应当包括 SyncSend 。不过这些都不重要了,因为到了这一步,编译错误就准备出现了,我们不必再纠结 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));
    }
}

testtest_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 之类的范畴,也能是引用。

后面我会开始翻译该大佬写的有关特征对象的系列文章。相信在完全消化了那些文章后,能更加深入的理解这个问题。

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇