Skip to content
Snippets Groups Projects
Forked from orestis.malaspin / rust-101
2 commits behind the upstream repository.

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).
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?
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)

La représentation en mémoire du vecteur v = (1,2,3,4)

  • 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

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

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

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

fn take_own(_v: Vec<i32>) {
	// 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

fn give_own() -> Vec<i32> {
	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

fn get_len(v: Vec<i32>) -> (Vec<i32>, 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, &yest 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

fn get_len(v: &Vec<i32>) -> 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

fn get_len(v: &Vec<i32>) -> 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

fn get_len(v: &mut Vec<i32>) -> 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

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")

fn dangling() -> &Vec<i32> { // 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)

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)

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

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