oop - ¿Por qué Rust no admite la conversión ascendente de objetos de rasgos?
language-design lsp (3)
Dado este código:
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) {}
}
Desafortunadamente, no puedo lanzar
&Derived
a
&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
¿Porqué es eso?
La vtable
Derived
tiene que hacer referencia a los métodos
Base
de una forma u otra.
La inspección del LLVM IR revela lo siguiente:
@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
}
Todas las vtables de Rust contienen un puntero al destructor, el tamaño y la alineación en los primeros campos, y las vtables de subtrait no las duplican al hacer referencia a los métodos de supertrait, ni utilizan referencias indirectas a las supertrait vtables. Solo tienen copias de los punteros del método textualmente y nada más.
Dado ese diseño, es fácil entender por qué esto no funciona. Sería necesario construir una nueva vtable en tiempo de ejecución, que probablemente residiría en la pila, y esa no es exactamente una solución elegante (u óptima).
Por supuesto, hay algunas soluciones alternativas, como agregar métodos explícitos de transmisión a la interfaz, pero eso requiere un poco de repetitivo (o frenesí macro) para funcionar correctamente.
Ahora, la pregunta es: ¿por qué no se implementa de alguna manera que permita la conversión de objetos de rasgos? Como agregar un puntero a la vtable del supertrait en la vtable del substrait. Por ahora, el despacho dinámico de Rust no parece satisfacer el principio de sustitución de Liskov , que es un principio muy básico para el diseño orientado a objetos.
Por supuesto, puede usar el envío estático, que de hecho es muy elegante de usar en Rust, pero fácilmente conduce a una acumulación de código que a veces es más importante que el rendimiento computacional, como en los sistemas integrados, y los desarrolladores de Rust afirman que admiten tales casos de uso del idioma. Además, en muchos casos puede usar con éxito un modelo que no está orientado exclusivamente a objetos, lo que parece alentarse por el diseño funcional de Rust. Aún así, Rust admite muchos de los patrones OO útiles ... entonces, ¿por qué no el LSP?
¿Alguien sabe la razón de tal diseño?
A partir de junio de 2017, el estado de esta "coerción de subtrait" (o "coerción de supertrait") es la siguiente:
-
Un RFC
#0401
aceptado menciona esto como parte de la coerción.
Entonces esta conversión debe hacerse implícitamente.
coerce_inner (
T
) =U
dondeT
es un sub-rasgo deU
; - Sin embargo, esto aún no está implementado. Hay un problema correspondiente #18600 .
También hay un problema duplicado github.com/rust-lang/rust/issues/5665 . Los comentarios explican qué impide que esto se implemente.
-
Básicamente, el problema es cómo derivar vtables para super-rasgos.
El diseño actual de vtables es el siguiente (en el caso x86-64):
+-----+-------------------------------+ | 0- 7|pointer to "drop glue" function| +-----+-------------------------------+ | 8-15|size of the data | +-----+-------------------------------+ |16-23|alignment of the data | +-----+-------------------------------+ |24- |methods of Self and supertraits| +-----+-------------------------------+ No contiene una vtable para un súper rasgo como subsecuencia. Tenemos al menos algunos ajustes con vtables.
- Por supuesto, hay formas de mitigar este problema, ¡pero muchas tienen diferentes ventajas / desventajas! Uno tiene un beneficio para el tamaño de la tabla cuando hay una herencia de diamantes. Se supone que otro es más rápido.
Allí @typelist dice que prepararon un borrador de RFC que parece estar bien organizado, pero parece que desaparecieron después de eso (noviembre de 2016).
En realidad, creo que tengo la razón. Encontré una forma elegante de agregar soporte de transmisión a cualquier rasgo que lo desee, y de esa manera el programador puede elegir si agregar esa entrada vtable adicional al rasgo, o prefiere no hacerlo, lo cual es una compensación similar a la de Los métodos virtuales y no virtuales de C ++: elegancia y corrección del modelo versus rendimiento.
El código se puede implementar de la siguiente manera:
trait Base: AsBase {
// ...
}
trait AsBase {
fn as_base(&self) -> &Base;
}
impl<T: Base> AsBase for T {
fn as_base(&self) -> &Base {
self
}
}
Se pueden agregar métodos adicionales para lanzar un puntero
&mut
o un
Box
(que agrega el requisito de que
T
debe ser un
''static
tipo
''static
), pero esta es una idea general.
Esto permite una transmisión segura y simple (aunque no implícita) de cada tipo derivado sin repetitivo para cada tipo derivado.
Me encontré con la misma pared cuando comencé con Rust. Ahora, cuando pienso en los rasgos, tengo una imagen diferente en mente que cuando pienso en las clases.
trait X: Y {}
significa que cuando implementa el rasgo
X
para la estructura
S
, también
necesita
implementar el rasgo
Y
para
S
Por supuesto, esto significa que a
&X
sabe que también es a
&Y
y, por lo tanto, ofrece las funciones adecuadas.
Requeriría un poco de tiempo de ejecución (más desreferencia de puntero) si necesitara atravesar punteros a la tabla de
Y
primero.
Por otra parte, el diseño actual + punteros adicionales a otras vtables probablemente no dañarían mucho, y permitirían implementar fácilmente la conversión. Entonces, ¿tal vez necesitamos ambos? Esto es algo que se discutirá en internals.rust-lang.org