Quali sono le esatte regole di auto-dereferenziazione di Rust?


181

Sto imparando / sperimentando Rust, e in tutta l'eleganza che trovo in questa lingua, c'è una peculiarità che mi sconcerta e sembra totalmente fuori posto.

Rust dereferenzia automaticamente i puntatori quando si effettuano chiamate di metodo. Ho fatto alcuni test per determinare il comportamento esatto:

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 == @
}

( Parco giochi )

Quindi, sembra che, più o meno:

  • Il compilatore inserirà tutti gli operatori di dereference necessari per invocare un metodo.
  • Il compilatore, durante la risoluzione dei metodi dichiarati utilizzando &self(call-by-reference):
    • Per prima cosa cerca di chiamare una singola dereferenza di self
    • Quindi prova a chiamare il tipo esatto di self
    • Quindi, prova a inserire tutti gli operatori di dereference necessari per una corrispondenza
  • I metodi dichiarati usando self(call-by-value) per il tipo si Tcomportano come se fossero stati dichiarati usando &self(call-by-reference) per il tipo &Te chiamassero il riferimento a qualunque cosa si trovi sul lato sinistro dell'operatore punto.
  • Le regole di cui sopra vengono prima provate con il dereferenziamento incorporato non elaborato e, se non vi è corrispondenza, Derefviene utilizzato il sovraccarico con tratto.

Quali sono le esatte regole di auto-dereferenziazione? Qualcuno può dare una logica formale per tale decisione progettuale?


Ho inviato questo post al subreddit Rust nella speranza di ottenere delle buone risposte!
Shepmaster,

Per un divertimento extra prova a ripetere l'esperimento in generici e confronta i risultati.
user2665887,

Risposte:


137

Il tuo pseudo-codice è praticamente corretto. Per questo esempio, supponiamo di avere una chiamata al metodo in foo.bar()cui foo: T. Ho intenzione di utilizzare la sintassi completo (FQS) di essere inequivocabile su quale tipo il metodo viene chiamato con, ad esempio, A::bar(foo)o A::bar(&***foo). Scriverò solo una pila di lettere maiuscole casuali, ognuna è solo un tipo / tratto arbitrario, tranne che Tè sempre il tipo della variabile originale su foocui viene chiamato il metodo.

Il nucleo dell'algoritmo è:

  • Per ogni "passaggio di dereference" U (ovvero, impostare U = Te quindi U = *T, ...)
    1. se esiste un metodo in barcui il tipo di ricevitore (il tipo di selfnel metodo) corrisponde Uesattamente, usalo ( un "metodo per valore" )
    2. altrimenti, aggiungi un auto-ref (take &o &mutdel ricevitore) e, se il ricevitore di un metodo corrisponde &U, usalo ( un "metodo autorefd" )

In particolare, tutto considera il "tipo di ricevitore" del metodo, non il Selftipo di tratto, vale a impl ... for Foo { fn method(&self) {} }dire &Fooquando si confronta il metodo e fn method2(&mut self)si pensa &mut Fooquando si abbina.

È un errore se ci sono mai più metodi di tratto validi nei passaggi interni (cioè, ci possono essere solo zero o un metodo di tratto valido in ciascuno di 1. o 2., ma può esserne uno valido per ciascuno: quello da 1 sarà preso per primo), e i metodi inerenti hanno la precedenza su quelli tratti. È anche un errore se arriviamo alla fine del ciclo senza trovare nulla che corrisponda. È anche un errore avere Derefimplementazioni ricorsive , che rendono il loop infinito (colpiranno il "limite di ricorsione").

Queste regole sembrano fare ciò che intendo nella maggior parte dei casi, sebbene avere la capacità di scrivere il modulo FQS non ambiguo sia molto utile in alcuni casi limite e per messaggi di errore sensibili per il codice generato da macro.

Viene aggiunto un solo riferimento automatico perché

  • se non c'era limite, le cose diventano cattive / lente, poiché ogni tipo può avere un numero arbitrario di riferimenti presi
  • prendere un riferimento &foomantiene una forte connessione foo(è l'indirizzo di foose stesso), ma prendere più inizia a perderlo: &&fooè l'indirizzo di qualche variabile temporanea nello stack che memorizza &foo.

Esempi

Supponiamo di avere una chiamata foo.refm(), se fooha tipo:

  • X, quindi iniziamo con U = X, refmha il tipo di ricevitore &..., quindi il passaggio 1 non corrisponde, prendendo un auto-ref ci dà &X, e questo corrisponde (con Self = X), quindi la chiamata èRefM::refm(&foo)
  • &X, inizia con U = &X, che corrisponde &selfal primo passaggio (con Self = X), quindi la chiamata èRefM::refm(foo)
  • &&&&&X, questo non corrisponde a nessuno dei due passaggi (il tratto non è implementato per &&&&Xo &&&&&X), quindi dereference una volta per ottenere U = &&&&X, che corrisponde a 1 (con Self = &&&X) e la chiamata èRefM::refm(*foo)
  • Z, non corrisponde a nessuno dei due passaggi, quindi viene assegnato una volta, per ottenere Y, che non corrisponde, quindi viene nuovamente indicato, per ottenere X, che non corrisponde a 1, ma corrisponde dopo l'autorefing, quindi la chiamata è RefM::refm(&**foo).
  • &&A, 1. non corrisponde e nemmeno 2. poiché il tratto non è implementato per &A(per 1) o &&A(per 2), quindi è senza riferimenti &A, che corrisponde a 1., conSelf = A

Supponiamo di avere foo.m(), e Anon lo è Copy, se fooha tipo:

  • A, quindi U = Acorrisponde selfdirettamente in modo che la chiamata sia M::m(foo)conSelf = A
  • &A, quindi 1. non corrisponde, e nemmeno 2. ( &A&&Aimplementa né il tratto), quindi è dereferenziato A, che corrisponde, ma M::m(*foo)richiede di prendere Aper valore e quindi uscire da foo, quindi l'errore.
  • &&A, 1. non corrisponde, ma dà autorefing &&&A, che corrisponde, quindi la chiamata è M::m(&foo)con Self = &&&A.

(Questa risposta si basa sul codice ed è ragionevolmente vicina al README (leggermente obsoleto) . Anche Niko Matsakis, l'autore principale di questa parte del compilatore / linguaggio, ha dato un'occhiata a questa risposta.)


15
Questa risposta sembra esaustiva e dettagliata, ma penso che manchi un riassunto breve e accessibile delle regole. Uno di questi riassunti è dato in questo commento di Shepmaster : "[L'algoritmo deref] deref il più volte possibile ( &&String-> &String-> String-> str) e quindi riferimento al massimo una volta ( str-> &str)".
Lii,

(Non so quanto sia precisa e completa questa spiegazione.)
Lii,

1
In quali casi si verifica la dereferenziazione automatica? Viene utilizzato solo per l'espressione del destinatario per la chiamata del metodo? Anche per gli accessi ai campi? Assegnazione lato destro? Lati di sinistra? Parametri funzionali? Restituisci espressioni di valore?
Lii,

1
Nota: attualmente, il nomicon ha una nota TODO per rubare informazioni da questa risposta e scriverle in static.rust-lang.org/doc/master/nomicon/dot-operator.html
SamB

1
La coercizione (A) è stata provata prima o (B) dopo questa o (C) è stata provata in ogni fase di questo algoritmo o (D) qualcos'altro?
Haslersn,

8

Il riferimento Rust ha un capitolo sull'espressione della chiamata al metodo . Ho copiato la parte più importante di seguito. Promemoria: stiamo parlando di un'espressione recv.m(), dove di seguito recvviene chiamata "espressione del ricevitore".

Il primo passo è creare un elenco di tipi di ricevitori candidati. Ottenere questi dereferenziando ripetutamente il tipo dell'espressione del destinatario, aggiungendo ogni tipo incontrato alla lista, quindi infine tentando una coercizione non dimensionata alla fine e aggiungendo il tipo di risultato se ha esito positivo. Quindi, per ciascun candidato T, aggiungere &Te &mut Tall'elenco immediatamente dopoT .

Ad esempio, se il ricevitore ha tipo Box<[i32;2]>, quindi i tipi candidati saranno Box<[i32;2]>, &Box<[i32;2]>, &mut Box<[i32;2]>, [i32; 2](dereferenziando), &[i32; 2], &mut [i32; 2], [i32](per coercizione non calibrati), &[i32]e infine&mut [i32] .

Quindi, per ciascun tipo di candidato T, cerca un metodo visibile con un ricevitore di quel tipo nei seguenti luoghi:

  1. Tmetodi intrinseci (metodi implementati direttamente su T [¹]).
  2. Uno dei metodi forniti da un tratto visibile implementato da T. [...]

( Nota su [¹] : in realtà penso che questo fraseggio sia sbagliato. Ho aperto un problema . Ignoriamo semplicemente quella frase tra parentesi.)


Analizziamo alcuni esempi del tuo codice in dettaglio! Per i tuoi esempi, possiamo ignorare la parte relativa a "coercizione non dimensionata" e "metodi intrinseci".

(*X{val:42}).m(): il tipo dell'espressione del destinatario è i32. Eseguiamo questi passaggi:

  • Creazione di un elenco di tipi di destinatari candidati:
    • i32 non può essere verificato, quindi abbiamo già terminato il passaggio 1. Elenco: [i32]
    • Quindi, aggiungiamo &i32e &mut i32. Elenco:[i32, &i32, &mut i32]
  • Ricerca di metodi per ciascun tipo di destinatario candidato:
    • Troviamo <i32 as M>::mquale ha il tipo di ricevitore i32. Quindi abbiamo già finito.


Fin qui tutto facile. Ora Prendiamo un esempio più difficili: (&&A).m(). Il tipo dell'espressione del destinatario è &&A. Eseguiamo questi passaggi:

  • Creazione di un elenco di tipi di destinatari candidati:
    • &&Apuò essere fatto riferimento a &A, quindi lo aggiungiamo all'elenco. &Apuò essere nuovamente referenziato, quindi aggiungiamo anche Aall'elenco. Anon possiamo essere dediti, quindi ci fermiamo. Elenco:[&&A, &A, A]
    • Successivamente, per ogni tipo Tnell'elenco, aggiungiamo &Te &mut Tsubito dopo T. Elenco:[&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
  • Ricerca di metodi per ciascun tipo di destinatario candidato:
    • Non esiste un metodo con il tipo di ricevitore &&A , quindi passiamo al tipo successivo nell'elenco.
    • Troviamo il metodo <&&&A as M>::mche ha effettivamente il tipo di ricevitore &&&A. Quindi abbiamo finito.

Ecco gli elenchi dei destinatari candidati per tutti i tuoi esempi. Il tipo che è racchiuso ⟪x⟫è quello che ha "vinto", ovvero il primo tipo per il quale è stato possibile trovare un metodo di adattamento. Ricorda inoltre che il primo tipo nell'elenco è sempre il tipo dell'espressione del destinatario. Infine, ho formattato l'elenco in righe di tre, ma è solo una formattazione: questo elenco è un elenco semplice.

  • (*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]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.