reference dereference formal-semantics rust

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



dereference formal-semantics (1)

Su pseudocódigo es bastante correcto. Para este ejemplo, supongamos que tenemos un método llamado foo.bar() donde foo: T Voy a usar la sintaxis totalmente calificada (FQS) para no ser ambiguo sobre el tipo de método con el que se llama, por ejemplo, A::bar(foo) o A::bar(&***foo) . Solo voy a escribir un montón de letras mayúsculas al azar, cada una es solo un tipo / rasgo arbitrario, excepto que T es siempre el tipo de la variable original foo que se llama el método.

El núcleo del algoritmo es:

Notablemente, todo considera el "tipo de receptor" del método, no el tipo Self del rasgo, es decir, impl ... for Foo { fn method(&self) {} } piensa en &Foo cuando coincide con el método, y fn method2(&mut self) pensaría en &mut Foo al hacer coincidir.

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 implementaciones recursivas de Deref , 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
  • tomando una referencia &foo conserva una fuerte conexión con foo (es la dirección de foo sí), 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() , si foo tiene tipo:

  • X , entonces comenzamos con U = X , refm tiene el tipo de receptor &... , por lo que el paso 1 no coincide, tomar una referencia automática nos da &X , y esto sí coincide (con Self = X ), entonces la llamada es RefM::refm(&foo)
  • &X , comienza con U = &X , que coincide con &self en el primer paso (con Self = X ), por lo que la llamada es RefM::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 obtener U = &&&&X , que coincide con 1 (con Self = &&&X ) y la llamada es RefM::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 , el 1. no coincide y tampoco 2. ya que el rasgo no se implementa para &A (para 1) o &&A (para 2), por lo que se desreferencia a &A , que coincide con 1., con Self = A

Supongamos que tenemos foo.m() , y que A no es Copy , si foo tiene tipo:

  • A , entonces U = A coincide con self directamente, por lo que la llamada es M::m(foo) con Self = A
  • &A , entonces 1. no coincide, y tampoco 2. (ni &A ni &&A implementan el rasgo), por lo que se desreferencia a A , que sí coincide, pero M::m(*foo) requiere tomar A por valor y, por lo tanto, salir de foo , de ahí el error.
  • &&A , 1. no coincide, pero el autoajuste proporciona &&&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.

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<''a> M for &''a X { fn m(self) { println!("&X::m()"); } } impl<''a, ''b> M for &''a &''b X { fn m(self) { println!("&&X::m()"); } } impl<''a, ''b, ''c> M for &''a &''b &''c 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<''a> RefM for &''a X { fn refm(&self) { println!("&X::refm()"); } } impl<''a, ''b> RefM for &''a &''b X { fn refm(&self) { println!("&&X::refm()"); } } impl<''a, ''b, ''c> RefM for &''a &''b &''c 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 } } struct A; impl std::marker::Copy for A {} impl M for A { fn m(self) { println!("A::m()"); } } impl<''a, ''b, ''c> M for &''a &''b &''c A { fn m(self) { println!("&&&A::m()"); } } impl RefM for A { fn refm(&self) { println!("A::refm()"); } } impl<''a, ''b, ''c> RefM for &''a &''b &''c 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::refm() , 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 == **@ (*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 == **@ Y{val:42}.refm(); // i32::refm() , self == *@ Z{val:Y{val:42}}.refm(); // i32::refm() , self == **@ 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 == @ }

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 T comportan como si se declararan usando &self (llamada por referencia) para el tipo &T y se invoque la referencia a lo que esté en el lado izquierdo del operador de puntos.
  • Las reglas anteriores se Deref primero con la desreferenciación incorporada sin procesar, y si no hay coincidencia, se usa la sobrecarga con el rasgo de Deref .

¿Cuáles son las reglas exactas de desreferencia automática? ¿Alguien puede dar alguna razón formal para tal decisión de diseño?