¿Por qué Rust no admite la actualización de objetos de rasgos?
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 transmitir &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 Derived
vtable tiene que hacer referencia a los Base
métodos 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 los duplican cuando se hace referencia a métodos de supertrait, ni utilizan referencias indirectas a vtables de supertrait. Solo tienen copias textuales de los punteros del método y nada más.
Dado ese diseño, es fácil entender por qué esto no funciona. Sería necesario construir una nueva tabla virtual 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, existen algunas soluciones, como agregar métodos upcast explícitos a la interfaz, pero eso requiere bastante repetición (o frenesí de macros) para funcionar correctamente.
Ahora, la pregunta es: ¿por qué no se implementa de alguna manera que permita la actualización de objetos de rasgos? Por ejemplo, agregar un puntero a la vtable del superrasgo en la vtable del subrasgo. 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 sobrecarga 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 admitir tales casos de uso del idioma. Además, en muchos casos se puede utilizar con éxito un modelo que no esté puramente orientado a objetos, lo que parece potenciado 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 el motivo de tal diseño?
En realidad, creo que entendí la razón. Encontré una manera elegante de agregar soporte de upcasting a cualquier rasgo que lo desee, y de esa manera el programador puede elegir si agrega esa entrada vtable adicional al rasgo, o prefiere no hacerlo, lo cual es una compensación similar a la de Métodos virtuales versus 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 convertir un &mut
puntero o a Box
(que agrega el requisito de que T
debe ser un 'static
tipo), pero esta es una idea general. Esto permite una conversión ascendente segura y sencilla (aunque no implícita) de cada tipo derivado sin texto repetitivo para cada tipo derivado.
A junio de 2017, el estado de esta "coerción de subrasgo" (o "coerción de superrasgo") es el siguiente:
- Un RFC #0401 aceptado menciona esto como parte de la coerción. Por tanto, esta conversión debe realizarse de forma implícita.
coerce_inner(
T
) =U
dondeT
es un subrasgo deU
; - Sin embargo, esto aún no se ha implementado. Hay un problema correspondiente #18600 .
También hay un número duplicado n.º 5665 . Los comentarios allí explican qué impide que esto se implemente.
- Básicamente, el problema es cómo derivar vtables para superrasgos. El diseño actual de vtables es el siguiente (en el caso x86-64):
+-----+-------------------------------+ | 0- 7|puntero a la función "soltar pegamento"| +-----+-------------------------------+ | 8-15|tamaño de los datos | +-----+-------------------------------+ |16-23|alineación de los datos | +-----+-------------------------------+ |24- |métodos de Self y superrasgos| +-----+-------------------------------+
No contiene una tabla virtual para un superrasgo como subsecuencia. Al menos tenemos que hacer algunos ajustes con vtables. - Por supuesto, hay formas de mitigar este problema, ¡pero muchas con diferentes ventajas y desventajas! Se tiene un beneficio para el tamaño de vtable 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 bien organizado, pero parece que desapareció después de eso (noviembre de 2016).