Почему 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);
}