Home
# Ownership --- ## Généralités - Les règles sur la `propriété` sont ce qui rend Rust unique. - La `propriété` est un ensemble de règles vérifiées à la compilation. - Garantit la bonne gestion de la mémoire sans `garbage collector`. - Oblige à réfléchir à ce que fait notre programme quand on le compile (et que le compilateur râle). - Spoiler alert: simple à expliquer, difficile à appliquer. --- # Pile et tas (Stack and heap) --- ## La pile - Partie de la mémoire réservée pour **chaque** thread. - Lorsqu'une fonction est appelée, un bloc est réservé pour stocker les variables locales. - Lorsque la fonction retourne, la mémoire est libérée et peut être réutilisée. - C'est une structure LIFO (Last in first out): le dernier ajout sera libéré en premier. - Stocke typiquement les objets dont la taille est connue à la compilation (entiers, nombres à virgule flottante, caractères, booléens, ...). --- ## Le tas - Partie de la mémoire réservée est **commune** à tous les threads. - Utilisée pour l'allocation dynamique (la taille des objets peut varier en cours d'exécution). - Peut modifier de la mémoire non-locale à une fonction. - Pas de structure particulière pour l'allocation/désallocation. - Plus compliqué de gérer l'allocation/désallocation. - Typiquement plus lent que la pile (il faut chercher de la place pour l'allocation et "sauter" en mémoire pour la retrouver). --- # La propriété (Ownership) --- ## Les règles de la propriété 1. Chaque valeur a une variable qui est son propriétaire (`owner`). 2. Une valeur ne peut avoir qu'un seul propriétaire à chaque instant. 3. Quand le programme sort de la portée du propriétaire, la valeur est détruite (`dropped`). ```rust [2|3-6|] fn main() { let x = 5; // x est propriétaire de la mémoire contenant 5 { let y = 6; // y est propriétaire de la mémoire contenant 6 println!("La valeur de (x,y) est: ({}, {}).", x, y); } // y sort de la portée et est détruite avec la valeur 6 println!("La valeur de x est: {}", x); } // x sort de la portée et sa valeur est détruite ``` --- ## Allocation de la mémoire: Manuelle * Manuellement (C/C++, ...): on ordonne à l'OS d'allouer/désallouer de la mémoire sur le tas. - Oublier de désallouer la mémoire allouée (fuite mémoire/memory leak). - Désallouer de la mémoire trop tôt (dangling pointer et comportement indéfini). - Libérer la mémoire à double. --- ## Allocation de la mémoire: Garbage collection * Automatiquement: on a un "garbage collector" (java, scala, ...). - Consomme des ressources. - Est une "boîte magique": il fait ce qu'il veut. --- ## Allocation de la mémoire: Rust - En Rust on contrôle où et quand on alloue/désalloue à la compilation: - Difficulté: il faut suivre des règles **très** strictes. - Garbage collection "à la compilation". --- ## Allocation de mémoire (tas) - Les types vus jusque là sont stockés dans la pile: leur *taille est connue* à la compilation. - Que se passe-t-il lorsque la taille est *inconnue à la compilation*? ```rust fn main() { let x = [1, 2, 3, 4]; // x de type [i32; 4], sur la pile // on ne peut pas augmenter sa taille println!("La valeur de x est: {:?}", x); let mut y = Vec::new(); // un vecteur dont la taille est variable for i in 0..5 { // On rajoute des éléments au vecteur avec push(elem) y.push(i); } println!("La valeur de y est: {:?}", y); } // x/y sortent de la portée, il sont détruits et la mémoire est libérée ``` --- ## Allocation de mémoire (Vec)  - Pile: 1 pointeur vers le tas, et 2 entiers (longueur et capacité). - Tas: 1, 2, 3, 4. - Variable (proprétaire) sort de la portée, mémoire **automatiquement libérée**. --- ## Extension de la propriété Flexibiliser la propriété: 1. Donner la propriété à un autre propriétaire: `move`. 2. Emprunter pour un temps les données: `borrow`. 3. Être co-propriétaires dans des structures avancées: `Rc` et `Arc`. --- # Move --- ## Changement de propriétaire ```rust [7-8|] fn main() { let mut y = Vec::new(); for i in 0..5 { y.push(i); } println!("La valeur de y est: {:?}", y); let z = y; // le vecteur (1,2,3,4) est maintenant propriété de z // y est une variable non initialisée println!("La valeur de z est: {:?}", z); } // z sort de la portée, il est détruit et la mémoire est libérée ``` --- ## Changement de propriétaire: invalide ```rust compile_fail [7-8|9|] fn main() { let mut y = Vec::new(); for i in 0..5 { y.push(i); } println!("La valeur de y est: {:?}", y); let z = y; // le vecteur (1,2,3,4) est maintenant propriété de z // y est une variable non initialisée println!("La valeur de y est: {:?}", y); } // Ce code ne compilera pas. ``` --- ## Changement de propriétaire (3/3) - La variable `y` et copiée dans la variable `z`. - En ne faisant rien on a **deux** propriétaires des données. - Illégal: on invalide `y`.  --- ## Exception au `move`: `Copy` ```rust [3-5] fn main() { let y = 1; let mut z = y; // Généralement: move // y est i32: on copie puis // on assigne la valeur à z println!("Les valeurs de y et z sont : ({}, {})", y, z); z = 2; // comme la valeur est copiée modifier z ne modifie pas y println!("Les valeurs de y et z sont : ({}, {})", y, z); } ``` --- ## Différence entre Copie et Move - *move*: copie uniquement la variable et le propriétaire **change**. - *copie*: on duplique la variable **et** les données.  --- ## Quand interviennent les `move`? - Si le type de la variable **n'est pas** `Copy`: 1. Lors d'une assignation. 2. Lors du passage en paramètre à une fonction. 3. Lors du retour d'une fonction. --- ## Lors du passage en paramètre à une fonction ```rust compile_fail [1-3,8|] fn take_own(_v: Vec
) { // on fait des choses } fn main() { let mut y = Vec::new(); // un vecteur dont la taille est variable // On rajoute des éléments au vecteur avec push(elem) y.push(1); y.push(2); y.push(3); y.push(4); take_own(y); println!("La valeur de y est: {:?}", y); } // A votre avis que se passe-t-il? ``` --- ## Lors du retour d'une fonction ```rust fn give_own() -> Vec
{ let mut y = Vec::new(); // un vecteur dont la taille est variable y.push(1); y.push(2); y.push(3); y.push(4); y // on retourne y } fn main() { let y = give_own(); println!("La valeur de y est: {:?}", y); } // A votre avis que se passe-t-il? ``` --- ## Un mélange des deux ```rust fn get_len(v: Vec
) -> (Vec
, usize) { let length = v.len(); // on ajoute 2 au vecteur (v, length) // on retourne v et sa longueur } fn main() { let mut y = Vec::new(); y.push(1); y.push(2); y.push(3); y.push(4); let (y, length) = get_len(y); println!("La valeur de y est: {:?} et sa longueur {}", y, length); } // A votre avis que se passe-t-il? ``` --- # L'emprunt (Borrowing) --- ## La référence - Le `move` est trop contraignant, la copie lente (et pas toujours ce qu'on veut). - Il est pratique de pouvoir **emprunter** les objets. - Le **borrowing** permet d'accéder aux données sans avoir la propriété de l'objet. - Cela se fait à l'aide d'une **référence** sur l'objet qu'on souhaite emprunter. - Si `y` est une variable, `&y`est la référence vers la variable (le pointeur vers cette variable). - La référence permet **l'emprunt** de données **sans** en prendre la **propriété**. --- ## La référence (schéma)  --- ## Exemple 1 ```rust [1-3,9|] fn get_len(v: &Vec
) -> usize { v.len() } // on sort de la portée de la fonction, // la propriété des données dans v est rendue. fn main() { let mut y = Vec::new(); y.push(1); y.push(2); y.push(3); y.push(4); let length = get_len(&y); // la référence vers y est passée println!("La valeur de y est: {:?} et sa longueur {}", y, length); } ``` --- ## Exemple 2 ```rust compile_fail [1-4,10|] fn get_len(v: &Vec
) -> usize { v.push(2); // on ajoute 2 à v v.len() } // on sort de la portée de la fonction, // la propriété des données dans v est rendue. fn main() { let mut y = Vec::new(); y.push(1); y.push(2); y.push(3); y.push(4); let length = get_len(&y); // la référence vers y est passée println!("La valeur de y est: {:?} et sa longueur {}", y, length); } ``` --- ## Référence mutable ```rust [1,6,8|] fn get_len(v: &mut Vec
) -> usize { // référence mutable v.push(2); v.len() } fn main() { let mut y = Vec::new(); // variable mutable y.push(1); y.push(2); y.push(3); y.push(4); let length = get_len(&mut y); // référence mutable en argument println!("La valeur de y est: {:?} et sa longueur {}", y, length); } ``` --- ## Règles pour les références - Sur une variable on peut avoir: 1. Autant de **références immutables** qu'on veut. 2. Une seule **référence mutable**. - La référence doit toujours être **valide**. --- ## Sans la règle sur la référence mutable ```rust compile_fail [2|3|4|5-6|] fn main() { let mut y = Vec::new(); let z = &y; y.push(1); y.push(2); y.push(3); y.push(4); println!("La valeur de y est: {:?} et celle de z est {:?}", y, z); // Quel est le problème? } ``` --- ## Dangling pointer ("pointeur pendouillant") ```rust compile_fail fn dangling() -> &Vec
{ // la fonction retourne une référence vers un pointeur let mut v = Vec::new(); v.push(1); v.push(2); // on a créé un vec avec 1,2 dedans. &v; // on retourne une réf vers v } // v sort de la portée de la fonction et est détruit: // la mémoire est libérée. fn main() { let dangling_reference = dangling(); } ``` - Mémoire désallouée et le pointeur vers cette mémoire: **dangling pointer**. - Tout un tas de langages autorise ce comportement indéfini. --- ## Exemples (1/2) ```rust fn main() { let mut y = Vec::new(); y.push(1); y.push(2); y.push(3); y.push(4); let y1 = &y; let y2 = &y; println!("La valeur de y1 et y2 sont: {:?}, {:?}.", y1, y2); } ``` --- ## Exemples (2/2) ```rust fn main() { let mut y = Vec::new(); y.push(1); y.push(2); y.push(3); y.push(4); { // optionnel, le compilo est smart let mut y1 = &mut y; y1.push(7); } println!("La valeur de y est: {:?}.", y); } ``` --- ## Déréférencement ```rust [4,5|4,6|] fn main() { let mut y = Vec::new(); y.push(1); y.push(2); y.push(3); y.push(4); let z = &y; println!("La valeur de z est: {:?}.", z); println!("La valeur de z est: {:?}.", *z); } ```