Estoy aprendiendo / experimentando con Rust, y con toda la elegancia que encuentro en este idioma, hay una peculiaridad que me desconcierta y parece totalmente fuera de lugar.
Rust desreferencia automáticamente los punteros al hacer llamadas a métodos. Hice algunas pruebas para determinar el comportamiento exacto:
struct X { val: i32 }
impl std::ops::Deref for X {
type Target = i32;
fn deref(&self) -> &i32 { &self.val }
}
trait M { fn m(self); }
impl M for i32 { fn m(self) { println!("i32::m()"); } }
impl M for X { fn m(self) { println!("X::m()"); } }
impl M for &X { fn m(self) { println!("&X::m()"); } }
impl M for &&X { fn m(self) { println!("&&X::m()"); } }
impl M for &&&X { fn m(self) { println!("&&&X::m()"); } }
trait RefM { fn refm(&self); }
impl RefM for i32 { fn refm(&self) { println!("i32::refm()"); } }
impl RefM for X { fn refm(&self) { println!("X::refm()"); } }
impl RefM for &X { fn refm(&self) { println!("&X::refm()"); } }
impl RefM for &&X { fn refm(&self) { println!("&&X::refm()"); } }
impl RefM for &&&X { fn refm(&self) { println!("&&&X::refm()"); } }
struct Y { val: i32 }
impl std::ops::Deref for Y {
type Target = i32;
fn deref(&self) -> &i32 { &self.val }
}
struct Z { val: Y }
impl std::ops::Deref for Z {
type Target = Y;
fn deref(&self) -> &Y { &self.val }
}
#[derive(Clone, Copy)]
struct A;
impl M for A { fn m(self) { println!("A::m()"); } }
impl M for &&&A { fn m(self) { println!("&&&A::m()"); } }
impl RefM for A { fn refm(&self) { println!("A::refm()"); } }
impl RefM for &&&A { fn refm(&self) { println!("&&&A::refm()"); } }
fn main() {
// I'll use @ to denote left side of the dot operator
(*X{val:42}).m(); // i32::m() , Self == @
X{val:42}.m(); // X::m() , Self == @
(&X{val:42}).m(); // &X::m() , Self == @
(&&X{val:42}).m(); // &&X::m() , Self == @
(&&&X{val:42}).m(); // &&&X:m() , Self == @
(&&&&X{val:42}).m(); // &&&X::m() , Self == *@
(&&&&&X{val:42}).m(); // &&&X::m() , Self == **@
println!("-------------------------");
(*X{val:42}).refm(); // i32::refm() , Self == @
X{val:42}.refm(); // X::refm() , Self == @
(&X{val:42}).refm(); // X::refm() , Self == *@
(&&X{val:42}).refm(); // &X::refm() , Self == *@
(&&&X{val:42}).refm(); // &&X::refm() , Self == *@
(&&&&X{val:42}).refm(); // &&&X::refm(), Self == *@
(&&&&&X{val:42}).refm(); // &&&X::refm(), Self == **@
println!("-------------------------");
Y{val:42}.refm(); // i32::refm() , Self == *@
Z{val:Y{val:42}}.refm(); // i32::refm() , Self == **@
println!("-------------------------");
A.m(); // A::m() , Self == @
// without the Copy trait, (&A).m() would be a compilation error:
// cannot move out of borrowed content
(&A).m(); // A::m() , Self == *@
(&&A).m(); // &&&A::m() , Self == &@
(&&&A).m(); // &&&A::m() , Self == @
A.refm(); // A::refm() , Self == @
(&A).refm(); // A::refm() , Self == *@
(&&A).refm(); // A::refm() , Self == **@
(&&&A).refm(); // &&&A::refm(), Self == @
}
( Área de juegos )
Entonces, parece que, más o menos:
- El compilador insertará tantos operadores de desreferencia como sea necesario para invocar un método.
- El compilador, al resolver métodos declarados usando
&self
(llamada por referencia):- Primero intenta pedir una única desreferencia de
self
- Luego intenta llamar al tipo exacto de
self
- Luego, intenta insertar tantos operadores de desreferencia como sea necesario para una coincidencia
- Primero intenta pedir una única desreferencia de
- Los métodos declarados usando
self
(llamada por valor) para el tipo seT
comportan como si se declararan usando&self
(llamada por referencia) para el tipo&T
y se llamaran a la referencia a lo que esté en el lado izquierdo del operador de punto. - Las reglas anteriores se prueban primero con la desreferenciación incorporada sin procesar, y si no hay coincidencia,
Deref
se utiliza la sobrecarga con el rasgo.
¿Cuáles son las reglas exactas de desreferencia automática? ¿Alguien puede dar alguna razón formal para tal decisión de diseño?
reference
dereference
formal-semantics
rust
kFYatek
fuente
fuente
Respuestas:
Su pseudocódigo es bastante correcto. Para este ejemplo, supongamos que tenemos una llamada al método
foo.bar()
wherefoo: T
. Voy a usar la sintaxis totalmente calificada (FQS) para no ser ambiguo sobre con qué tipo se llama al método, por ejemplo,A::bar(foo)
oA::bar(&***foo)
. Solo voy a escribir una pila de letras mayúsculas al azar, cada una es solo un tipo / rasgo arbitrario, exceptoT
que siempre es el tipo de la variable original sobre lafoo
que se invoca el método.El núcleo del algoritmo es:
U
(es decir, establecerU = T
y luegoU = *T
, ...)bar
donde el tipo de receptor (el tipo deself
en el método) coincideU
exactamente, úselo ( un "método por valor" )&
o&mut
del receptor) y, si el receptor de algún método coincide&U
, úselo ( un "método autorefd" )Notablemente, todo considera el "tipo de receptor" del método, no el
Self
tipo del rasgo, es decir,impl ... for Foo { fn method(&self) {} }
piensa&Foo
en la coincidencia del método yfn method2(&mut self)
pensaría&mut Foo
en la coincidencia.Es un error si alguna vez hay varios métodos de rasgo válidos en los pasos internos (es decir, solo puede haber cero o un método de rasgo válido en cada uno de 1. o 2., pero puede haber uno válido para cada uno: el uno de 1 se tomará primero), y los métodos inherentes tienen prioridad sobre los rasgos. También es un error si llegamos al final del ciclo sin encontrar nada que coincida. También es un error tener
Deref
implementaciones recursivas , que hacen que el bucle sea infinito (alcanzarán el "límite de recursividad").Estas reglas parecen hacer lo que quiero decir en la mayoría de las circunstancias, aunque tener la capacidad de escribir el formulario FQS inequívoco es muy útil en algunos casos extremos y para mensajes de error sensibles para el código generado por macro.
Solo se agrega una referencia automática porque
&foo
conserva una fuerte conexión confoo
(es la dirección defoo
sí misma), pero tomar más comienza a perderla:&&foo
es la dirección de alguna variable temporal en la pila que almacena&foo
.Ejemplos
Supongamos que tenemos una llamada
foo.refm()
, sifoo
tiene tipo:X
, luego comenzamos conU = X
,refm
tiene un tipo de receptor&...
, por lo que el paso 1 no coincide, tomar un auto-ref nos da&X
, y esto sí coincide (conSelf = X
), por lo que la llamada esRefM::refm(&foo)
&X
, comienza conU = &X
, que coincide&self
en el primer paso (conSelf = X
), por lo que la llamada esRefM::refm(foo)
&&&&&X
, esto no coincide con ninguno de los pasos (el rasgo no está implementado para&&&&X
o&&&&&X
), por lo que desreferenciamos una vez para obtenerU = &&&&X
, que coincide con 1 (conSelf = &&&X
) y la llamada esRefM::refm(*foo)
Z
, no coincide con ninguno de los pasos, por lo que se desreferencia una vez, para obtenerY
, que tampoco coincide, por lo que se vuelve a desreferenciar para obtenerX
, que no coincide con 1, pero coincide después de la autorefinación, por lo que la llamada esRefM::refm(&**foo)
.&&A
, 1. no coincide y tampoco 2. ya que el rasgo no está implementado para&A
(para 1) o&&A
(para 2), por lo que se desreferencia a&A
, que coincide con 1., conSelf = A
Supongamos que tenemos
foo.m()
, y esoA
no esCopy
, sifoo
tiene tipo:A
, luegoU = A
coincideself
directamente para que la llamada seaM::m(foo)
conSelf = A
&A
, entonces 1. no coincide, y tampoco lo hace 2. (&A
ni&&A
implementa el rasgo), por lo que se desreferencia aA
, lo que sí coincide, peroM::m(*foo)
requiere tomarA
por valor yfoo
, por lo tanto, salir de , de ahí el error.&&A
, 1. no coincide, pero la autorrefinación da&&&A
, que sí coincide, por lo que la llamada esM::m(&foo)
conSelf = &&&A
.(Esta respuesta se basa en el código y está razonablemente cerca del archivo README (ligeramente desactualizado) . Niko Matsakis, el autor principal de esta parte del compilador / lenguaje, también echó un vistazo a esta respuesta.
fuente
&&String
->&String
->String
->str
) y luego hará referencia al máximo una vez (str
->&str
)".La referencia de Rust tiene un capítulo sobre la expresión de llamada al método . Copié la parte más importante a continuación. Recordatorio: estamos hablando de una expresión
recv.m()
, donderecv
se llama "expresión del receptor" a continuación.( Nota sobre [¹] : De hecho, creo que esta redacción está mal. He abierto un problema . Ignoremos esa oración entre paréntesis).
¡Veamos algunos ejemplos de su código en detalle! Para sus ejemplos, podemos ignorar la parte sobre "coerción no dimensionada" y "métodos inherentes".
(*X{val:42}).m()
: el tipo de expresión del receptor esi32
. Realizamos estos pasos:i32
no se puede desreferenciar, por lo que ya hemos terminado con el paso 1. Lista:[i32]
&i32
y&mut i32
. Lista:[i32, &i32, &mut i32]
<i32 as M>::m
cuál tiene el tipo de receptori32
. Entonces ya hemos terminado.Hasta ahora muy fácil. Ahora vamos a elegir un ejemplo más difícil
(&&A).m()
. El tipo de expresión del receptor es&&A
. Realizamos estos pasos:&&A
puede ser desreferenciado&A
, así que lo agregamos a la lista.&A
puede desreferenciarse nuevamente, por lo que también agregamosA
a la lista.A
no puede ser desreferenciado, así que nos detenemos. Lista:[&&A, &A, A]
T
en la lista, agregamos&T
e&mut T
inmediatamente despuésT
. Lista:[&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
&&A
, por lo que pasamos al siguiente tipo en la lista.<&&&A as M>::m
que de hecho tiene el tipo de receptor&&&A
. Entonces hemos terminado.Aquí están las listas de receptores candidatos para todos sus ejemplos. El tipo encerrado
⟪x⟫
es el que "ganó", es decir, el primer tipo para el que se pudo encontrar un método de ajuste. Recuerde también que el primer tipo en la lista es siempre el tipo de expresión del receptor. Por último, formateé la lista en líneas de tres, pero eso es solo formato: esta lista es una lista plana.(*X{val:42}).m()
→<i32 as M>::m
X{val:42}.m()
→<X as M>::m
(&X{val:42}).m()
→<&X as M>::m
(&&X{val:42}).m()
→<&&X as M>::m
(&&&X{val:42}).m()
→<&&&X as M>::m
(&&&&X{val:42}).m()
→<&&&X as M>::m
(&&&&&X{val:42}).m()
→<&&&X as M>::m
(*X{val:42}).refm()
→<i32 as RefM>::refm
X{val:42}.refm()
→<X as RefM>::refm
(&X{val:42}).refm()
→<X as RefM>::refm
(&&X{val:42}).refm()
→<&X as RefM>::refm
(&&&X{val:42}).refm()
→<&&X as RefM>::refm
(&&&&X{val:42}).refm()
→<&&&X as RefM>::refm
(&&&&&X{val:42}).refm()
→<&&&X as RefM>::refm
Y{val:42}.refm()
→<i32 as RefM>::refm
Z{val:Y{val:42}}.refm()
→<i32 as RefM>::refm
A.m()
→<A as M>::m
(&A).m()
→<A as M>::m
(&&A).m()
→<&&&A as M>::m
(&&&A).m()
→<&&&A as M>::m
A.refm()
→<A as RefM>::refm
(&A).refm()
→<A as RefM>::refm
(&&A).refm()
→<A as RefM>::refm
(&&&A).refm()
→<&&&A as RefM>::refm
fuente