Hinweis Übersetzer: Der Datensatz ist vom 13. Mai 2014 datiert, sodass einige Details, einschließlich des Quellcodes, möglicherweise nicht dem aktuellen Stand der Dinge entsprechen. Die Antwort auf die Frage, warum die Übersetzung eines so langjährigen Beitrags erforderlich ist, wird der Wert seines Inhalts sein, um ein Verständnis für eines der grundlegenden Konzepte der Rust-Sprache wie die Sprachkompetenz zu entwickeln.
Mit der Zeit wurde ich überzeugt, dass es besser wäre, die Unterscheidung zwischen veränderlichen und unveränderlichen lokalen Variablen in Rust aufzugeben. Zumindest viele Menschen stehen diesem Thema skeptisch gegenüber. Ich wollte meine Position öffentlich darlegen. Ich werde verschiedene Motive nennen: philosophisch, technisch und praktisch, sowie mich der Hauptverteidigung des gegenwärtigen Systems zuwenden. (Hinweis: Ich habe dies als Rust RFC angesehen, aber entschieden, dass der Ton für einen Blog-Beitrag besser ist, und ich habe jetzt keine Zeit, ihn neu zu schreiben.)
Erklärung
Ich habe diesen Artikel ziemlich entschlossen geschrieben und glaube, dass die Linie, die ich verteidige, korrekt sein wird. Wenn wir das aktuelle System jedoch nicht vollständig unterstützen, ist dies keine Katastrophe oder ähnliches. Es hat seine Vorteile und insgesamt finde ich es ziemlich angenehm. Ich denke nur, wir können es verbessern.
In einem Wort
Ich möchte die Unterscheidung zwischen unveränderlichen und veränderlichen lokalen Variablen aufheben und &mut
Zeiger auf &my
, &only
oder &uniq
(das &uniq
für mich keinen Unterschied). Wenn es nur kein Schlüsselwort mut
gäbe.
Philosophisches Motiv
Der Hauptgrund, warum ich dies tun möchte, ist, dass ich glaube, dass dies die Sprache konsistenter und verständlicher macht. Dies wird uns im Wesentlichen von der Diskussion über Veränderlichkeit zur Verwendung von Aliasnamen (die ich als "Teilen" bezeichnen werde, siehe unten) neu ausrichten.
Variabilität wird zu einer Folge der Einzigartigkeit: „Sie können jederzeit alles ändern, auf das Sie einen eindeutigen Zugriff haben. Freigegebene Daten sind normalerweise unveränderlich. Bei Bedarf können Sie sie jedoch mithilfe eines Zelltyps ändern.“
Mit anderen Worten, im Laufe der Zeit wurde mir klar, dass Probleme mit Datenrennen und Speichersicherheit auftreten, wenn Sie sowohl Aliase als auch Veränderlichkeit verwenden. Ein funktionaler Ansatz zur Lösung dieses Problems besteht darin, die Veränderlichkeit zu beseitigen. Rusts Ansatz wäre es, die Verwendung von Aliasen zu entfernen. Dies gibt uns eine Geschichte, die erzählt werden kann und die uns hilft, sie herauszufinden.
Ein Hinweis zur Terminologie: Ich denke, wir sollten die Verwendung von Aliasen als Trennung bezeichnen ( Anmerkung des Übersetzers: Im Folgenden wird überall anstelle von "Aliasing" "Teilen" im Sinne von "Trennung" oder "gemeinsames Eigentum" verwendet, da weder "Verwendung von Aliasen", " Weder "Pseudonymisierung" gibt ein Verständnis dafür, worum es geht . In der Vergangenheit haben wir dies aufgrund seiner Multithread-Referenzen vermieden. Wenn wir jedoch die von mir vorgeschlagenen Datenparallelisierungspläne implementieren, ist diese Konnotation nicht völlig unangemessen. Angesichts der engen Beziehung zwischen Speichersicherheit und Datenrennsport möchte ich diese Konnotation wirklich fördern.
Bildungsmotiv
Ich denke, die aktuellen Regeln sind schwieriger zu verstehen, als sie sein sollten. Es ist zum Beispiel nicht offensichtlich, dass &mut T
keine gemeinsame Eigentümerschaft impliziert. Darüber hinaus impliziert die Bezeichnung &mut T
, dass &T
keine Mutabilität impliziert, die aufgrund von Typen wie Cell
nicht ganz genau ist. Und es ist unmöglich, sich darauf zu einigen, wie man sie nennt ("veränderbare / unveränderliche Links" sind am häufigsten, aber das ist nicht ganz richtig).
Im Gegensatz dazu scheint ein Typ wie &my T
oder &only T
die Erklärung zu vereinfachen. Dies ist eine eindeutige Verbindung - natürlich können Sie nicht zwei von ihnen zwingen, auf dieselbe Stelle zu zeigen. Und Veränderlichkeit ist eine orthogonale Sache: Sie beruht auf Einzigartigkeit, gilt aber auch für Zellen. Und der &T
Typ ist genau das Gegenteil, ein gemeinsamer Link . RFC PR # 58 bietet eine Reihe ähnlicher Argumente. Ich werde sie hier nicht wiederholen.
Praktisches Motiv
Derzeit besteht eine Lücke zwischen geliehenen Zeigern, die entweder gemeinsam genutzt oder veränderbar + eindeutig sein können, und lokalen Variablen, die immer eindeutig, aber veränderlich oder unveränderlich sein können. Das Endergebnis davon ist, dass Benutzer mut
Anzeigen zu Dingen mut
sollten, die nicht direkt bearbeitet werden können.
Lokale Variablen können nicht mithilfe von Referenzen modelliert werden
Dieses Phänomen tritt auf, weil Verknüpfungen nicht so aussagekräftig sind wie lokale Variablen. Dies verhindert im Allgemeinen die Abstraktion. Lassen Sie mich einige Beispiele geben, um zu erklären, was ich meine. Stellen Sie sich vor, ich habe eine Umgebungsstruktur, in der ein Zeiger auf einen Fehlerzähler gespeichert ist:
struct Env { errors: &mut usize }
Jetzt kann ich Instanzen dieser Struktur erstellen (und verwenden):
let mut errors = 0; let env = Env { errors: &mut errors }; ... if some_condition { *env.errors += 1; }
OK, stellen Sie sich jetzt vor, ich möchte den Code, der env.errors
ändert, in eine separate Funktion env.errors
. Ich könnte denken, da die env
Variable nicht als veränderbar deklariert ist, kann ich den unveränderlichen &
link verwenden:
let mut errors = 0; let env = Env { errors: &mut errors }; helper(&env); fn helper(env: &Env) { ... if some_condition { *env.errors += 1;
Aber das ist nicht so. Das Problem ist, dass &Env
ein Typ mit gemeinsamem Besitz ist ( Anmerkung des Übersetzers: Wie Sie wissen, kann mehr als eine unveränderliche Objektreferenz gleichzeitig vorhanden sein ), und daher erscheint env.errors
in einem Bereich, der einen separaten Besitz des env
Objekts ermöglicht. Damit dieser Code funktioniert, muss ich env
als veränderbar deklarieren und den &mut
Link ( Anmerkung des Übersetzers: &mut
) verwenden, um dem Compiler mitzuteilen, dass env
eindeutig im Besitz ist, da jeweils nur eine veränderbare Objektreferenz vorhanden sein kann und Datenrennen ausgeschlossen sind. aber mut
weil Sie keinen veränderlichen Verweis auf ein unveränderliches Objekt erstellen können ):
let mut errors = 0; let mut env = Env { errors: &mut errors }; helper(&mut env);
Dieses Problem tritt auf, weil wir wissen, dass lokale Variablen eindeutig sind, aber wir können dieses Wissen nicht in eine geliehene Referenz einfügen, ohne es veränderbar zu machen.
Dieses Problem tritt an einer Reihe anderer Stellen auf. Bisher haben wir auf unterschiedliche Weise darüber geschrieben, aber ich bin weiterhin von dem Gefühl verfolgt, dass es sich um eine Pause handelt, die einfach nicht sein sollte.
Typprüfung auf Verschlüsse
Wir mussten diese Einschränkung bei Schließungen umgehen. Verschlüsse sind in Strukturen wie Env
meist offen, aber nicht ganz. Dies liegt daran, dass ich nicht verlangen möchte, dass lokale Variablen mut
deklariert werden, wenn sie über &mut
in einem Abschluss verwendet werden. Mit anderen Worten, nehmen Sie einen Code, zum Beispiel:
fn foo(errors: &mut usize) { do_something(|| *errors += 1) }
Ein Ausdruck, der den Abschluss beschreibt, erstellt tatsächlich eine Instanz der Env
Struktur:
struct ClosureEnv<'a, 'b> { errors: &uniq &mut usize }
Überprüfen Sie den &uniq
Link. Dies kann der Endbenutzer nicht eingeben. Es bedeutet einen "eindeutigen, aber nicht unbedingt veränderlichen" Zeiger. Dies ist erforderlich, um die Typprüfung zu bestehen. Wenn der Benutzer versuchen würde, diese Struktur manuell zu schreiben, müsste er &mut &mut usize
schreiben, was wiederum erfordern würde, dass der mut errors: &mut usize
als mut errors: &mut usize
deklariert wird mut errors: &mut usize
.
Ausgepackte Verschlüsse und Verfahren
Ich gehe davon aus, dass diese Einschränkung ein Problem für ausgepackte Verschlüsse ist. Lassen Sie mich auf das Design eingehen, über das ich nachgedacht habe. Grundsätzlich war die Idee, dass der Ausdruck ||
entspricht einem neuen Strukturtyp, der eines der Merkmale Fn
implementiert:
trait Fn<A, R> { fn call(&self, ...); } trait FnMut<A, R> { fn call(&mut self, ...); } trait FnOnce<A, R> { fn call(self, ...); }
Der genaue Typ wird ab heute entsprechend dem erwarteten Typ ausgewählt. In diesem Fall können Verbraucher von Schließungen eines von zwei Dingen schreiben:
fn foo(&self, closure: FnMut<usize, usize>) { ... } fn foo<T: FnMut<usize, usize>>(&self, closure: T) { ... }
Wir ... wollen wahrscheinlich die Syntax FnMut(usize) -> usize
, vielleicht Zucker wie FnMut(usize) -> usize
oder | usize | speichern -> usize etc. Es ist nicht so wichtig, es ist wichtig, dass wir den Abschluss nach Wert übergeben . Bitte beachten Sie, dass es gemäß den aktuellen DST-Regeln (Dynamically-Sized Types) zulässig ist, einen Typ als Wert als Argument an das FnMut<usize, usize>
. Daher ist das Argument FnMut<usize, usize>
eine gültige Sommerzeit und kein Problem.
Nebenbei : Dieses Projekt ist nicht abgeschlossen, und ich werde alle Details in einer separaten Nachricht beschreiben.
Das Problem ist, dass ein &mut
Link erforderlich ist, um einen Abschluss aufzurufen. Da der Abschluss als Wert übergeben wird, müssen Benutzer erneut mut
schreiben, wo er fehl am Platz aussieht:
fn foo(&self, mut closure: FnMut<usize, usize>) { let x = closure.call(3); }
Dies ist das gleiche Problem wie im obigen Env
Beispiel: Was hier tatsächlich passiert, ist, dass das FnMut
nur eine eindeutige Verknüpfung wünscht, aber da es nicht Teil des Typsystems ist, fordert es eine veränderbare Verknüpfung an.
Jetzt können wir das vielleicht auf verschiedene Arten umgehen. Eine Option, die wir tun könnten, ist ||
Die Syntax würde sich nicht zu einem „bestimmten Strukturtyp“ erweitern, sondern zu einem „Strukturtyp oder einem Zeiger auf einen Strukturtyp, wie durch Typinferenz vorgegeben“. In diesem Fall könnte der Anrufer schreiben:
fn foo(&self, closure: &mut FnMut<usize, usize>) { let x = closure.call(3); }
Ich möchte nicht sagen, dass dies das Ende der Welt ist. Dies ist jedoch ein weiterer Schritt vorwärts in den wachsenden Verzerrungen, die wir durchlaufen müssen, um diese Lücke zwischen lokalen Variablen und Referenzen aufrechtzuerhalten.
Andere API-Teile
Ich habe keine erschöpfende Studie durchgeführt, aber dieser Unterschied macht sich natürlich anderswo bemerkbar. Um beispielsweise aus Socket
zu lesen, benötige ich einen eindeutigen Zeiger, daher muss ich ihn als veränderbar deklarieren. Daher funktioniert dies manchmal nicht:
let socket = Socket::new(); socket.read()
Nach meinem Vorschlag würde ein solcher Code natürlich gut funktionieren. Sie würden immer noch eine Fehlermeldung erhalten, wenn Sie versuchen würden, von &Socket
zu lesen, aber dann würde es so etwas wie "Es ist unmöglich, einen eindeutigen Link zu einem gemeinsam genutzten Link zu erstellen" lesen, was ich persönlich für verständlicher halte.
Aber brauchen wir nicht mut
für die Sicherheit?
Nein, überhaupt nicht. Rust-Programme wären gleich gut, wenn Sie nur alle Bindungen als mut
deklarieren würden. Der Compiler ist perfekt in der Lage zu verfolgen, welche lokalen Variablen sich zu einem bestimmten Zeitpunkt ändern - gerade weil sie für die aktuelle Funktion lokal sind. Was das Typensystem wirklich interessiert, ist die Einzigartigkeit.
Die Bedeutung, die ich in den aktuellen Regeln für die Anwendung von mut
sehe, und ich werde nicht leugnen, dass es Wert hat, ist in erster Linie, dass sie helfen, die Absicht zu erklären. Das heißt, wenn ich den Code lese, weiß ich, welche Variablen neu zugewiesen werden können. Andererseits verbringe ich auch viel Zeit damit, C ++ - Code zu lesen, und ehrlich gesagt habe ich nie bemerkt, dass dies ein großer Stolperstein ist. (Gleiches gilt für die Zeit, die ich mit dem Lesen von Code in Java, JavaScript, Python oder Ruby verbracht habe.)
Es stimmt auch, dass ich manchmal Fehler finde, weil ich die Variable als mut
deklariert und vergessen habe, sie zu ändern. Ich denke, wir könnten ähnliche Vorteile mit anderen, aggressiveren Überprüfungen erzielen (zum Beispiel ändert sich keine der Variablen, die in der Schleifenbedingung verwendet werden, im Körper der Schleife). Ich persönlich kann mich nicht erinnern, mit der umgekehrten Situation konfrontiert zu sein: Wenn der Compiler sagt, dass etwas veränderbar sein sollte, bedeutet dies im Grunde immer, dass ich das mut
Schlüsselwort irgendwo vergessen habe. (Denken Sie daran: Wann haben Sie das letzte Mal auf einen Compilerfehler bezüglich einer ungültigen Änderung reagiert, indem Sie etwas anderes getan haben, als den Code neu zu strukturieren, um die Änderung gültig zu machen?)
Alternativen
Ich sehe drei Alternativen zum aktuellen System:
- Die, die ich vorgestellt habe, in der Sie einfach „Veränderlichkeit“ wegwerfen und nur die Einzigartigkeit verfolgen.
- Eine, bei der Sie drei Referenztypen haben:
&
, &uniq
und &mut
. (Wie ich schrieb, ist dies tatsächlich das Typensystem, das wir heute haben, zumindest aus der Sicht eines Kreditprüfers.) Eine strengere Option, bei der Nicht-Mut-Variablen immer als getrennt betrachtet werden. Dies würde bedeuten, dass Sie schreiben müssten:
let mut errors = 0; let mut p = &mut errors;
Sie müssen p
als mut
deklarieren, da sonst die Variable als getrennt betrachtet wird, obwohl es sich um eine lokale Variable handelt. Daher ist das Ändern von *p
nicht zulässig. Was in diesem Schema seltsam ist, ist, dass die lokale Variable KEINEN separaten Besitz zulässt, und wir wissen es mit Sicherheit, denn wenn Sie versuchen, ihren Alias zu erstellen, bewegt sie sich, der Destruktor startet darauf usw. Das heißt, wir haben immer noch das Konzept "besessen", das sich von "erlaubt kein separates Eigentum" unterscheidet.
Wenn wir andererseits dieses System beschreiben und sagen, dass die Veränderlichkeit über &mut
Zeiger vererbt wird, ohne über das gemeinsame Eigentum zu stottern, könnte dies sinnvoll sein.
Von diesen dreien bevorzuge ich definitiv Nr. 1. Es ist das einfachste, und jetzt interessiert mich am meisten, wie wir Rust vereinfachen können, indem wir seinen Charakter bewahren. Ansonsten bevorzuge ich die, die wir gerade haben.
Fazit
Grundsätzlich finde ich, dass die aktuellen Regeln zur Veränderlichkeit einen gewissen Wert haben, aber sie sind teuer. Sie sind eine Art fließende Abstraktion: Das heißt, sie erzählen eine einfache Geschichte, die sich tatsächlich als unvollständig herausstellt. Dies führt zu Verwirrung, wenn Menschen von einem anfänglichen Verständnis, in dem &mut
die Funktionsweise von Mutabilität widerspiegelt, zu einem vollständigen Verständnis mut
: Manchmal wird mut
nur benötigt, um die Eindeutigkeit sicherzustellen, und manchmal wird Mutabilität ohne das Schlüsselwort mut
.
Darüber hinaus müssen wir mit Vorsicht handeln, um die Fiktion aufrechtzuerhalten, die mut
Mutabilität und nicht Einzigartigkeit bedeutet. Wir haben Sonderfälle hinzugefügt, in denen der Kreditnehmer nach Schließungen suchen kann. Wir müssen die Regeln bezüglich &mut
Mutabilität im Allgemeinen komplexer gestalten. Wir müssen entweder mut
zu den Closures hinzufügen, damit wir sie aufrufen können, oder die Syntax der Closures auf weniger offensichtliche Weise öffnen. Usw.
Am Ende wird alles zu einer komplexeren Sprache als Ganzes. Anstatt nur über gemeinsames Eigentum und Einzigartigkeit nachzudenken, sollte der Benutzer über gemeinsames Eigentum und Veränderlichkeit nachdenken, und beide sind irgendwie durcheinander.
Ich denke nicht, dass es das wert ist.