Почему Rust не поддерживает вытеснение объекта черты?

Учитывая этот код:

trait Base {
    fn a(&self);
    fn b(&self);
    fn c(&self);
    fn d(&self);
}

trait Derived : Base {
    fn e(&self);
    fn f(&self);
    fn g(&self);
}

struct S;

impl Derived for S {
    fn e(&self) {}
    fn f(&self) {}
    fn g(&self) {}
}

impl Base for S {
    fn a(&self) {}
    fn b(&self) {}
    fn c(&self) {}
    fn d(&self) {}
}

К сожалению, я не могу бросить &Derived в &Base:

fn example(v: &Derived) {
    v as &Base;
}
error[E0605]: non-primitive cast: `&Derived` as `&Base`
  --> src/main.rs:30:5
   |
30 |     v as &Base;
   |     ^^^^^^^^^^
   |
   = note: an `as` expression can only be used to convert between primitive types. Consider using the `From` trait

Это почему? Derived vtable должен ссылаться на Base методы так или иначе.


Проверка LLVM IR показывает следующее:

@vtable4 = internal unnamed_addr constant {
    void (i8*)*,
    i64,
    i64,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*
} {
    void (i8*)* @_ZN2i813glue_drop.98717h857b3af62872ffacE,
    i64 0,
    i64 1,
    void (%struct.S*)* @_ZN6S.Base1a20h57ba36716de00921jbaE,
    void (%struct.S*)* @_ZN6S.Base1b20h3d50ba92e362d050pbaE,
    void (%struct.S*)* @_ZN6S.Base1c20h794e6e72e0a45cc2vbaE,
    void (%struct.S*)* @_ZN6S.Base1d20hda31e564669a8cdaBbaE
}

@vtable26 = internal unnamed_addr constant {
    void (i8*)*,
    i64,
    i64,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*
} {
    void (i8*)* @_ZN2i813glue_drop.98717h857b3af62872ffacE,
    i64 0,
    i64 1,
    void (%struct.S*)* @_ZN9S.Derived1e20h9992ddd0854253d1WaaE,
    void (%struct.S*)* @_ZN9S.Derived1f20h849d0c78b0615f092aaE,
    void (%struct.S*)* @_ZN9S.Derived1g20hae95d0f1a38ed23b8aaE,
    void (%struct.S*)* @_ZN6S.Base1a20h57ba36716de00921jbaE,
    void (%struct.S*)* @_ZN6S.Base1b20h3d50ba92e362d050pbaE,
    void (%struct.S*)* @_ZN6S.Base1c20h794e6e72e0a45cc2vbaE,
    void (%struct.S*)* @_ZN6S.Base1d20hda31e564669a8cdaBbaE
}

Все Vust-таблицы Rust содержат указатель на деструктор, размер и выравнивание в первых полях, а Vtable-таблицы вычитаний не дублируют их при обращении к методам supertrait и не используют косвенную ссылку на vtables supertrait. У них просто есть точные копии указателей метода и ничего больше.

Учитывая этот дизайн, легко понять, почему это не работает. Новый vtable должен быть создан во время выполнения, который, вероятно, будет находиться в стеке, и это не совсем элегантное (или оптимальное) решение.

Конечно, есть некоторые обходные пути, такие как добавление явных upcast-методов к интерфейсу, но для их правильной работы требуется немало шаблонных (или макро-безумных).

Теперь возникает вопрос - почему он не реализован каким-либо образом, который позволил бы вывести объект из черты? Например, добавление указателя на vtable супертрейта в vtable. На данный момент динамическая диспетчеризация Rust, похоже, не удовлетворяет принципу подстановки Лискова, который является очень основным принципом объектно-ориентированного проектирования.

Конечно, вы можете использовать статическую диспетчеризацию, которая действительно очень элегантна для использования в Rust, но она легко приводит к раздуванию кода, что иногда более важно, чем вычислительная производительность - как во встроенных системах, и разработчики Rust утверждают, что поддерживают такие варианты использования язык. Кроме того, во многих случаях вы можете успешно использовать модель, которая не является чисто объектно-ориентированной, что, похоже, поощряется функциональным дизайном Rust. Тем не менее, Rust поддерживает множество полезных шаблонов OO... так почему не LSP?

Кто-нибудь знает обоснование такого дизайна?

5 ответов

На самом деле, я думаю, у меня есть причина. Я нашел элегантный способ добавить поддержку апскейтинга к любой характеристике, которая этого требует, и таким образом программист может выбрать, добавлять ли эту дополнительную запись vtable в характеристику или нет, что является аналогичным компромиссом, как в С ++ виртуальные и не виртуальные методы: элегантность и корректность модели в сравнении с производительностью.

Код может быть реализован следующим образом:

trait Base: AsBase {
    // ...
}

trait AsBase {
    fn as_base(&self) -> &Base;
}

impl<T: Base> AsBase for T {
    fn as_base(&self) -> &Base {
        self
    }
}

Можно добавить дополнительные методы для приведения &mut указатель или Box (это добавляет требование, что T должен быть 'static типа), но это общая идея. Это обеспечивает безопасное и простое (хотя и неявное) обновление для каждого производного типа без шаблонов для каждого производного типа.

По состоянию на июнь 2017 года статус этого "принуждения к суб-чертам" (или "принуждения к сверх-чертам") выглядит следующим образом:

  • В принятом RFC № 0401 это упоминается как часть принуждения. Так что это преобразование должно быть сделано неявно.

    coerce_inner (Tзнак равно U где T это под-черта U;

  • Однако это еще не реализовано. Есть соответствующая проблема № 18600.

Существует также повторяющаяся проблема № 5665. Комментарии там объясняют, что мешает этому быть реализованным.

  • По сути, проблема в том, как вывести таблицы для супер-черт. Текущее расположение vtables выглядит следующим образом (в случае x86-64):
    + ----- + ------------------------------- +
    | 0- 7 | указатель на функцию "Drop Glue" |
    +-----+-------------------------------+
    | | 8-15| размер данных |
    +-----+-------------------------------+
    |16-23| выравнивание данных |
    +-----+-------------------------------+
    |24-  | Методы Самости и Супертренировки |
    +-----+-------------------------------+
    Он не содержит vtable для супер-черты как подпоследовательность. У нас есть, по крайней мере, несколько настроек с vtables.
  • Конечно, есть способы смягчить эту проблему, но многие из них имеют различные преимущества / недостатки! Один имеет преимущество для размера vtable, когда есть наследование алмазов. Другой должен быть быстрее.

Там @typelist говорит, что они подготовили проект RFC, который выглядит хорошо организованным, но после этого они выглядят как исчезнувшие (ноябрь 2016).

Я столкнулся с той же стеной, когда начал с Rust. Теперь, когда я думаю о чертах, я имею в виду другой образ, чем когда я думаю о классах.

trait X: Y {} значит, когда вы реализуете черту X для структуры S вам также нужно реализовать черту Y за S,

Конечно это означает, что &X знает, что это тоже &Yи, следовательно, предлагает соответствующие функции. Это потребует некоторых усилий во время выполнения (больше разыменований указателей), если вам нужно перебрать указатели на YПервым будет vtable.

С другой стороны, текущий дизайн + дополнительные указатели на другие vtables, вероятно, не сильно повредят и позволят реализовать простое приведение типов. Так может нам нужны оба? Это то, что будет обсуждаться на http://internals.rust-lang.org/

Эта функция настолько востребована, что существует проблема с отслеживанием ее добавления в язык и наличие специального репозитория инициатив для людей, участвующих в ее реализации.

Отслеживание проблемы: https://github.com/rust-lang/rust/issues/65991

Репозиторий инициативы: https://github.com/rust-lang/dyn-upcasting-coercion-initiative

Теперь это работает на стабильной ржавчине, вы можете преобразовать базовый трейт в базовый трейт, также вы можете вызывать функции базового трейта непосредственно из производного трейт-объекта.

      trait Base {
   fn a(&self) {
     println!("a from base");
   }
}

trait Derived: Base {
   fn e(&self) {
     println!("e from derived");
   }
}

fn call_derived(d: &impl Derived) {
   d.e();
   d.a();
   call_base(d);
}

fn call_base(b: &impl Base) {
   b.a();
}

struct S;
impl Base for S {}
impl Derived for S {}

fn main() {
   let s = S;
   call_derived(&s);
}

ссылка на игровую площадку

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