L'ownership : le cœur de Rust
Syntaxe
L’ownership est LE concept de Rust, c’est à cause de lui que tu as des erreurs de compilation sans arrêt si tu essayes le langage sans avoir compris cette notion.
Bon alors déjà à quoi ça sert ?
Les différentes techniques pour gérer la mémoire
Rust a pour principale mission d’assurer la safety du programme, c’est-à-dire être sûr que ton programme ne fait pas fuiter de mémoire.
Le Garbage Collector
Dans d’autres langages comme Python, Go etc on n’a pas à gérer manuellement la mémoire, car on a un système qui s’appelle un Garbage Collector qui va faire le ménage dans la mémoire plus utilisée par le programme.
Ça marche assez bien, mais ce système est assez prudent et donc il préfère laisser trainer de la mémoire inutile plutôt que de supprimer de la mémoire dont le programme aurait besoin plus tard, résultat on a plus de mémoire utilisée que pour le même programme en C par exemple.
Gestion manuelle de la mémoire
En C, c’est à nous de tout faire, on alloue notre mémoire et on la désalloue manuellement.
Là aussi ça fonctionne, mais il ne faut pas oublier de la désallouer, car sinon on utilise de la mémoire pour rien et SURTOUT il faut faire attention à ne pas accéder à un pointeur qui n’existe plus en mémoire, car on accéderait à une autre valeur en mémoire que celle demandé (ça peut être de la mémoire d’un autre programme et c’est là le début du hacking!)
L’approche de Rust
En Rust l’approche est différente, on n’alloue pas nous même de la mémoire comme en C et on ne la désalloue pas non plus.
Il n’y pas de Garbage Collector non plus, mais alors comment fait Rust pour gérer la mémoire ?!
Rust a choisi une approche très intelligente ou il va automatiquement alloué de la mémoire et la désallouer en fonction de certaines règles, ce sont les règles de l’ownership.
L’ownership en français ça veut dire la propriété et owner veut dire propriétaire.
Voici les 3 règles de l’ownership que je te conseille de connaître par cœur :
- Chaque valeur appartient à une variable qu’on appelle son owner
- Une valeur ne peut avoir qu’un owner à la fois
- Quand l’owner sort du scope, la valeur sera effacée
Bon là comme ça, ça ne doit pas vraiment te parler, mais je vais t’illustrer ça avec des exemples au fil des sections suivantes.
Chaque valeur appartient à un seul owner
Dis comme ça on se dit que ce concept est évident, c’est-à-dire que pour chaque valeur, il n’y a qu’une seule variable associée.
Mais en réalité pour Rust ça a une grande importance, car cela va lui permettre d’économiser de la mémoire en faisant ce
qu’on appelle un move
.
Un move
, c’est le fait de changer l’owner d’une valeur.
Un exemple d’un move :
Ici lorsqu’on fait let b = a;
la valeur de a
(à savoir la string “Bonjour”) est move vers b
et
donc a
est supprimé puisqu’il ne contient plus de valeur !
Au départ la string “Bonjour” est liée à a
Puis lorsqu’on fait let b = a;
la string “Bonjour” devient lié à b
et comme le dit la 2ᵉ règle de l’ownership “Une
valeur ne peut avoir qu’un owner à la fois”. C’est pourquoi la string “Bonjour” change d’owner en passant de a à b
Du coup a
se retrouve sans aucune valeur liée à lui et donc a
sera drop
, c’est-à-dire effacé de la mémoire.
Il existe cependant une exception à cette règle, ce sont toutes les valeurs qui ne coutent pas grande chose en termes de mémoire et de performance à copier, à savoir toutes les valeurs scalaires (u8,… i32 etc), à la différence des strings qui sont plus lourdes à copier.
Dans ces cas-là Rust copie la valeur plutôt que de la move.
Si on reprend le même exemple, mais avec des i32 voici ce que ça donne :
La portée d’une variable
Lorsqu’une variable sort de son scope sa valeur est effacée.
C’est le fameux système de l’ownership de Rust.
Rust efface tout ce quitte le scope et c’est comme ça qu’il s’assure de ne pas faire fuiter de la mémoire.
C’est aussi pour ça que les débutants ont beaucoup de mal à commencer à utiliser Rust sans comprendre ce concept au préalable.
Mais Rust ne le fait pas uniquement lorsqu’une variable quitte le scope, mais aussi lorsqu’elle change de scope.
“Attends comment ça change de scope ?!”
Quand tu appelles une fonction en lui passant une variable en paramètre, cette variable va se déplacer de scope et va prendre le scope de la fonction.
En réalité ce qui se passe, c’est que là que valeur de la variable va être move vers le paramètre de la fonction.
Donc techniquement quand tu envoies une variable dans une fonction, elle n’est plus accessible dans le scope ou elle a été déclarée.
Dans l’exemple suivant je vais créer une string, même si on ne les a pas encore vus.
Voici ce qui se passe en détails
Au départ on a juste une variable me qui est associée à la valeur “John”.
Et puis lorsqu’on appelle la fonction en passant en paramètre me
, la valeur de me
va être move dans first_name
Comme me
n’a plus de valeur associée il est drop et disparait du programme.
C’est ce qui explique pourquoi on ne peut plus utiliser me
après l’avoir fait passer dans la fonction.
Ce qui se passe en réalité, c’est que la valeur de me
va être move dans l’argument first_name
de la fonction
show_first_name
Il existe une exception avec certain type de valeurs qui ne sont pas move…
Comme Rust est malin il sait qu’il peut se permettre de copier certaine valeur qui ne prennent pas trop de place en mémoire pour éviter de les move, c’est le cas de tous les types scalaire (u8,u16,…,i32,f32 etc).
Si on reprend le même exemple
Donc tous les types scalaires sont copiés, car ça ne coute pas grand-chose en termes de mémoire et de performance (à la différence des strings).
Tu vois donc que Rust est très radical sur la portée des variables et leur accessibilité, à première vu, c’est très extrême et contraignant, mais on verra juste après comment faire pour ne pas move une variable.
Comment utiliser une variable sans transférer son ownership ?
Pour passer une variable dans une fonction sans la move, on va utiliser ce qu’on appelle une référence avec le
symbole &
.
Pour faire simple une référence, c’est simplement comme son nom l’indique un “référence” à la valeur que contient la variable, mais pas la variable elle-même, prenons l’exemple suivant :
Ici tu remarques que le type de la fonction est &String
, car on indique qu’on doit recevoir une référence de String, pareil dans l’appel de la fonction on envoie une référence de a
Rust introduit 2 règles concernant les références.
À chaque moment pour une variable donnée il peut y avoir soit :
- Une référence mutable
- N’importe quel nombre de références immutables
Mais pas les 2 en même temps !