Was sind die genauen Regeln für die automatische Dereferenzierung von Rust?

180

Ich lerne / experimentiere mit Rust und in all der Eleganz, die ich in dieser Sprache finde, gibt es eine Besonderheit, die mich verblüfft und völlig fehl am Platz zu sein scheint.

Rust dereferenziert Zeiger automatisch, wenn Methodenaufrufe ausgeführt werden. Ich habe einige Tests durchgeführt, um das genaue Verhalten zu bestimmen:

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

( Spielplatz )

Es scheint also mehr oder weniger:

  • Der Compiler fügt so viele Dereferenzierungsoperatoren ein, wie zum Aufrufen einer Methode erforderlich sind.
  • Der Compiler beim Auflösen von Methoden, die mit &self(Call-by-Reference) deklariert wurden :
    • Versuchen Sie zunächst, eine einzige Dereferenzierung von zu fordern self
    • Dann wird versucht, den genauen Typ von aufzurufen self
    • Versuchen Sie dann, so viele Dereferenzierungsoperatoren einzufügen, wie für eine Übereinstimmung erforderlich sind
  • Methoden, die mit self(call-by-value) für den Typ Tdeklariert wurden, verhalten sich so, als ob sie mit &self(call-by-reference) für den Typ deklariert worden wären&T und als Referenz auf das aufgerufen würden, was sich auf der linken Seite des Punktoperators befindet.
  • Die oben genannten Regeln werden zuerst mit der eingebauten Roh-Dereferenzierung ausprobiert. Wenn keine Übereinstimmung vorliegt, wird die Überladung mit dem DerefMerkmal verwendet.

Was sind die genauen Regeln für die automatische Dereferenzierung? Kann jemand eine formale Begründung für eine solche Entwurfsentscheidung geben?

kFYatek
quelle
Ich habe dies in der Hoffnung, einige gute Antworten zu bekommen, in den Rust-Subreddit gepostet !
Shepmaster
Versuchen Sie für zusätzlichen Spaß, das Experiment in Generika zu wiederholen und die Ergebnisse zu vergleichen.
user2665887

Antworten:

137

Ihr Pseudocode ist ziemlich korrekt. Nehmen wir für dieses Beispiel an, wir hätten einen Methodenaufruf, foo.bar()bei dem foo: T. Ich werde die vollqualifizierte Syntax (FQS) verwenden, um eindeutig zu bestimmen, mit welchem ​​Typ die Methode aufgerufen wird, z . B. A::bar(foo)oder A::bar(&***foo). Ich werde nur einen Stapel zufälliger Großbuchstaben schreiben, jeder ist nur ein beliebiger Typ / eine beliebige Eigenschaft, außer es Tist immer der Typ der ursprünglichen Variablenfoo , auf die sich die Methode bezieht.

Der Kern des Algorithmus ist:

  • Für jeden "Dereferenzierungsschritt" U ( dh setzen U = Tund dannU = *T ...)
    1. Wenn es eine Methode gibt, barbei der der Empfängertyp (der Typ selfin der Methode) Ugenau übereinstimmt , verwenden Sie sie ( eine "By-Value-Methode"). ).
    2. Andernfalls fügen Sie eine automatische Referenz hinzu (take &oder &mutdes Empfängers), und wenn der Empfänger einer Methode übereinstimmt &U, verwenden Sie diese ( eine "autorefd-Methode" ).

Insbesondere berücksichtigt alles den "Empfängertyp" der Methode, nicht den SelfTyp des Merkmals, dh impl ... for Foo { fn method(&self) {} }denkt &Foobeim Abgleichen der Methode darüber nach und fn method2(&mut self)würde &mut Foobeim Abgleichen darüber nachdenken .

Es ist ein Fehler, wenn in den inneren Schritten jemals mehrere Merkmalsmethoden gültig sind (das heißt, es kann nur null oder eine Merkmalsmethode in jeder von 1. oder 2. geben, aber es kann jeweils eine gültige geben: die eine von 1 wird zuerst genommen), und inhärente Methoden haben Vorrang vor Merkmalen. Es ist auch ein Fehler, wenn wir das Ende der Schleife erreichen, ohne etwas Passendes zu finden. Es ist auch ein Fehler, rekursive DerefImplementierungen zu haben , die die Schleife unendlich machen (sie werden das "Rekursionslimit" erreichen).

Diese Regeln scheinen in den meisten Fällen das zu tun, was ich meine, obwohl die Fähigkeit, das eindeutige FQS-Formular zu schreiben, in einigen Randfällen und für sinnvolle Fehlermeldungen für makrogenerierten Code sehr nützlich ist.

Da wird nur eine automatische Referenz hinzugefügt

  • Wenn es keine Bindung gab, werden die Dinge schlecht / langsam, da für jeden Typ eine beliebige Anzahl von Referenzen verwendet werden kann
  • Wenn Sie eine Referenz nehmen, &foobleibt eine starke Verbindung zu foo(es ist die Adresse von sich fooselbst), aber wenn Sie mehr nehmen, geht diese verloren: Dies &&fooist die Adresse einer temporären Variablen auf dem Stapel, die gespeichert wird &foo.

Beispiele

Angenommen, wir haben einen Anruf foo.refm(), wenn fooder Typ:

  • X, dann fangen wir an mit U = X, refmhat Empfängertyp &..., also stimmt Schritt 1 nicht überein, eine automatische Referenz gibt uns &X, und dies stimmt (mit Self = X) überein , also ist der AnrufRefM::refm(&foo)
  • &X, beginnt mit U = &X, was &selfim ersten Schritt (mit Self = X) übereinstimmt , und so ist der AufrufRefM::refm(foo)
  • &&&&&XDies entspricht keinem Schritt (das Merkmal ist nicht für &&&&Xoder implementiert &&&&&X), daher wird dereferenziert, um zu erhalten U = &&&&X, welches mit 1 (mit Self = &&&X) übereinstimmt und der Aufruf lautetRefM::refm(*foo)
  • Z, stimmt mit keinem der beiden Schritte Yüberein, so dass es einmal dereferenziert wird, um zu erhalten , was auch nicht übereinstimmt, also wird es erneut dereferenziert, um zu erhalten X, was nicht mit 1 übereinstimmt, aber nach dem automatischen Referenzieren übereinstimmt, also ist der AufrufRefM::refm(&**foo) .
  • &&A, die 1. stimmt nicht überein und auch nicht 2. da das Merkmal nicht für &A(für 1) oder &&A(für 2) implementiert ist , wird es dereferenziert auf &A, was mit 1. übereinstimmt, mitSelf = A

Nehmen wir an foo.m(), wir haben und das Aist nicht Copy, wenn fooder Typ:

  • A, U = Astimmt dann selfdirekt überein, so dass der Anruf M::m(foo)mit istSelf = A
  • &A, dann stimmt 1. nicht überein und auch nicht 2. (weder &Anoch &&Aimplementiert das Merkmal), so dass es dereferenziert wird A, was übereinstimmt, aber M::m(*foo)erfordert, Anach Wert zu nehmen und sich daher zu entfernen foo, daher der Fehler.
  • &&A, 1. stimmt nicht überein, aber Autorefing gibt &&&A, was passt, also ist der Anruf M::m(&foo)mit Self = &&&A.

(Diese Antwort basiert auf dem Code und kommt der (leicht veralteten) README- Datei ziemlich nahe . Niko Matsakis, der Hauptautor dieses Teils des Compilers / der Sprache, warf ebenfalls einen Blick auf diese Antwort.)

huon
quelle
15
Diese Antwort scheint erschöpfend und detailliert zu sein, aber ich denke, es fehlt eine kurze und leicht zugängliche Zusammenfassung der Regeln. Eine solche Zusammenfassung wird in diesem Kommentar von Shepmaster gegeben : "Es [der Deref-Algorithmus] wird so oft wie möglich deref ( &&String(> &String-> String-> str) und dann maximal einmal referenzieren ( str-> &str)".
Lii
(Ich weiß nicht, wie genau und vollständig diese Erklärung für mich ist.)
Lii
1
In welchen Fällen tritt eine automatische Dereferenzierung auf? Wird es nur für den Empfängerausdruck für den Methodenaufruf verwendet? Für Feldzugriffe auch? Aufgabe rechts? Linke Seite? Funktionsparameter? Wertausdrücke zurückgeben?
Lii
1
Hinweis: Derzeit hat das Nomicon eine TODO-Notiz, um Informationen aus dieser Antwort zu stehlen und sie in static.rust-lang.org/doc/master/nomicon/dot-operator.html
SamB
1
Wird (A) zuvor oder (B) danach oder (C) in jedem Schritt dieses Algorithmus oder (D) etwas anderes versucht?
Haslersn
7

Die Rust-Referenz enthält ein Kapitel über den Ausdruck des Methodenaufrufs . Ich habe den wichtigsten Teil unten kopiert. Erinnerung: Wir sprechen von einem Ausdruck recv.m(), der recvunten als "Empfängerausdruck" bezeichnet wird.

Der erste Schritt besteht darin, eine Liste der Kandidatenempfängertypen zu erstellen. Erhalten Sie diese, indem Sie den Typ des Empfängerausdrucks wiederholt dereferenzieren, jeden angetroffenen Typ zur Liste hinzufügen, am Ende einen nicht erzwungenen Zwang versuchen und den Ergebnistyp hinzufügen, wenn dies erfolgreich ist. Dann wird für jeden Kandidaten T, fügen &Tund &mut Tin die Liste sofort nach T.

Wenn der Empfängertyp Zum Beispiel hat Box<[i32;2]>, dann werden die Kandidatentypen sein Box<[i32;2]>, &Box<[i32;2]>, &mut Box<[i32;2]>, [i32; 2](durch dereferencing) &[i32; 2], &mut [i32; 2], [i32](durch unsized Zwang), &[i32]und schließlich&mut [i32] .

TSuchen Sie dann für jeden Kandidatentyp an folgenden Stellen nach einer sichtbaren Methode mit einem Empfänger dieses Typs:

  1. Tinhärente Methoden (Methoden, die direkt auf implementiert sind T [¹] ).
  2. Jede der Methoden, die durch ein sichtbares Merkmal bereitgestellt werden, implementiert von T. [...]

( Anmerkung zu [¹] : Ich denke tatsächlich, dass diese Formulierung falsch ist. Ich habe ein Problem eröffnet . Lassen Sie uns diesen Satz in der Klammer einfach ignorieren.)


Lassen Sie uns einige Beispiele aus Ihrem Code im Detail durchgehen! Für Ihre Beispiele können wir den Teil über "nicht erzwungenen Zwang" und "inhärente Methoden" ignorieren.

(*X{val:42}).m(): Der Typ des Empfängerausdrucks ist i32. Wir führen diese Schritte aus:

  • Liste der Empfängerempfängertypen erstellen:
    • i32 kann nicht dereferenziert werden, daher sind wir bereits mit Schritt 1 fertig. Liste: [i32]
    • Als nächstes fügen wir &i32und hinzu &mut i32. Aufführen:[i32, &i32, &mut i32]
  • Suche nach Methoden für jeden Kandidatenempfängertyp:
    • Wir finden, <i32 as M>::mwelche den Empfängertyp hat i32. Wir sind also schon fertig.


So weit so einfach. Wählen wir nun ein schwierigeres Beispiel : (&&A).m(). Der Typ des Empfängerausdrucks ist &&A. Wir führen diese Schritte aus:

  • Liste der Empfängerempfängertypen erstellen:
    • &&Akann dereferenziert werden &A, also fügen wir das der Liste hinzu. &Akann wieder dereferenziert werden, daher fügen wir auch Ader Liste hinzu. Akann nicht dereferenziert werden, also hören wir auf. Aufführen:[&&A, &A, A]
    • Als nächstes Tfügen wir für jeden Typ in der Liste &Tund &mut Tunmittelbar danach hinzu T. Aufführen:[&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
  • Suche nach Methoden für jeden Kandidatenempfängertyp:
    • Es gibt keine Methode mit Empfängertyp &&A, daher gehen wir zum nächsten Typ in der Liste.
    • Wir finden die Methode, <&&&A as M>::mdie tatsächlich den Empfängertyp hat &&&A. Also sind wir fertig.

Hier sind die Kandidatenempfängerlisten für alle Ihre Beispiele. Der Typ, der eingeschlossen ⟪x⟫ist, ist derjenige, der "gewonnen" hat, dh der erste Typ, für den eine Anpassungsmethode gefunden werden konnte. Denken Sie auch daran, dass der erste Typ in der Liste immer der Typ des Empfängerausdrucks ist. Zuletzt habe ich die Liste in Dreierzeilen formatiert, aber das ist nur Formatierung: Diese Liste ist eine flache Liste.

  • (*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
quelle