如何在Rust中按值传递装箱特征对象



我正在写一些代码,并且有一个按值取self的方法的特点。我想在Box'd特征对象上调用此方法(消耗Box及其值)。这可能吗?如果是,如何?

就代码而言,一个最小的例子看起来像以下(不完整的)代码:

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

我的问题是如何用指定的签名填充函数consume_box,以便返回的值是通过对Box'd值调用consume所获得的值。

我最初写的是

ptr.consume()

作为函数的主体,尽管我意识到这不是一个完全正确的想法,因为它没有跨越这样一个事实,即我希望Box被消费,不仅仅是它的内容,而且它是我唯一能想到的东西。这不会编译,给出一个错误:

不能移动dyn Consumerable类型的值:dyn Consumeable的大小不能静态确定

这让我有点惊讶,作为Rust的新手,我曾认为self参数的传递可能与C++中的右值引用类似(这正是我真正想要的——在C++中,我可能会通过一个具有签名virtual std::uint64_t consume() &&的方法来实现这一点,让std::unique_ptr通过虚拟析构函数清理移动的from对象),但我猜Rust确实是在传递值,将参数移动到先前的位置,因此它拒绝代码是合理的。

问题是,我不确定如何获得我想要的行为,在那里我可以使用Box'dtrait对象。我试着用默认实现为特性添加一个方法,认为这可能会在vtable中为我带来一些有用的东西:

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: 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();尝试定义默认实现将遇到";不能移动类型CCD_ 21的值-不能静态地确定CCD_错误。


一般情况下,尽管不支持此功能。dyn Consumable表示一个未大小的类型,除了通过间接(通过引用或类似Box的结构)之外,这种类型非常有限。它适用于上述情况,因为Box有点特殊(是您可以从中获得所有权的唯一可调度类型),并且consume_box方法没有将self作为动态特征对象放在堆栈上(仅在每个具体实现中)。

然而,有RFC 1909:Unsize RV值,它希望放松其中的一些限制。一种是能够传递无尺寸的函数参数,在这种情况下类似于self。这个RFC的当前实现在夜间使用unsized_fn_params:编译时接受您的初始代码

#![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());
}

在操场上看。

如果你不使用夜间Rust,我在这里写了一个宏。它自动生成第二个特征函数。

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

我相信

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

可能会做你想做的事。我几乎是一个Rust专家,或者C++专家,但我认为它应该像你提到的C++中关于内存行为的移动语义一样工作。据我所知,它是一种泛型形式,Rust为您调用它的每一种类型实现Function。

最新更新