为什么我要在特质上实现方法而不是作为特质的一部分?

52

我试图更好地理解Any特质,发现它有一个用于特质本身的impl。我不理解这个结构的目的,甚至不知道它是否有一个特定的名称。

我对“普通”特质方法和在impl块中定义的方法进行了一些小实验:

trait Foo {
    fn foo_in_trait(&self) {
        println!("in foo")
    }
}

impl dyn Foo {
    fn foo_in_impl(&self) {
        println!("in impl")
    }
}

impl Foo for u8 {}

fn main() {
    let x = Box::new(42u8) as Box<dyn Foo>;
    x.foo_in_trait();
    x.foo_in_impl();

    let y = &42u8 as &dyn Foo;
    y.foo_in_trait();
    y.foo_in_impl(); // May cause an error, see below
}

Editor's note

In versions of Rust up to and including Rust 1.15.0, the line y.foo_in_impl() causes the error:

error: borrowed value does not live long enough
  --> src/main.rs:20:14
   |
20 |     let y = &42u8 as &Foo;
   |              ^^^^ does not live long enough
...
23 | }
   | - temporary value only lives until here
   |
   = note: borrowed value must be valid for the static lifetime...

This error is no longer present in subsequent versions, but the concepts explained in the answers are still valid.

从这个有限的实验来看,似乎在impl块中定义的方法比在trait块中定义的方法更加受限制。很可能有一些额外的东西通过这种方式解锁,但我还不知道是什么!^_^
《Rust编程语言》中关于traitstrait objects的章节没有提到这一点。在搜索Rust源代码时,似乎只有AnyError使用了这个特定的功能。在我查看过源代码的一些crate中,我没有看到使用这个功能的情况。

3
非常有趣的问题!在特质块中,“Self”是“Foo”,在“impl”块中,“Self”是“Foo + 'static”。 - Lukas Kalbertodt
2个回答

36
当您定义一个名为Foo的trait,可以将其制作成对象时,Rust还定义了一个名为dyn Foo的trait对象类型。在旧版本的Rust中,此类型仅称为Foo(请参见 What does "dyn" mean in a type?)。为与这些旧版本向后兼容,Foo仍然可用于命名trait对象类型,尽管应为新代码使用dyn语法。
Trait对象具有指定实现者生命周期参数中最短的寿命参数的生命周期参数。要指定该生命周期,请将类型编写为dyn Foo + 'a
当您编写impl dyn Foo {(或只是使用旧语法的impl Foo {)时,您并未指定该生命周期参数,它默认为'static。编译器对y.foo_in_impl();语句的提示表明了这一点:

note: borrowed value must be valid for the static lifetime...

我们所要做的就是编写针对任何生命周期的通用impl,以使其更加通用。
impl<'a> dyn Foo + 'a {
    fn foo_in_impl(&self) { println!("in impl") }
}

请注意,foo_in_impl中的self参数是一个借用指针,并且有自己的生命周期参数。完整形式上,self的类型看起来像&'b (dyn Foo + 'a)(由于运算符优先级的缘故,括号是必需的)。Box<u8>拥有它的u8 - 它不借用任何东西 - 因此可以从中创建一个&(dyn Foo + 'static)。另一方面,&42u8创建一个&'b (dyn Foo + 'a),其中'a不是'static,因为42u8被放置在堆栈上的隐藏变量中,而特质对象借用了这个变量。(虽然这真的没有多大意义;u8不会借用任何东西,因此它的Foo实现应始终与dyn Foo + 'static兼容...事实上,42u8从堆栈借用应该影响'b,而不是'a。)

另一个需要注意的事情是,即使默认实现的特质方法没有被重写,它们也是多态的,而特质对象上的固有方法是单态的(无论特质背后是什么,都只有一个函数)。例如:

use std::any::type_name;

trait Foo {
    fn foo_in_trait(&self)
    where
        Self: 'static,
    {
        println!("{}", type_name::<Self>());
    }
}

impl dyn Foo {
    fn foo_in_impl(&self) {
        println!("{}", type_name::<Self>());
    }
}

impl Foo for u8 {}
impl Foo for u16 {}

fn main() {
    let x = Box::new(42u8) as Box<dyn Foo>;
    x.foo_in_trait();
    x.foo_in_impl();

    let x = Box::new(42u16) as Box<Foo>;
    x.foo_in_trait();
    x.foo_in_impl();
}

样例输出:

u8
dyn playground::Foo
u16
dyn playground::Foo

在trait方法中,我们获取底层类型的类型名称(这里是u8u16),因此我们可以得出结论:&self的类型将因实现者而异(对于u8实现者,它将是&u8,对于u16实现者,它将是&u16——而不是一个trait对象)。然而,在内部方法中,我们获取dyn Foo的类型名称(带有+ 'static),因此我们可以得出结论:&self的类型始终为&dyn Foo(一个trait对象)。


9
我怀疑原因非常简单:可能被覆盖或未被覆盖?
在trait块中实现的方法可以被trait的实现者覆盖,它只提供默认值。
另一方面,在impl块中实现的方法无法被覆盖。
如果这种推理是正确的,那么你为y.foo_in_impl()得到的错误只是缺乏光泽的表现:它应该能工作。请参见Francis Gagné关于与生命周期交互的更完整的答案。

2
你的回答在某种程度上更好。它直接回答了疑问的核心部分。复杂的细节可以随后解释,但主要思想应该放在前面;而另一个回答则相反。 - legends2k

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接