Ein weiterer Artikel über die Lebenszeiten in Rust

In den ersten Monaten eines unerfahrenen Riotman geht es in der Regel um eine Überschrift über das Konzept eines Lebens und Besitzes. Einige Leute brechen dies zusammen, aber für diejenigen, die überleben konnten, scheint dies nicht länger ungewöhnlich oder falsch zu sein. Ich werde die wichtigsten Punkte beschreiben, die meines Erachtens dazu beigetragen haben, sich schneller und besser an das Konzept von Leben und Besitz anzupassen.


Natürlich ist der offizielle Newsletter umfassender und detaillierter, aber es erfordert auch mehr Zeit und Geduld, um alle Informationen vollständig zu verstehen und aufzunehmen. Ich habe versucht, eine große Anzahl von Details zu vermeiden und alles in der Reihenfolge zunehmender Komplexität zu präsentieren, um diesen Artikel für diejenigen zugänglicher zu machen, die entweder gerade erst angefangen haben, das Rast zu sehen, oder die ersten Momente des offiziellen Bulletin Boards nicht wirklich verstanden haben.


Es hat mich auch dazu gebracht zu schreiben, dass man zum Beispiel von Monaden einige offizielle Schulungsmaterialien finden kann, die jedoch nicht immer gut verstanden werden, und dass Verständnis erst entsteht, wenn man so etwas wie eine „weitere Einführung“ zu diesem Thema liest.


Lebenszeit


Zuerst müssen wir uns mit zwei Dingen vertraut machen - dem Ende des Blocks und dem Verschieben des Werts in einen anderen Block. Später werden wir beginnen, es durch Hinzufügen von "Kreditvergabe", "Veränderlichkeit" und "versteckte Veränderlichkeit" zu komplizieren.


Zunächst wird die Lebensdauer eines Wertes durch das folgende Segment bestimmt:


  • Der Beginn des Lebens: Wertschöpfung. Dies ist für die meisten Programmiersprachen üblich und trägt daher keine ungewöhnliche Last.
  • Das Ende des Lebens. Hier ruft Rust automatisch den Destruktor auf und vergisst den Wert. In einem Scope-Block geschieht dies am Ende dieses Blocks, ohne sich zu bewegen. Meiner Meinung nach ist die mentale Verfolgung des Lebensendes der Schlüssel für eine erfolgreiche Interaktion mit dem Kreditnehmer.

Ich werde ein Detail hinzufügen, das nützlich sein kann: Wenn der Bereich mehrere Werte enthält, werden diese in umgekehrter Reihenfolge der Erstellung zerstört.


Ein weiterer Punkt: Ich werde eine Zeichenfolge erstellen, da sie keine Kopiermarkierung enthält und die Werte mit dieser Markierung nicht verschoben, sondern kopiert werden. Dies wird als recht billige Operation angesehen, ändert jedoch das Verhalten der Verschiebung (und erleichtert die Arbeit mit primitiven Typen). aber dazu später mehr.


Beispiele können hier ausgeführt werden: https://play.rust-lang.org/


fn main() { { //    let a = "a".to_string(); // <-   "a" let b = 100; // <-   "b" // <-   b // <-   a } //    //     "a"  "b" } 

Mit einem einfachen Block ist alles relativ einfach. Die nächste Stufe tritt ein, wenn wir scheinbar einfache Dinge wie Funktionen und Verschlüsse verwenden:


Umzug


Fügen Sie ein Konzept wie das Verschieben eines Werts hinzu. Mit einfachen Worten bedeutet „Bewegen“, dass der aktuelle Block nicht mehr am Schicksal des Werts interessiert ist und es vergisst und sein Schicksal auf einen anderen Block übertragen wird, beispielsweise auf eine andere Funktion oder auf einen Abschluss oder einfach auf einen anderen Wert.


 fn f<T: std::fmt::Display>(x: T) { //   ,         . println!("{}", x); // <-  ,   "a",    . } fn main() { let a = "a".to_string(); // "a"    let b = 2; f(a); //   "a"  f //        f(a) -   ,    "a"        .    a  b,    ,      Copy   . // "b" . } 

Mit Verschlüssen.


Damit der Abschluss den erfassten Wert in seinen Block verschiebt, wird das Schlüsselwort move verwendet. Wenn Sie move nicht schreiben, wird der Wert ausgeliehen, über den ich sehr bald schreiben werde.


 fn main() { let a = "a".to_string(); // "a"    let b = 2; let f_1 = move || {println!("{}", a)}; //   "a" //    "a"    . // let f_2 = move || {println!("{}", a)}; f_1(); } 

Sie können sowohl zur Funktion als auch von der Funktion oder zu einem anderen Wert wechseln.


Dieses Beispiel zeigt, wie Sie verfolgen können, wie sich Werte bewegen, um in Frieden mit dem Kreditprüfer zu leben.


 fn f(x: String) -> String { x + " and x" //    x   +,     . //  +   String,    . } fn main() { let a = "a".to_string(); //  "a" let b = f(a); //  "a"  "f",  f     b. println!("{}", b); // "a"   . } 

Ausleihe


Wir führen dieses neue Konzept ein: Im Gegensatz zum Verschieben bedeutet dies, dass der aktuelle Block die Kontrolle über den Wert behält und dem anderen Block einfach erlaubt, seinen Wert zu verwenden.


Ich stelle fest, dass die Ausleihe auch dort stattfindet, wo sie beendet wurde, was in diesen Beispielen nicht sehr wichtig ist, aber im nächsten Absatz auftauchen wird.


Hinweis: Ich werde nicht darüber schreiben, wie die Lebensdauer direkt in der Funktion angegeben werden kann, da der moderne Rost dies automatisch besser macht als früher, und die Offenlegung all dessen ist ein paar weitere Seiten.


 fn f(x: &String) { //   &,    . println!("{}", x); // <-  ,  "x"     } fn main() { let a = "a".to_string(); // "a"    f(&a); //   "a"  f //   f(&a); //    -  . println!("{}", a); //   // "a"  . } 

Mit Verschlüssen ähnlich:


 fn main() { let mut a = "a".to_string(); // "a"    let f_1 = || a.push_str("and x"); //   "a" let f_2 = || a.push_str("and x"); //   f_1(); f_2(); println!("{}", a); // "a"  . } 

Tatsächlich muss der Benutzer in den meisten dieser einfachen Konstruktionen nur entscheiden, wo er die Lebensdauer des Werts beenden möchte: am Ende des aktuellen Blocks und Ausleihen an einige Funktionen oder, wenn wir wissen, dass wir den Wert nicht mehr benötigen, ihn am Ende in die Funktion verschieben Dadurch, dass es selbst zerstört wird, wird der Speicher umso schneller freigegeben, aber der Wert ist im aktuellen Block nicht mehr verfügbar.


Veränderlichkeit


In Rasta, wie zum Beispiel in Kotlin, gibt es eine Unterteilung in veränderbare und instabile Werte. Es stellt sich jedoch das Problem, dass sich die Veränderlichkeit auf die Kreditvergabe auswirkt:
Sie können einen nicht stabilen Wert viele Male ausleihen, und ein veränderlicher Wert kann nur einmal gegenseitig ausgeliehen werden. Sie können einen bereits zuvor geliehenen Wert nicht mutieren.


Ein Beispiel, das nicht mit den vorherigen verwandt ist, wenn dieses Konzept uns vor Problemen bewahrt, indem es die gleichzeitige veränderliche und nicht stabile Kreditvergabe verbietet:


 fn main() { let mut a = "abc".to_string(); for x in a.chars() { //   a.push_str(" and "); //  .  . a.push(x); } } 

Hier ist es bereits notwendig, sich mit verschiedenen Tricks einzudecken, um die gerechten Ansprüche der Rasta größtenteils zu befriedigen. Im obigen Beispiel wäre es am einfachsten, "a" zu klonen -> der Klon hat ein nicht stabiles Darlehen und bezieht sich nicht auf das ursprüngliche "a".


 for x in a.clone().chars() { //  ,   . a.push_str(" and "); //  .      -   . 

Aber ich gehe besser zu unseren Beispielen zurück, um die Konsistenz aufrechtzuerhalten. Wir müssen "a" ändern und können es nicht tun.


 fn main() { let mut a = "a".to_string(); // "a"    let mut f_1 = || a.push_str(" and x"); //   "a".   - ,  mut  mut. //      ,   f_1  . let mut f_2 = || a.push_str(" and y"); //     : second mutable borrow occurs here f_1(); f_2(); println!("{}", a); } 

Versteckte Mutation


Theoretisch kann ein Abschluss an eine Funktion übergeben werden, die beispielsweise asynchron in einem anderen Thread verarbeitet wird, und dann hätten wir wirklich Probleme, aber in diesem Fall ist der Kreditprüfer rückversichert, obwohl dies nicht die Tatsache aufhebt, dass wir irgendwie damit einverstanden sein müssen .


Fazit: Wir brauchen zwei mutierende Anleihen, aber das Rast lässt nur eines zu, aber die gerissenen Erfinder des Rasta haben eine „versteckte Mutation“: RefCell.


RefCell - was wir in RefCell einschließen - das Raster hält es für nemable. Mit der Funktion bor_mut () können wir jedoch vorübergehend einen veränderlichen Link extrahieren, mit dem der Wert geändert werden kann. Es gibt jedoch eine wichtige Nuance : Der Link kann nur abgerufen werden, wenn RefCell zur Laufzeit sicherstellt, dass keine anderen vorhanden sind aktive Kredite, sonst wird er in Panik geraten oder einen Fehler zurückgeben, wenn try_borrow_mut () verwendet wird. Das heißt, Hier macht das Wachstum alle Sorgen um die Kreditvergabe an den Benutzer, und er selbst muss sicherstellen, dass er den Wert nicht an mehreren Stellen gleichzeitig ausleiht.


 use std::cell::RefCell; fn main() { let a = RefCell::new("a".to_string()); // "a"    let f_1 = || a.borrow_mut().push_str(" and x"); //    "a" let f_2 = || a.borrow_mut().push_str(" and y"); //    f_1(); //      a.borrow_mut() ,           mut    . f_2(); //   . println!("{}", a.borrow()); //         . } 

Rc Link Counter


Diese Konstruktion ist in vielen Sprachen bekannt und wird im Rast verwendet, wenn wir beispielsweise aus irgendeinem Grund keinen Wert ausleihen können und mehrere Referenzwerte für einen einzelnen Wert erforderlich sind. Rc ist, wie der Name schon sagt, einfach ein Referenzzähler, der einen Wert besitzt. Er kann instabile Links ausleihen, ihre Nummer zählen und sobald ihre Nummer zurückgesetzt wird, den Wert und sich selbst zerstören. Es stellt sich heraus, dass Rc es sozusagen erlaubt, die Lebensdauer des darin enthaltenen Werts heimlich zu verlängern.


Ich werde hinzufügen, dass der Rast automatisch deref für die Strukturen ausführen kann, für die er definiert ist, was bedeutet, dass Sie für die Arbeit mit Rc in der Regel keine zusätzliche Extraktion des internen Werts benötigen und wir nur mit Rc wie mit dem darin enthaltenen Wert arbeiten.


Hier wurde ein einfaches Beispiel ein wenig überlegt. Versuchen wir zu emulieren, dass der Abschluss aus dem obigen Beispiel & T oder & String nicht akzeptieren möchte, sondern nur String:


 fn f(x: String) { //  String,    &String println!("{}", x); } fn main() { let a = "a".to_string(); let f_1 = move || f(a); //   move,    ... let f_2 = move || f(a); // ...     ,           f_1(); f_2(); println!("{}", a); } 

Dieses Problem wäre leicht zu lösen, wenn wir die Funktion in fn f(x: &String) (oder & str) ändern könnten, aber stellen wir uns vor, dass wir & aus irgendeinem Grund nicht verwenden können


Wir verwenden Rc


 use std::rc::Rc; fn f(x: Rc<String>) { //       Rc println!("{}", x); //     ,  println          ,           ,       ,    . } fn main() { let a_rc = Rc::new("a".to_string()); //  Rc   let a_ref_1 = a.clone(); //   -,  . let a_ref_2 = a.clone(); //   let f_1 = move || f(a_ref_1); //      - let f_2 = move || f(a_ref_2); //  f_1(); f_2(); println!("{}", a_rc); //     Rc  . //    a_rc       . } 

Ich werde das letzte Beispiel hinzufügen, da eines der häufigsten Containerpaare, die gefunden werden können, Rc <RefCell> ist

 use std::rc::Rc; use std::cell::RefCell; fn f(x: Rc<RefCell<String>>) { x.borrow_mut().push_str(" and x"); //      ,       ,   . } fn main() { let a = Rc::new(RefCell::new("a".to_string())); //      let a_ref_1 = a.clone(); let a_ref_2 = a.clone(); let f_1 = move || f(a_ref_1); let f_2 = move || f(a_ref_2); f_1(); f_2(); println!("{}", a.borrow()); // Rc   ,   RefCell   } 

Außerdem wäre es logisch, dieses Tutorial auf ein thread-sicheres Analogon von Rc-Arc zu verschieben und dann mit Mutex fortzufahren. Sie werden jedoch nicht in einem Absatz über Thread-Sicherheit und Leihprüfung sprechen, und es ist nicht klar, ob diese Art von Artikel überhaupt benötigt wird, da es einen offiziellen Thread gibt. Also schließe ich.

Source: https://habr.com/ru/post/de442962/


All Articles