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
- Versuchen Sie zunächst, eine einzige Dereferenzierung von zu fordern
- Methoden, die mit
self
(call-by-value) für den TypT
deklariert 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
Deref
Merkmal verwendet.
Was sind die genauen Regeln für die automatische Dereferenzierung? Kann jemand eine formale Begründung für eine solche Entwurfsentscheidung geben?
reference
dereference
formal-semantics
rust
kFYatek
quelle
quelle
Antworten:
Ihr Pseudocode ist ziemlich korrekt. Nehmen wir für dieses Beispiel an, wir hätten einen Methodenaufruf,
foo.bar()
bei demfoo: T
. Ich werde die vollqualifizierte Syntax (FQS) verwenden, um eindeutig zu bestimmen, mit welchem Typ die Methode aufgerufen wird, z . B.A::bar(foo)
oderA::bar(&***foo)
. Ich werde nur einen Stapel zufälliger Großbuchstaben schreiben, jeder ist nur ein beliebiger Typ / eine beliebige Eigenschaft, außer esT
ist immer der Typ der ursprünglichen Variablenfoo
, auf die sich die Methode bezieht.Der Kern des Algorithmus ist:
U
( dh setzenU = T
und dannU = *T
...)bar
bei der der Empfängertyp (der Typself
in der Methode)U
genau übereinstimmt , verwenden Sie sie ( eine "By-Value-Methode"). ).&
oder&mut
des 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
Self
Typ des Merkmals, dhimpl ... for Foo { fn method(&self) {} }
denkt&Foo
beim Abgleichen der Methode darüber nach undfn method2(&mut self)
würde&mut Foo
beim 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
Deref
Implementierungen 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
&foo
bleibt eine starke Verbindung zufoo
(es ist die Adresse von sichfoo
selbst), aber wenn Sie mehr nehmen, geht diese verloren: Dies&&foo
ist die Adresse einer temporären Variablen auf dem Stapel, die gespeichert wird&foo
.Beispiele
Angenommen, wir haben einen Anruf
foo.refm()
, wennfoo
der Typ:X
, dann fangen wir an mitU = X
,refm
hat Empfängertyp&...
, also stimmt Schritt 1 nicht überein, eine automatische Referenz gibt uns&X
, und dies stimmt (mitSelf = X
) überein , also ist der AnrufRefM::refm(&foo)
&X
, beginnt mitU = &X
, was&self
im ersten Schritt (mitSelf = X
) übereinstimmt , und so ist der AufrufRefM::refm(foo)
&&&&&X
Dies entspricht keinem Schritt (das Merkmal ist nicht für&&&&X
oder implementiert&&&&&X
), daher wird dereferenziert, um zu erhaltenU = &&&&X
, welches mit 1 (mitSelf = &&&X
) übereinstimmt und der Aufruf lautetRefM::refm(*foo)
Z
, stimmt mit keinem der beiden SchritteY
überein, so dass es einmal dereferenziert wird, um zu erhalten , was auch nicht übereinstimmt, also wird es erneut dereferenziert, um zu erhaltenX
, 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 dasA
ist nichtCopy
, wennfoo
der Typ:A
,U = A
stimmt dannself
direkt überein, so dass der AnrufM::m(foo)
mit istSelf = A
&A
, dann stimmt 1. nicht überein und auch nicht 2. (weder&A
noch&&A
implementiert das Merkmal), so dass es dereferenziert wirdA
, was übereinstimmt, aberM::m(*foo)
erfordert,A
nach Wert zu nehmen und sich daher zu entfernenfoo
, daher der Fehler.&&A
, 1. stimmt nicht überein, aber Autorefing gibt&&&A
, was passt, also ist der AnrufM::m(&foo)
mitSelf = &&&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.)
quelle
&&String
(>&String
->String
->str
) und dann maximal einmal referenzieren (str
->&str
)".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()
, derrecv
unten als "Empfängerausdruck" bezeichnet wird.( 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 isti32
. Wir führen diese Schritte aus:i32
kann nicht dereferenziert werden, daher sind wir bereits mit Schritt 1 fertig. Liste:[i32]
&i32
und hinzu&mut i32
. Aufführen:[i32, &i32, &mut i32]
<i32 as M>::m
welche den Empfängertyp hati32
. 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:&&A
kann dereferenziert werden&A
, also fügen wir das der Liste hinzu.&A
kann wieder dereferenziert werden, daher fügen wir auchA
der Liste hinzu.A
kann nicht dereferenziert werden, also hören wir auf. Aufführen:[&&A, &A, A]
T
fügen wir für jeden Typ in der Liste&T
und&mut T
unmittelbar danach hinzuT
. Aufführen:[&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
&&A
, daher gehen wir zum nächsten Typ in der Liste.<&&&A as M>::m
die 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
X{val:42}.m()
→<X as M>::m
(&X{val:42}).m()
→<&X as M>::m
(&&X{val:42}).m()
→<&&X as M>::m
(&&&X{val:42}).m()
→<&&&X as M>::m
(&&&&X{val:42}).m()
→<&&&X as M>::m
(&&&&&X{val:42}).m()
→<&&&X as M>::m
(*X{val:42}).refm()
→<i32 as RefM>::refm
X{val:42}.refm()
→<X as RefM>::refm
(&X{val:42}).refm()
→<X as RefM>::refm
(&&X{val:42}).refm()
→<&X as RefM>::refm
(&&&X{val:42}).refm()
→<&&X as RefM>::refm
(&&&&X{val:42}).refm()
→<&&&X as RefM>::refm
(&&&&&X{val:42}).refm()
→<&&&X as RefM>::refm
Y{val:42}.refm()
→<i32 as RefM>::refm
Z{val:Y{val:42}}.refm()
→<i32 as RefM>::refm
A.m()
→<A as M>::m
(&A).m()
→<A as M>::m
(&&A).m()
→<&&&A as M>::m
(&&&A).m()
→<&&&A as M>::m
A.refm()
→<A as RefM>::refm
(&A).refm()
→<A as RefM>::refm
(&&A).refm()
→<A as RefM>::refm
(&&&A).refm()
→<&&&A as RefM>::refm
quelle