Ich habe das Kapitel "Lebenszeiten" des Rust-Buches gelesen und bin auf dieses Beispiel für eine benannte / explizite Lebenszeit gestoßen:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let x; // -+ x goes into scope
// |
{ // |
let y = &5; // ---+ y goes into scope
let f = Foo { x: y }; // ---+ f goes into scope
x = &f.x; // | | error here
} // ---+ f and y go out of scope
// |
println!("{}", x); // |
} // -+ x goes out of scope
Mir ist ziemlich klar, dass der Fehler, der vom Compiler verhindert wird, die Verwendung der Referenz ist, die zugewiesen wurde x
: nachdem der innere Bereich fertig ist, f
und daher &f.x
ungültig wird und nicht zugewiesen werden sollte x
.
Mein Problem ist, dass das Problem ohne die explizite 'a
Lebensdauer leicht hätte analysiert werden können, indem beispielsweise auf eine illegale Zuordnung eines Verweises zu einem größeren Bereich geschlossen wurde ( x = &f.x;
).
In welchen Fällen werden explizite Lebensdauern tatsächlich benötigt, um Use-After-Free-Fehler (oder Fehler anderer Klassen?) Zu verhindern?
reference
rust
static-analysis
lifetime
Corazza
quelle
quelle
Antworten:
Die anderen Antworten haben alle wichtige Punkte ( fjhs konkretes Beispiel, in dem eine explizite Lebensdauer erforderlich ist ), aber es fehlt eine wichtige Sache: Warum werden explizite Lebensdauern benötigt, wenn der Compiler Ihnen mitteilt, dass Sie sie falsch verstanden haben ?
Dies ist eigentlich die gleiche Frage wie "Warum werden explizite Typen benötigt, wenn der Compiler darauf schließen kann". Ein hypothetisches Beispiel:
Natürlich kann der Compiler sehen, dass ich a zurückgebe.
&'static str
Warum muss der Programmierer es also eingeben?Der Hauptgrund ist, dass der Compiler zwar sehen kann, was Ihr Code tut, aber nicht weiß, was Ihre Absicht war.
Funktionen sind eine natürliche Grenze für die Firewall der Auswirkungen von Codeänderungen. Wenn wir zulassen würden, dass Lebensdauern vollständig anhand des Codes überprüft werden, könnte eine unschuldig aussehende Änderung die Lebensdauern beeinflussen, was dann zu Fehlern in einer weit entfernten Funktion führen könnte. Dies ist kein hypothetisches Beispiel. Soweit ich weiß, hat Haskell dieses Problem, wenn Sie sich für Funktionen der obersten Ebene auf die Typinferenz verlassen. Rust drückte dieses spezielle Problem im Keim.
Der Compiler bietet auch einen Effizienzvorteil: Es müssen nur Funktionssignaturen analysiert werden, um Typen und Lebensdauern zu überprüfen. Noch wichtiger ist, dass dies einen Effizienzvorteil für den Programmierer hat. Wenn wir keine expliziten Lebensdauern hatten, was macht diese Funktion:
Es ist unmöglich zu sagen, ohne die Quelle zu untersuchen, was gegen eine große Anzahl von Best Practices für die Codierung verstoßen würde.
Bereiche sind im Wesentlichen Lebensdauern. Etwas klarer ist, dass eine Lebensdauer
'a
ein generischer Lebensdauerparameter ist , der zur Kompilierungszeit basierend auf der Aufrufsite auf einen bestimmten Bereich spezialisiert werden kann.Überhaupt nicht. Lebensdauern sind erforderlich, um Fehler zu vermeiden, aber explizite Lebensdauern sind erforderlich, um die kleinen vernünftigen Programmierer zu schützen.
quelle
f x = x + 1
ohne Typensignatur , die Sie in einem anderen Modul verwenden. Wenn Sie die Definition später in ändernf x = sqrt $ x + 1
, ändert sich ihr Typ vonNum a => a -> a
bisFloating a => a -> a
, was zu Typfehlern an allen Aufrufstellen führt, an denenf
zInt
. B. mit einem Argument aufgerufen wird . Durch eine Typensignatur wird sichergestellt, dass Fehler lokal auftreten.sqrt $
, nach der Änderung nur ein lokaler Fehler aufgetreten wäre und nicht viele Fehler an anderen Stellen (was viel besser ist, wenn wir es nicht getan hätten Möchten Sie den tatsächlichen Typ nicht ändern?Schauen wir uns das folgende Beispiel an.
Hier sind die expliziten Lebensdauern wichtig. Dies wird kompiliert, da das Ergebnis von
foo
dieselbe Lebensdauer hat wie sein erstes Argument ('a
), sodass es möglicherweise sein zweites Argument überlebt. Dies wird durch die Lebensdauernamen in der Signatur von ausgedrücktfoo
. Wenn Sie die Argumente im Aufruf anfoo
den Compiler umstellen würden, würde sich das beschweren,y
das nicht lange genug lebt:quelle
Die Lebensdaueranmerkung in der folgenden Struktur:
Foo
Gibt an, dass eine Instanz die darin enthaltene Referenz (x
Feld) nicht überleben soll .Das Beispiel , das Sie in Rust Buch stieß auf nicht illustrieren dies nicht , weil
f
undy
Variablen zugleich der Umfang hinausgehen.Ein besseres Beispiel wäre dies:
Jetzt
f
überlebt wirklich die Variable, auf die von gezeigt wirdf.x
.quelle
Beachten Sie, dass dieser Code außer der Strukturdefinition keine expliziten Lebensdauern enthält. Der Compiler ist perfekt in der Lage, Lebensdauern in abzuleiten
main()
.In Typdefinitionen sind jedoch explizite Lebensdauern unvermeidbar. Zum Beispiel gibt es hier eine Mehrdeutigkeit:
Sollten dies unterschiedliche Lebensdauern sein oder sollten sie gleich sein? Es spielt aus Sicht der Nutzung eine Rolle, unterscheidet
struct RefPair<'a, 'b>(&'a u32, &'b u32)
sich stark vonstruct RefPair<'a>(&'a u32, &'a u32)
.Nun, für einfache Fälle, wie die, die Sie zur Verfügung gestellt, der Compiler könnte theoretisch elide Lebensdauern wie es funktioniert in anderen Orten, aber solche Fälle sind sehr begrenzt und nicht wert zusätzliche Komplexität in den Compiler, und dieser Gewinn an Klarheit bei der wäre am wenigsten fraglich.
quelle
'static
,'static
kann überall dort verwendet werden, wo lokale Lebensdauern verwendet werden können. In Ihrem Beispielp
wird daher der Lebensdauerparameter als lokale Lebensdauer von abgeleitety
.RefPair<'a>(&'a u32, &'a u32)
bedeutet, dass'a
dies der Schnittpunkt der beiden Eingangslebensdauern ist, dh in diesem Fall die Lebensdauer vony
.Der Fall aus dem Buch ist von Natur aus sehr einfach. Das Thema Lebenszeiten wird als komplex angesehen.
Der Compiler kann die Lebensdauer einer Funktion mit mehreren Argumenten nicht einfach ableiten.
Außerdem hat meine eigene optionale Kiste einen
OptionBool
Typ mit eineras_slice
Methode, deren Signatur tatsächlich lautet:Der Compiler hätte das auf keinen Fall herausfinden können.
quelle
Ich habe hier eine weitere gute Erklärung gefunden: http://doc.rust-lang.org/0.12.0/guide-lifetimes.html#returning-references .
quelle
Wenn eine Funktion zwei Referenzen als Argumente empfängt und eine Referenz zurückgibt, gibt die Implementierung der Funktion manchmal die erste und manchmal die zweite Referenz zurück. Es ist unmöglich vorherzusagen, welche Referenz für einen bestimmten Anruf zurückgegeben wird. In diesem Fall ist es unmöglich, eine Lebensdauer für die zurückgegebene Referenz abzuleiten, da jede Argumentreferenz auf eine andere Variablenbindung mit einer anderen Lebensdauer verweisen kann. Explizite Lebensdauern helfen, eine solche Situation zu vermeiden oder zu klären.
Wenn eine Struktur zwei Referenzen enthält (als zwei Mitgliedsfelder), kann eine Mitgliedsfunktion der Struktur manchmal die erste Referenz und manchmal die zweite zurückgeben. Wiederum verhindern explizite Lebensdauern solche Unklarheiten.
In einigen einfachen Situationen gibt es eine Lebenszeitentscheidung, bei der der Compiler auf Lebensdauern schließen kann.
quelle
Der Grund, warum Ihr Beispiel nicht funktioniert, liegt einfach darin, dass Rust nur eine lokale Lebensdauer und Typinferenz hat. Was Sie vorschlagen, erfordert globale Folgerung. Wenn Sie eine Referenz haben, deren Lebensdauer nicht geändert werden kann, muss sie mit Anmerkungen versehen werden.
quelle
Als Neuling bei Rust verstehe ich, dass explizite Lebenszeiten zwei Zwecken dienen.
Durch das Hinzufügen einer expliziten lebenslangen Annotation zu einer Funktion wird der Codetyp eingeschränkt, der möglicherweise in dieser Funktion angezeigt wird. Durch explizite Lebensdauern kann der Compiler sicherstellen, dass Ihr Programm das tut, was Sie beabsichtigt haben.
Wenn Sie (der Compiler) überprüfen möchten, ob ein Code gültig ist, müssen Sie (der Compiler) nicht iterativ in jede aufgerufene Funktion schauen. Es reicht aus, einen Blick auf die Anmerkungen von Funktionen zu werfen, die direkt von diesem Code aufgerufen werden. Dies erleichtert es Ihnen (dem Compiler), über Ihr Programm nachzudenken, und macht die Kompilierungszeiten überschaubar.
Betrachten Sie unter Punkt 1 das folgende in Python geschriebene Programm:
welches drucken wird
Diese Art von Verhalten überrascht mich immer wieder. Was passiert, ist, dass
df
das Gedächtnis mit geteiltar
wird. Wenn also ein Teil des Inhalts vondf
Änderungen inwork
, ändert sich diese Änderungar
auch. In einigen Fällen kann dies jedoch aus Gründen der Speichereffizienz genau das sein, was Sie möchten (keine Kopie). Das eigentliche Problem in diesem Code ist, dass die Funktionsecond_row
die erste Zeile anstelle der zweiten zurückgibt. Viel Glück beim Debuggen.Betrachten Sie stattdessen ein ähnliches Programm, das in Rust geschrieben wurde:
Wenn Sie dies zusammenstellen, erhalten Sie
In der Tat erhalten Sie zwei Fehler, es gibt auch einen mit den Rollen von
'a
und'b
vertauscht. Wennsecond_row
wir uns die Annotation von ansehen , stellen wir fest, dass die Ausgabe sein sollte&mut &'b mut [i32]
, dh die Ausgabe soll eine Referenz auf eine Referenz mit Lebensdauer sein'b
(die Lebensdauer der zweiten Zeile vonArray
). Da wir jedoch die erste Zeile zurückgeben (die eine Lebensdauer hat'a
), beschwert sich der Compiler über eine Nichtübereinstimmung der Lebensdauer. Am richtigen Ort. Zur richtigen Zeit. Das Debuggen ist ein Kinderspiel.quelle
Ich stelle mir eine lebenslange Anmerkung als einen Vertrag über eine bestimmte Referenz vor, der nur im Empfangsbereich gültig war, während er im Quellbereich gültig bleibt. Wenn Sie mehr Referenzen in derselben Lebenszeit deklarieren, werden die Bereiche zusammengeführt, was bedeutet, dass alle Quellenreferenzen diesen Vertrag erfüllen müssen. Durch diese Anmerkung kann der Compiler die Vertragserfüllung überprüfen.
quelle