如何指定一个结构体字段必须实现一个特质?

103

我有一个特性 Foo

pub trait Foo {
   fn do_something(&self) -> f64;
}

以及引用该trait的结构体

pub struct Bar {
   foo: Foo,
}

尝试编译时我收到以下错误信息:

error: reference to trait `Foo` where a type is expected; try `Box<Foo>` or `&Foo`

将结构体更改为

struct Bar {
   foo: &Foo,
}

告诉我 error: missing lifetime specifier

将定义更改为

struct Bar {
   foo: Box<Foo>,
}

编译成功——耶!

但是,当我想让一个函数在bar时返回foo,类似于:

impl Bar {
    fn get_foo(&self) -> Foo {
        self.foo
    }
}

显然 bar.foo 是一个 Box<Foo>,因此我得到了 error: reference to trait `Foo` where a type is expected; try `Box<Foo>` or `&Foo` 的错误提示。

将签名更改为:

impl Bar {
    fn get_foo(&self) -> Box<Foo> {
        let this = *self;
        this.foo
    }
}

但是现在当我尝试对self进行解引用时,会出现错误:无法移动`&`指针的解引用

更改为

impl Bar {
    fn get_foo(self) -> Box<Foo> {
        self.foo
    }
}

一切都很好。

那么...

  1. 为什么bar结构体中的&不起作用?我猜想我们必须将其包装成指向特质的指针(因为我们无法知道它的大小),这样才能保证结构体有一个固定的内存布局,但是编译器为什么会建议一些不能编译的代码呢?
  2. 为什么我不能在get_foo()中取消引用self - 所有我看到的示例都使用了借用的self语法?
  3. 去掉&直接使用self的影响是什么?

学习Rust很有趣,但内存安全既让人着迷又令人生畏!

完整可编译代码:

trait Foo {
    fn do_something(&self) -> f64;
}

struct Bar {
    foo: Box<Foo>,
}

impl Bar {
    fn get_foo(self) -> Box<Foo> {
        let foo = self.foo;
        foo.do_something();
        foo
    }
}

fn main() {}
3个回答

167

这是 trait 对象的棘手之处,您需要非常明确地说明谁拥有基础对象。

事实上,当您将 trait 用作类型时,基础对象必须存储在某个地方,因为 trait 对象实际上是指向实现给定 trait 的对象的引用。这就是为什么您不能使用裸的 MyTrait 作为类型,它必须是一个引用 &MyTrait 或者一个 box Box<MyTrait>

使用引用

您尝试的第一种方法是使用引用,编译器抱怨缺少生命周期说明符:

struct Bar {
   foo : &Foo,
}

问题在于,引用并不拥有底层对象,其他对象或作用域必须在某个地方拥有它:你只是借用它。因此,编译器需要了解这个引用将有效多长时间:如果底层对象被销毁,你的 Bar 实例将持有指向已释放内存的引用,这是禁止的!

这里的想法是添加生命周期:

struct Bar<'a> {
   foo : &'a (Foo + 'a),
}
你在这里对编译器所表达的意思是: "我的Bar对象不能比其中的Foo引用存在时间更长"。你需要指定两次生命周期: 一次是引用的生命周期,另一次是特质对象本身的生命周期,因为特质可以被实现为引用,如果底层对象是引用,则必须指定其生命周期。
一个特殊情况是编写:
struct Bar<'a> {
   foo : &'a (Foo + 'static),
}
在这种情况下,'static 要求基础对象必须是一个真正的结构体或 &'static 引用,但不允许其他引用。
此外,为了构建您的对象,您将不得不给它一个对您自己存储的另一个对象的引用。
最终您会得到类似以下的东西:
trait Foo {}

struct MyFoo;

impl Foo for MyFoo {}

struct Bar<'a> {
    foo: &'a (Foo + 'a),
}

impl<'a> Bar<'a> {
    fn new(the_foo: &'a Foo) -> Bar<'a> {
        Bar { foo: the_foo }
    }

    fn get_foo(&'a self) -> &'a Foo {
        self.foo
    }
}

fn main() {
    let myfoo = MyFoo;
    let mybar = Bar::new(&myfoo as &Foo);
}

使用 Boxes

盒子(Box)相反地拥有其内容,因此它允许您将基础对象的所有权交给您的Bar结构体。然而,由于这个基础对象可能是一个引用,您还需要指定一个生命周期:

struct Bar<'a> {
    foo: Box<Foo + 'a>
}
如果您知道底层对象不能是引用,您也可以这样写:

如果您知道底层对象不能为引用,则还可以编写:

struct Bar {
    foo: Box<Foo + 'static>
}

寿命问题完全消失了。

因此,对象的构建类似,但更简单,因为您不需要自己存储基础对象,它由框处理:

trait Foo {}

struct MyFoo;

impl Foo for MyFoo {}

struct Bar<'a> {
    foo: Box<Foo + 'a>,
}

impl<'a> Bar<'a> {
    fn new(the_foo: Box<Foo + 'a>) -> Bar<'a> {
        Bar { foo: the_foo }
    }

    fn get_foo(&'a self) -> &'a Foo {
        &*self.foo
    }
}

fn main() {
    let mybar = Bar::new(box MyFoo as Box<Foo>);
}

在这种情况下,'static 版本将是:

trait Foo {}

struct MyFoo;

impl Foo for MyFoo {}

struct Bar {
    foo: Box<Foo + 'static>,
}

impl Bar {
    fn new(the_foo: Box<Foo + 'static>) -> Bar {
        Bar { foo: the_foo }
    }

    fn get_foo<'a>(&'a self) -> &'a Foo {
        &*self.foo
    }
}

fn main() {
    let mybar = Bar::new(box MyFoo as Box<Foo>);
    let x = mybar.get_foo();
}

仅使用裸值

回答你的最后一个问题:

删除&并只使用self的含义是什么?

如果一个方法的定义如下:

fn unwrap(self) {}

这意味着在过程中会消耗你的对象,在调用bar.unwrap()后,你将无法再使用bar

通常情况下,这是一种让你的结构体所拥有的数据归还所有权的过程。你将会在标准库中遇到很多unwrap()函数。


哇,非常感谢您提供如此详细的信息。在接受之前,我会仔细阅读几遍,但这为我提供了很多可以尝试理解和操作的内容。 - neil danson
感谢您提供详细的描述!对于那些有类基础语言背景的人来说,这是一个陡峭的学习曲线。 - Robert Knight
3
似乎自那时起生命周期的限制发生了改变。我现在可以写成 foo: &'a Foofoo: Box<Foo>,这些变化的原因是什么? - nhaarman

23

作为将来参考的注意事项:语法已经发生了变化,从

struct Bar<'a> {
    foo: &'a Foo + 'a,
}

struct Bar<'a> {
    foo: &'a (Foo + 'a), // with parens
}

根据RFC 438

的规定


2
我认为这又过时了 :) 现在你还需要在一些“dyn”中摆弄,这是我的编译器告诉我的。 - BitTickler

12
随着2021版,dyn现在是Trait对象的要求。截至1.57版本,box语法仍然不稳定,所以应该使用Box::new()代替(但无需进行强制转换)。因此,我认为@Levans提供的上述示例应该稍作修改。请注意,也不再需要生命周期。
trait Foo {}

struct MyFoo;

impl Foo for MyFoo {}

struct Bar<'a> {
    foo: Box<dyn Foo + 'a>,
}

impl<'a> Bar<'a> {
    fn new(the_foo: Box<dyn Foo + 'a>) -> Bar<'a> {
        Bar { foo: the_foo }
    }

    fn get_foo(&'a self) -> &'a dyn Foo {
        &*self.foo
    }
}

fn main() {
    let mybar = Bar::new(Box::new(MyFoo));
}

3
针对这个例子,由于rustc可以推断出生命周期注释的存在,因此不再需要添加生命周期注释。 - optevo
要小心,省略lifetime与明确提供它们是有不同含义的。在这里,Box<dyn Foo>被推断为Box<dyn Foo + 'static> - kmdreko
谢谢,我没意识到。我会暂时恢复它。 - Yuri Astrakhan

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