¿Cuáles son las reglas exactas de auto-referencia de Rust?

181

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
  • Los métodos declarados usando self(llamada por valor) para el tipo se Tcomportan como si se declararan usando &self(llamada por referencia) para el tipo &Ty 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, Derefse 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?

kFYatek
fuente
¡He publicado esto en el subreddit Rust con la esperanza de obtener algunas buenas respuestas!
Shepmaster
Para mayor diversión, intente repetir el experimento en genéricos y compare los resultados.
user2665887

Respuestas:

137

Su pseudocódigo es bastante correcto. Para este ejemplo, supongamos que tenemos una llamada al método foo.bar()where foo: 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)o A::bar(&***foo). Solo voy a escribir una pila de letras mayúsculas al azar, cada una es solo un tipo / rasgo arbitrario, excepto Tque siempre es el tipo de la variable original sobre la fooque se invoca el método.

El núcleo del algoritmo es:

  • Para cada "paso de desreferencia" U (es decir, establecer U = Ty luego U = *T, ...)
    1. Si hay un método bardonde el tipo de receptor (el tipo de selfen el método) coincide Uexactamente, úselo ( un "método por valor" )
    2. de lo contrario, agregue una referencia automática (toma &o &mutdel 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 Selftipo del rasgo, es decir, impl ... for Foo { fn method(&self) {} }piensa &Fooen la coincidencia del método y fn method2(&mut self)pensaría &mut Fooen 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 Derefimplementaciones 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

  • si no hubo límite, las cosas se ponen mal / lentas, ya que cada tipo puede tener un número arbitrario de referencias tomadas
  • tomar una referencia &fooconserva una fuerte conexión con foo(es la dirección de foosí misma), pero tomar más comienza a perderla: &&fooes la dirección de alguna variable temporal en la pila que almacena &foo.

Ejemplos

Supongamos que tenemos una llamada foo.refm(), si footiene tipo:

  • X, luego comenzamos con U = X, refmtiene un tipo de receptor &..., por lo que el paso 1 no coincide, tomar un auto-ref nos da &X, y esto sí coincide (con Self = X), por lo que la llamada esRefM::refm(&foo)
  • &X, comienza con U = &X, que coincide &selfen el primer paso (con Self = X), por lo que la llamada esRefM::refm(foo)
  • &&&&&X, esto no coincide con ninguno de los pasos (el rasgo no está implementado para &&&&Xo &&&&&X), por lo que desreferenciamos una vez para obtener U = &&&&X, que coincide con 1 (con Self = &&&X) y la llamada esRefM::refm(*foo)
  • Z, no coincide con ninguno de los pasos, por lo que se desreferencia una vez, para obtener Y, que tampoco coincide, por lo que se vuelve a desreferenciar para obtener X, que no coincide con 1, pero coincide después de la autorefinación, por lo que la llamada es RefM::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 eso Ano es Copy, si footiene tipo:

  • A, luego U = Acoincide selfdirectamente para que la llamada sea M::m(foo)conSelf = A
  • &A, entonces 1. no coincide, y tampoco lo hace 2. ( &Ani &&Aimplementa el rasgo), por lo que se desreferencia a A, lo que sí coincide, pero M::m(*foo)requiere tomar Apor valor y foo, 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 es M::m(&foo)con Self = &&&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.

huon
fuente
15
Esta respuesta parece exhaustiva y detallada, pero creo que carece de un resumen breve y accesible de las reglas. Shepmaster da un resumen de este tipo en este comentario : "[el algoritmo de eliminación] eliminará tantas veces como sea posible ( &&String-> &String-> String-> str) y luego hará referencia al máximo una vez ( str-> &str)".
Lii
(No sé cuán precisa y completa es esa explicación).
Lii
1
¿En qué casos se produce la desreferencia automática? ¿Se usa solo para la expresión del receptor para la llamada al método? ¿Para accesos de campo también? Asignación a la derecha? Lados izquierdos? Parámetros de función? Expresiones de valor de retorno?
Lii
1
Nota: Actualmente, el nomicon tiene una nota TODO para robar información de esta respuesta y escribirla en static.rust-lang.org/doc/master/nomicon/dot-operator.html
SamB
1
¿Se intenta la coerción (A) antes de esto o (B) después de esto o (C) se intenta en cada paso de este algoritmo o (D) algo más?
haslersn
8

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(), donde recvse llama "expresión del receptor" a continuación.

El primer paso es crear una lista de tipos de receptores candidatos. Para obtenerlos, haga referencia repetidamente al tipo de expresión del receptor, agregue cada tipo encontrado a la lista, luego intente finalmente una coerción no dimensionada al final y agregue el tipo de resultado si es exitoso. Luego, para cada candidato T, agregue &Ty &mut Ta la lista inmediatamente después T.

Por ejemplo, si el receptor tiene tipo Box<[i32;2]>, entonces los tipos candidato será Box<[i32;2]>, &Box<[i32;2]>, &mut Box<[i32;2]>, [i32; 2](por eliminación de referencias), &[i32; 2], &mut [i32; 2], [i32](por coacción no encolado), &[i32]y, por último &mut [i32].

Luego, para cada tipo de candidato T, busque un método visible con un receptor de ese tipo en los siguientes lugares:

  1. T's métodos inherentes (métodos implementados directamente en T[¹]).
  2. Cualquiera de los métodos proporcionados por un rasgo visible implementado por T. [...]

( 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 es i32. Realizamos estos pasos:

  • Crear una lista de tipos de receptores candidatos:
    • i32 no se puede desreferenciar, por lo que ya hemos terminado con el paso 1. Lista: [i32]
    • A continuación, agregamos &i32y &mut i32. Lista:[i32, &i32, &mut i32]
  • Búsqueda de métodos para cada tipo de receptor candidato:
    • Encontramos <i32 as M>::mcuál tiene el tipo de receptor i32. 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:

  • Crear una lista de tipos de receptores candidatos:
    • &&Apuede ser desreferenciado &A, así que lo agregamos a la lista. &Apuede desreferenciarse nuevamente, por lo que también agregamos Aa la lista. Ano puede ser desreferenciado, así que nos detenemos. Lista:[&&A, &A, A]
    • A continuación, para cada tipo Ten la lista, agregamos &Te &mut Tinmediatamente después T. Lista:[&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
  • Búsqueda de métodos para cada tipo de receptor candidato:
    • No hay ningún método con el tipo de receptor &&A, por lo que pasamos al siguiente tipo en la lista.
    • Encontramos el método <&&&A as M>::mque 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
    [i32, &i32, &mut i32]
  • X{val:42}.m()<X as M>::m
    [⟪X⟫, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&X{val:42}).m()<&X as M>::m
    [&X⟫, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&X{val:42}).m()<&&X as M>::m
    [&&X⟫, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&X{val:42}).m()<&&&X as M>::m
    [&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&X{val:42}).m()<&&&X as M>::m
    [&&&&X, &&&&&X, &mut &&&&X,&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&&X{val:42}).m()<&&&X as M>::m
    [&&&&&X, &&&&&&X, &mut &&&&&X, 
     &&&&X, &&&&&X, &mut &&&&X,&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]


  • (*X{val:42}).refm()<i32 as RefM>::refm
    [i32,&i32, &mut i32]
  • X{val:42}.refm()<X as RefM>::refm
    [X,&X⟫, &mut X, 
     i32, &i32, &mut i32]
  • (&X{val:42}).refm()<X as RefM>::refm
    [&X⟫, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&X{val:42}).refm()<&X as RefM>::refm
    [&&X⟫, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&X{val:42}).refm()<&&X as RefM>::refm
    [&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&X{val:42}).refm()<&&&X as RefM>::refm
    [&&&&X⟫, &&&&&X, &mut &&&&X, 
     &&&X, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&&X{val:42}).refm()<&&&X as RefM>::refm
    [&&&&&X, &&&&&&X, &mut &&&&&X,&&&&X⟫, &&&&&X, &mut &&&&X, 
     &&&X, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]


  • Y{val:42}.refm()<i32 as RefM>::refm
    [Y, &Y, &mut Y,
     i32,&i32, &mut i32]
  • Z{val:Y{val:42}}.refm()<i32 as RefM>::refm
    [Z, &Z, &mut Z,
     Y, &Y, &mut Y,
     i32,&i32, &mut i32]


  • A.m()<A as M>::m
    [⟪A⟫, &A, &mut A]
  • (&A).m()<A as M>::m
    [&A, &&A, &mut &A,
     ⟪A⟫, &A, &mut A]
  • (&&A).m()<&&&A as M>::m
    [&&A,&&&A⟫, &mut &&A,
     &A, &&A, &mut &A,
     A, &A, &mut A]
  • (&&&A).m()<&&&A as M>::m
    [&&&A⟫, &&&&A, &mut &&&A,
     &&A, &&&A, &mut &&A,
     &A, &&A, &mut &A,
     A, &A, &mut A]
  • A.refm()<A as RefM>::refm
    [A,&A⟫, &mut A]
  • (&A).refm()<A as RefM>::refm
    [&A⟫, &&A, &mut &A,
     A, &A, &mut A]
  • (&&A).refm()<A as RefM>::refm
    [&&A, &&&A, &mut &&A,&A⟫, &&A, &mut &A,
     A, &A, &mut A]
  • (&&&A).refm()<&&&A as RefM>::refm
    [&&&A,&&&&A⟫, &mut &&&A,
     &&A, &&&A, &mut &&A,
     &A, &&A, &mut &A,
     A, &A, &mut A]
Lukas Kalbertodt
fuente