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




dereference formal-semantics (2)

Il tuo pseudo-codice è praticamente corretto. Per questo esempio, supponiamo di avere un metodo chiamato foo.bar() dove foo: T Userò la sintassi completa (FQS) per essere inequivocabile sul tipo con cui viene chiamato il metodo, ad esempio A::bar(foo) o A::bar(&***foo) . Scriverò solo una pila di lettere maiuscole casuali, ognuna è solo un tipo / tratto arbitrario, eccetto che T è sempre il tipo della variabile originale foo cui il metodo è chiamato.

Il nucleo dell'algoritmo è:

  • Per ogni "passaggio di dereferenza" U (ovvero, imposta U = T e quindi U = *T , ...)
    1. se c'è una bar metodi in cui il tipo di ricevitore (il tipo di self nel metodo) corrisponde esattamente a U , usalo ( un "metodo per valore" )
    2. altrimenti, aggiungi un auto ref (prendi & o &mut del ricevitore), e, se il ricevitore di un metodo corrisponde a &U , usalo ( un "metodo autorefd" )

In particolare, tutto considera il "tipo di ricevitore" del metodo, non il tipo Self del tratto, cioè impl ... for Foo { fn method(&self) {} } pensa a &Foo quando combacia con il metodo, e fn method2(&mut self) penserebbe a &mut Foo durante l'abbinamento.

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

Queste regole sembrano fare-cosa-voglio-dire nella maggior parte delle circostanze, anche se avere la capacità di scrivere il modulo FQS non ambiguo è molto utile in alcuni casi limite e per i messaggi di errore sensibili per il codice generato dalla macro.

Viene aggiunto solo un riferimento automatico perché

  • se non ci fosse limite, le cose si mettono male / lentamente, poiché ogni tipo può avere un numero arbitrario di riferimenti presi
  • prendendo un riferimento &foo mantiene una forte connessione con foo (è l'indirizzo di foo stesso), ma prendendo più inizia a perderlo: &&foo è l'indirizzo di qualche variabile temporanea sullo stack che memorizza &foo .

Esempi

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

  • X , quindi iniziamo con U = X , refm ha tipo di ricevitore &... , quindi il passo 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 a &self nel primo passo (con Self = X ), e quindi la chiamata è RefM::refm(foo)
  • &&&&&X , questo non corrisponde ad alcun passo (il tratto non è implementato per &&&&X o &&&&&X ), quindi dereferiamo 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 passaggi, quindi è dereferenziato una volta, per ottenere Y , che non corrisponde, quindi è nuovamente dereferenziato, per ottenere X , che non corrisponde a 1, ma corrisponde dopo l'autorefing, quindi la chiamata è RefM::refm(&**foo) .
  • &&A , il 1. non corrisponde e nemmeno 2. poiché il tratto non è implementato per &A (per 1) o &&A (per 2), quindi è dereferenziato a &A , che corrisponde a 1., con Self = A

Supponiamo di avere foo.m() e che A non sia Copy , se foo ha tipo:

  • A , quindi U = A corrisponde direttamente a self modo che la chiamata sia M::m(foo) con Self = A
  • &A , quindi 1. non corrisponde, e nemmeno 2. (né &A&&A implementano il tratto), quindi è dereferenziato ad A , che corrisponde, ma M::m(*foo) richiede di prendere A base al valore e quindi uscire da foo , da qui l'errore.
  • &&A , 1. non corrisponde, ma l'autorefing dà &&&A , che corrisponde, quindi la chiamata è M::m(&foo) con Self = &&&A

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

Sto imparando / sperimentando con Rust, e in tutta l'eleganza che trovo in questa lingua, c'è una particolarità che mi confonde e sembra totalmente fuori luogo.

Ruggine deseleziona automaticamente i puntatori durante le 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<'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 == @
}

Quindi, sembra che, più o meno:

  • Il compilatore inserirà tutti gli operatori di dereferenze necessari per richiamare un metodo.
  • Il compilatore, quando risolve i metodi dichiarati usando &self (call-by-reference):
    • Primi tentativi di chiedere un singolo dereferenziamento di self
    • Quindi cerca di chiamare il tipo esatto di self
    • Quindi, prova a inserire tutti gli operatori di dereferenziamento necessari per una corrispondenza
  • I metodi dichiarati usando self (call-by-value) per il tipo T comportano come se fossero stati dichiarati usando &self (call-by-reference) per type &T e chiamati sul riferimento a qualunque cosa si trovi sul lato sinistro dell'operatore punto.
  • Le regole di cui sopra vengono prima provate con il dereferenziamento integrato grezzo, e se non c'è corrispondenza, viene utilizzato il sovraccarico con tratto Deref .

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


In parole semplici, il dereferenziamento significa accedere al valore da una certa posizione di memoria contro cui punta quel puntatore.







reference dereference formal-semantics rust