Как передать трейт-объект в штучной упаковке по значению в 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 указанной сигнатурой, чтобы возвращаемое значение было тем значением, которое будет получено путем вызова consume для значения Box'd.

Я изначально написал

ptr.consume()

как тело функции, хотя я понимаю, что это не совсем правильная идея, поскольку она не передает тот факт, что я хочу, чтобы Box потреблялся, а не только его содержимое, но это единственное, о чем я мог подумать. из. Не компилируется, выдает ошибку:

невозможно переместить значение типа dyn Consumable: размер dyn Consumable не может быть определен статически

Это было несколько неожиданно для меня, поскольку я новичок в Rust, я подумал, что, возможно, аргумент self передается аналогично ссылке rvalue в C++ (это действительно то, что я хочу — в C++ я, вероятно, реализовал бы это с помощью метода с подпись virtual std::uint64_t consume() &&, позволяя std::unique_ptr очистить объект, перемещенный из, с помощью виртуального деструктора), но я думаю, что Rust действительно передает по значению, перемещая аргумент на место раньше, поэтому разумно, что он отклоняет код.

Проблема в том, что я не знаю, как добиться желаемого поведения, когда я могу потреблять трейт-объект Box'd. Я попытался добавить метод к трейту с реализацией по умолчанию, думая, что это может принести мне что-то полезное в 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 известен во время компиляции, вы можете добавить : Sized к свойству

vallentin 12.12.2020 04:57

@vallentin Я только что попытался изменить первую строку на trait Consumable: Sized, но она жалуется, что «черта Consumable не может быть в объекте ... потому что для этого требуется Self: Sized» - я думаю, что она жалуется, что не знает размер объекта черты во время компиляции, а не размер какого-либо конкретного исполнителя (хотя я также не пытаюсь использовать какие-либо трюки с реализацией трейта для странных вещей)

Milo Brandt 12.12.2020 05:03

@vallentin Я добавил несколько фрагментов, чтобы дать представление о том, какие конкретные типы будут реализовывать эту черту, если это полезно.

Milo Brandt 12.12.2020 05:18
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
8
3
3 491
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Вы можете потреблять из Box<dyn Trait>, если параметр равен self: Box<Self>:

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() для каждой реализации; попытка определить реализацию по умолчанию приведет к ошибке «невозможно переместить значение типа Self - размер Self не может быть определен статически».


В общем, хотя это не поддерживается. dyn Consumable представляет собой неразмерный тип, который очень ограничен, за исключением косвенности (через ссылки или Box-подобные структуры). Это работает для приведенного выше случая, потому что Box немного особенный (это единственный диспетчеризируемый тип, которым вы можете стать владельцем), а метод consume_box не помещает self в стек как динамический объект типажа (только в каждой реализации, где его конкретный).

Однако есть RFC 1909: Unsized RValues ​​, который надеется ослабить некоторые из этих ограничений. Один из них может передавать параметры функции без размера, например 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());
}

Смотрите на детской площадке.

Я считаю

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

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

может делать то, что вы хотите. Я почти эксперт по Rust — или эксперт по C++, если на то пошло — но я думаю, что это должно работать почти так же, как семантика перемещения в C++, которую вы упомянули с точки зрения поведения памяти. Насколько я понимаю, это форма обобщения, в которой Rust реализует функцию для каждого типа, с которым вы ее вызываете.

Это не работает, потому что Box<dyn Consumable> не реализует Consumable.

jthulhu 30.12.2022 18:21

@jthulhu извините за поздний ответ, я давно не был на SO. Вы конечно правы. С тем, что я узнал, так как я вижу несколько вещей, которые можно сделать, которые не решают исходный вопрос, но выглядят похожими, где размер известен во время компиляции. Вот площадка: play.rust-lang.org/… . Не знаю, как добавить это в эту тему, возможно, я предложу отредактировать предложенный ответ, который добавляет по крайней мере метод «где Self: Sized» с его ограничениями.

Sam96 02.04.2023 20:43

Если вы не используете nightly Rust, я написал макрос здесь. Он автоматически генерирует вторую функцию типажа.

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

Это предотвращает превращение Consumable в трейт-объект.

jthulhu 30.12.2022 18:13

@jthulhu Его все еще можно использовать как трейт-объект, если параметр называется self: площадка . Из безопасности объектаBox<Self> можно отправить, а self: Box<Self> является допустимым получателем.

kmdreko 30.12.2022 18:22

Другие вопросы по теме