rust - una - ¿Por qué debería implementar métodos en un rasgo en lugar de como parte del rasgo?
que es una clase abstracta en poo (2)
Al intentar comprender mejor el rasgo
Any
, vi que
tiene un bloqueo
impl
para el rasgo en sí
.
No entiendo el propósito de esta construcción, o incluso si tiene un nombre específico.
Hice un pequeño experimento con un método de rasgo "normal" y un método definido en el bloque
impl
:
trait Foo {
fn foo_in_trait(&self) {
println!("in foo")
}
}
impl dyn Foo {
fn foo_in_impl(&self) {
println!("in impl")
}
}
impl Foo for u8 {}
fn main() {
let x = Box::new(42u8) as Box<dyn Foo>;
x.foo_in_trait();
x.foo_in_impl();
let y = &42u8 as &dyn Foo;
y.foo_in_trait();
y.foo_in_impl(); // May cause an error, see below
}
Nota del editor
En las versiones de Rust hasta Rust 1.15.0 inclusive, la línea
y.foo_in_impl()
provoca el error:
error: borrowed value does not live long enough --> src/main.rs:20:14 | 20 | let y = &42u8 as &Foo; | ^^^^ does not live long enough ... 23 | } | - temporary value only lives until here | = note: borrowed value must be valid for the static lifetime...
Este error ya no está presente en las versiones posteriores, pero los conceptos explicados en las respuestas siguen siendo válidos.
A partir de este experimento limitado, parece que los métodos definidos en el bloque
impl
son más restrictivos que los métodos definidos en el bloque de
trait
.
Es probable que haya algo extra que hacerlo de esta manera desbloquea, ¡pero todavía no sé qué es!
^ _ ^
Las secciones de
The Rust Programming Language
sobre
traits
y
objetos de rasgos
no mencionan esto.
Al buscar la fuente Rust en sí, parece que solo
Any
y
Error
usan esta característica en particular.
No he visto esto usado en el puñado de cajas donde he mirado el código fuente.
Cuando define un rasgo llamado
Foo
que se puede convertir en un objeto, Rust también define un tipo de objeto de rasgo llamado
dyn Foo
.
En versiones anteriores de Rust, este tipo solo se llamaba
Foo
(consulte
¿Qué significa "dyn" en un tipo?
).
Para la compatibilidad con estas versiones anteriores,
Foo
todavía trabaja para nombrar el tipo de objeto de rasgo, aunque la sintaxis
dyn
debe usar para el nuevo código.
Los objetos de rasgo tienen un parámetro de vida útil que designa el más corto de los parámetros de vida útil del implementador.
Para especificar esa duración, escriba el tipo como
dyn Foo + ''a
.
Cuando escribe
impl dyn Foo {
(o simplemente
impl Foo {
utilizando la sintaxis anterior), no está especificando ese parámetro de duración, y su valor predeterminado es
''static
.
Esta nota del compilador en
y.foo_in_impl();
declaración insinúa que:
nota: el valor prestado debe ser válido para la vida estática ...
Todo lo que tenemos que hacer para que esto sea más permisivo es escribir un
impl
genérico durante toda la vida:
impl<''a> dyn Foo + ''a {
fn foo_in_impl(&self) { println!("in impl") }
}
Ahora, observe que el argumento
self
en
foo_in_impl
es un puntero prestado, que tiene un parámetro propio de por vida.
El tipo de
self
, en su forma completa, se ve como
&''b (dyn Foo + ''a)
(los paréntesis son obligatorios debido a la precedencia del operador).
Un
Box<u8>
posee su
u8
, no toma prestado nada, por lo que puede crear un
&(dyn Foo + ''static)
partir de él.
Por otro lado,
&42u8
crea a
&''b (dyn Foo + ''a)
donde
''a
no es
''static
, porque
42u8
se coloca en una variable oculta en la pila, y el objeto rasgo toma prestada esta variable.
(Sin embargo, eso realmente no tiene sentido;
u8
no
u8
prestado nada, por lo que su implementación de
Foo
siempre debe ser compatible con
dyn Foo + ''static
... el hecho de que
42u8
se
42u8
prestado de la pila debería afectar
''b
, no
''a
.)
Otra cosa a tener en cuenta es que los métodos de rasgos son polimórficos, incluso cuando tienen una implementación predeterminada y no se anulan, mientras que los métodos inherentes a los objetos de rasgos son monomórficos (solo hay una función, sin importar qué hay detrás del rasgo). Por ejemplo:
use std::any::TypeId;
trait Foo {
fn foo_in_trait(&self)
where
Self: ''static,
{
println!("{:?}", TypeId::of::<Self>());
}
}
impl dyn Foo {
fn foo_in_impl(&self) {
println!("{:?}", TypeId::of::<Self>());
}
}
impl Foo for u8 {}
impl Foo for u16 {}
fn main() {
let x = Box::new(42u8) as Box<dyn Foo>;
x.foo_in_trait();
x.foo_in_impl();
let x = Box::new(42u16) as Box<Foo>;
x.foo_in_trait();
x.foo_in_impl();
}
Salida de muestra:
TypeId { t: 10115067289853930363 }
TypeId { t: 1357119791063736673 }
TypeId { t: 14525050876321463235 }
TypeId { t: 1357119791063736673 }
En el método de rasgo, obtenemos la identificación de tipo del tipo subyacente (aquí,
u8
o
u16
), por lo que podemos concluir que el tipo de
&self
variará de un implementador a otro (será
&u8
para el implementador
u8
y
&u16
para el implementador
u16
: no es un objeto de rasgo).
Sin embargo, en el método inherente, obtenemos el id de tipo de
dyn Foo
(
+ ''static
), por lo que podemos concluir que el tipo de
&self
es siempre
&dyn Foo
(un objeto de rasgo).
Sospecho que la razón es muy simple: ¿puede ser anulada o no?
Los implementadores del
trait
pueden anular un método implementado en un bloque de
trait
, solo proporciona un valor predeterminado.
Por otro lado, un método implementado en un bloque
impl
no puede ser anulado.
Si este razonamiento es correcto, entonces el error que obtiene para
Vea la respuesta más completa de Francis Gagné sobre la interacción con vidas.
y.foo_in_impl()
es solo una falta de pulido: debería haber funcionado.