Un autre article sur les vies à Rust

Les premiers mois d'un émeutier novice se résument généralement à un en-tête sur le concept de vie et de possession. Certaines personnes se décomposent à ce sujet, mais pour ceux qui ont pu survivre, cela ne semble plus inhabituel ou faux. Je décrirai les points clés qui, il me semble, ont aidé à s'adapter plus rapidement et mieux au concept de vie et de biens.


Bien sûr, les nouvelles officielles sont plus complètes et plus détaillées, mais elles nécessitent également plus de temps et de patience pour bien comprendre et absorber toutes les informations. J'ai essayé d'éviter un grand nombre de détails et de tout présenter par ordre croissant de complexité, dans le but de rendre cet article plus accessible à ceux qui ont juste commencé à regarder le rast ou qui n'ont pas vraiment compris les premiers moments du babillard officiel.


Cela m'a également fait écrire que, par exemple, dans les monades, vous pouvez trouver du matériel de formation officiel, mais ils ne sont pas toujours bien compris, et la compréhension n'apparaît qu'après avoir lu quelque chose comme «une autre introduction» sur ce sujet.


À vie


Nous devons d'abord nous familiariser avec deux choses: la fin du bloc et le déplacement de la valeur vers un autre bloc. Plus tard, nous commencerons à compliquer les choses en ajoutant «prêt», «mutabilité» et «mutabilité cachée».


Tout d'abord, la durée de vie d'une valeur est déterminée par le segment suivant:


  • Le début de la vie: créer de la valeur. Ceci est courant pour la plupart des langages de programmation, il ne supporte donc aucune charge inhabituelle.
  • La fin de vie. C'est là que Rust appellera automatiquement le destructeur et oubliera la valeur. Dans un bloc de portée, cela se produira à la fin de ce bloc sans bouger. C'est le suivi mental de la fin de vie qui est, à mon avis, la clé d'une interaction réussie avec le emprunteur.

J'ajouterai un détail qui peut être utile: s'il y a plusieurs valeurs dans la portée, elles seront détruites dans l'ordre inverse de la création.


Un autre point: je vais créer une chaîne, car elle n'a pas de marqueur de copie, et les valeurs qui ont ce marqueur ne bougent pas mais sont copiées, ce qui est considéré comme une opération plutôt bon marché, mais modifie le comportement du déplacement (et facilite le travail avec les types primitifs), mais plus à ce sujet plus tard.


Des exemples peuvent être exécutés ici: https://play.rust-lang.org/


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

Avec un bloc simple, tout est relativement simple, l'étape suivante se produit lorsque nous utilisons des choses apparemment simples comme les fonctions et les fermetures:


Déménagement


Ajoutez un concept comme déplacer une valeur. En termes simples, «déplacer» signifie que le bloc actuel n'est plus intéressé par le sort de la valeur et qu'il l'oublie, et son destin est transféré vers un autre bloc, par exemple, vers une autre fonction, ou vers une fermeture, ou simplement vers une autre valeur.


 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" . } 

Avec fermetures.


Pour que la fermeture déplace la valeur capturée vers son bloc, le mot-clé move est utilisé, si vous n'écrivez pas move, la valeur est empruntée, sur laquelle j'écrirai très bientôt.


 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(); } 

Vous pouvez vous déplacer à la fois vers la fonction et depuis la fonction ou vers une autre valeur.


Cet exemple montre comment suivre les mouvements des valeurs afin de vivre en paix avec le emprunteur.


 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"   . } 

Prêt


Nous introduisons ce nouveau concept: contrairement au déplacement, cela signifie que le bloc actuel réserve le contrôle de la valeur, il permet simplement à l'autre bloc d'utiliser sa valeur.


Je note que l'emprunt a également lieu là où il s'est terminé, ce qui n'est pas très important dans ces exemples, mais apparaîtra dans le paragraphe suivant.


Remarque: je n'écrirai pas sur la façon de spécifier la durée de vie directement dans la fonction, car la rouille moderne le fait automatiquement mieux que par le passé, et la divulgation de tout cela est de quelques pages de plus.


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

Avec des fermetures similaires:


 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"  . } 

En fait, dans la plupart de ces constructions simples, l'utilisateur a juste besoin de décider où il veut mettre fin à la vie de la valeur: à la fin du bloc en cours et en le prêtant à certaines fonctions, ou, si nous savons que nous n'avons plus besoin de la valeur, puis déplacez-le vers la fonction à la fin par lequel il sera lui-même détruit, plus nous libérerons rapidement la mémoire, mais la valeur ne sera plus disponible dans le bloc actuel.


Mutabilité


Dans le rasta, comme par exemple dans le kotlin, il y a une division en valeurs mutables et non stables. Mais le problème se pose que la mutabilité a un effet sur les prêts:
Vous pouvez emprunter plusieurs fois une valeur non stable et une valeur mutable ne peut être empruntée mutuellement qu'une seule fois. Vous ne pouvez pas muter une valeur déjà empruntée précédemment.


Un exemple qui n'est pas lié aux précédents, lorsque ce concept nous sauve des problèmes en interdisant les prêts simultanés mutables et non stables:


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

Ici, il est déjà nécessaire de faire le plein de diverses astuces pour satisfaire, pour la plupart, les justes revendications du rasta. Dans l'exemple ci-dessus, le plus simple serait de cloner "a" -> le clone aura un prêt non stable et ne sera pas lié au "a" d'origine.


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

Mais je ferais mieux de revenir à nos exemples afin de maintenir la cohérence. Nous devons changer "a" et nous ne pouvons pas le faire.


 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); } 

Mutation cachée


Théoriquement, une fermeture peut être transmise à une fonction qui traite, par exemple, de manière asynchrone dans un autre thread, puis nous aurions vraiment des problèmes, mais dans ce cas, le emprunteur est réassuré, bien que cela n'annule pas le fait que nous devons en quelque sorte l'accepter. .


Conclusion: nous avons besoin de deux emprunts mutants, mais le rast ne permet qu'une seule chose, mais les inventeurs rusés du rasta proposent une «mutation cachée»: RefCell.


RefCell - ce que nous encapsulons dans RefCell - le raster le considère nemable, cependant, en utilisant la fonction borrow_mut (), nous pouvons temporairement extraire un lien mutable par lequel il peut changer la valeur, mais il y a une nuance importante : le lien ne peut être obtenu que lorsque RefCell au moment de l'exécution s'assure qu'il n'y en a pas d'autres prêts actifs, sinon il lancera la panique ou retournera une erreur si try_borrow_mut () est utilisé. C'est-à-dire ici, la croissance donne tous les soucis du prêt aux soins de l'utilisateur, et il doit s'assurer lui-même qu'il n'emprunte pas la valeur à plusieurs endroits à la fois.


 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()); //         . } 

Compteur de liaison Rc


Cette construction est familière dans de nombreuses langues et est utilisée dans rast, lorsque, par exemple, nous ne pouvons pas emprunter une valeur pour une raison quelconque, et qu'il est nécessaire d'avoir plusieurs valeurs de référence pour une seule valeur. Rc, comme son nom l'indique, est simplement un compteur de référence qui possède une valeur, il peut emprunter des liens non fiables, compter leur nombre et dès que leur nombre est réinitialisé, il détruit la valeur et lui-même. Il s'avère que Rc permet, pour ainsi dire, d'étendre secrètement la durée de vie de la valeur qui y est contenue.


J'ajouterai que le rast peut automatiquement faire le deref pour les structures pour lesquelles il est défini, ce qui signifie que pour travailler avec Rc, en règle générale, vous n'avez besoin d'aucune extraction supplémentaire de la valeur interne et nous travaillons simplement avec Rc comme avec la valeur à l'intérieur.


Ici, un exemple simple a été pensé un peu dur, essayons d'émuler que la fermeture de l'exemple ci-dessus ne veut pas accepter & T ou & String, mais veut juste 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); } 

Ce problème serait facilement résolu si nous pouvions changer la fonction en fn f(x: &String) (ou & str), mais imaginons que pour une raison quelconque nous ne pouvons pas utiliser &


Nous utilisons 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       . } 

J'ajouterai le dernier exemple, car l'une des paires de conteneurs les plus fréquentes que l'on peut trouver est Rc <RefCell>

 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   } 

De plus, il serait logique de déplacer ce didacticiel vers un analogue sans fil de Rc-Arc, puis de continuer à propos de Mutex, mais vous ne parlerez pas de la sécurité des threads et du emprunteur dans un paragraphe, et il n'est pas clair si ce type d'article est nécessaire du tout, car il existe un thread officiel. Je conclus donc.

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


All Articles