如何在Rust中按值传递一个包装的特质对象?

8

我正在编写一些代码,并且有一个具有通过值传递self的方法的特征。我想在Box的特征对象上调用此方法(消耗Box及其值)。这是可能的吗?如果是,怎么做?

就代码而言,一个最简示例看起来像以下不完整的代码:

trait Consumable {
    fn consume(self) -> u64;
}
fn consume_box(ptr: Box<dyn Consumable>) -> u64 {
    //what can I put here?
}

我的问题是如何填写具有指定签名的consume_box函数,使得返回的值与调用Box值上的consume所获得的值相同。

我最初写了以下内容

ptr.consume()

虽然我意识到这不是完全正确的想法,因为它并没有表达出我想消耗的是Box本身而不仅是其内容,但作为函数主体是我能想到的唯一办法。然而这段代码无法编译,会报错:

cannot move a value of type dyn Consumable: the size of dyn Consumable cannot be statically determined

对于我这个新手来说,这有些令人惊讶。我曾认为self参数可能类似于C++中的一个右值引用(这正是我想要的——在C++中,我可能会通过签名为virtual std::uint64_t consume() &&的方法实现它,让std::unique_ptr通过虚析构函数清理移动过来的对象),但我猜Rust确实是传值的,将参数移动到位,所以拒绝了这段代码。

问题在于,我不知道如何获得我想要的行为,即如何消耗一个装箱的特质对象。我尝试给特质添加一个具有默认实现的方法,认为这可能会在虚函数表中给我带来一些有用的东西:

trait Consumable {
    fn consume(self) -> u64;
    fn consume_box(me: Box<Self>) -> u64 {
        me.consume()
    }
}

然而,这会导致错误:

特质Consumable无法成为一个对象

当我提到Box<dyn Consumable>类型时 - 这并不令人惊讶,因为编译器要找出如何处理一个其参数类型随Self变化的函数将是奇迹。

是否可能使用提供的签名实现函数consume_box - 即使需要修改特质?


如果有用的话,更具体地说,这是一些数学表达式的表示形式的一部分 - 也许一个玩具模型看起来大致如下:

impl Consumable for u64 {
    fn consume(self) -> u64 {
        self
    }
}
struct Sum<A, B>(A, B);
impl<A: Consumable, B: Consumable> Consumable for Sum<A, B> {
    fn consume(self) -> u64 {
        self.0.consume() + self.1.consume()
    }
}
struct Product<A, B>(A, B);
impl<A: Consumable, B: Consumable> Consumable for Product<A, B> {
    fn consume(self) -> u64 {
        self.0.consume() * self.1.consume()
    }
}
fn parse(&str) -> Option<Box<dyn Consumable> > {
    //do fancy stuff
}

在大多数情况下,这些东西都是普通的数据(但由于泛型可能是任意大的块),但也希望能够使用更加不透明的句柄来传递这些东西——因此希望能够使用Box<dyn Consumable>。至少在语言层面上,这是我所涉及的事物的良好模型——这些对象拥有的唯一资源是内存片段(与多线程无关且没有自引用的花哨操作)——尽管这个模型并未捕捉到我的用例是实现消耗对象而不仅仅是读取它,也没有适当地模拟我想要一个可能段的“开放”类别而不是有限的可能性集合(这使得很难像一个直接表示树的enum那样做)——这就是为什么我要询问是否通过值传递而不是尝试将其重写为通过引用传递。


如果Self的大小在编译时已知,则可以将: Sized添加到特质中。 - vallentin
@vallentin 我刚试着把第一行改成 trait Consumable: Sized,但它报错说 "the trait Consumable cannot be into an object ... because it requires Self: Sized" - 我认为它在抱怨编译时不知道特质对象的大小,而不是任何特定实现者的大小(尽管我也没有试图在奇怪的东西上实现特质)。 - Milo Brandt
@vallentin,我加入了一些具体类型的片段,以便给出实现该特质的类型的想法,如果有用的话。 - Milo Brandt
3个回答

11

如果参数是 self: Box<Self>,则可以从 Box<dyn Trait> 中消费:

trait Consumable {
    fn consume(self) -> u64;
    fn consume_box(self: Box<Self>) -> u64;
}

struct Foo;
impl Consumable for Foo {
    fn consume(self) -> u64 {
        42
    }
    fn consume_box(self: Box<Self>) -> u64 {
        self.consume()
    }
}

fn main() {
    let ptr: Box<dyn Consumable> = Box::new(Foo);
    println!("result is {}", ptr.consume_box());
}

然而,这种方法需要为每个实现都实现consume_box(),这很繁琐;试图定义一个默认实现会遇到一个"cannot move value of type Self - the size of Self cannot be statically determined"错误。
一般来说,这是不支持的。一个 dyn Consumable 表示一个 未定长 的类型,除了通过间接引用(通过引用或类似于 Box 的结构体)以外很有限。它适用于上述情况,因为 Box 有点特殊(是唯一可以从中取得所有权的 dispatchable 类型),而且 consume_box 方法不会将 self 作为动态特质对象放在堆栈上(只有在每个实现中才会具体化)。
然而,有 RFC 1909: Unsized RValues 希望放宽其中一些限制,例如能够传递未定长的函数参数,就像在这种情况下的 self。当使用 unsized_fn_params 在 nightly 上编译时,该 RFC 的当前实现接受您的初始代码。
#![feature(unsized_fn_params)]

trait Consumable {
    fn consume(self) -> u64;
}

struct Foo;
impl Consumable for Foo {
    fn consume(self) -> u64 {
        42
    } 
}

fn main () {
    let ptr: Box<dyn Consumable> = Box::new(Foo);
    println!("result is {}", ptr.consume());
}

请在playground上查看。


1
如果您不使用 Rust 的 nightly 版本,这里有一个宏(链接)可以使用。它会自动生成第二个 trait 函数。
trait Consumable {
    fn consume(self) -> u64;
    fn consume_box(me: Box<Self>) -> u64 ;
}

这将防止Consumable被转换为特质对象。 - jthulhu
1
@jthulhu 如果参数命名为 self,它仍然可以用作特质对象:playground。根据 object safetyBox<Self> 是可分派的,而 self: Box<Self> 是一个有效的接收器。 - kmdreko

0

我相信

trait Consumable {
    fn consume(self) -> u64;
}

fn consume_box(val: impl Consumable) -> u64 {
    val.consume()
}

可能会做你想要的事情。我并不是Rust专家 - 或者说C++专家 - 但我认为它应该在内存行为方面与你提到的C++中的move语义非常相似。据我所知,它是一种通用形式,在Rust中实现了对你调用它时的每种类型的函数。


这不起作用是因为 Box<dyn Consumable> 没有实现 Consumable - jthulhu
1
@jthulhu,抱歉回复晚了,我已经有一段时间没有上SO了。你当然是正确的。根据我学到的知识,我可以看到一些东西,它们不能解决原始问题,但看起来很相似,其中大小在编译时已知。这是一个游乐场:https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f00ad1f7fd0a94173a0bd99e3bc84082。不确定如何将其添加到此线程中,也许我会建议编辑建议的答案,至少添加“where Self:Sized”方法及其限制。 - Sam96

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