Rust

Any donation is very welcome
Fork me on GitHub

II. Spécificités de Rust

5. Propriété (ou ownership)

Jusqu'à présent, de temps à autres, on utilisait le caractère '&' devant des paramètres de fonctions sans que je vous explique à quoi ça servait. Exemple :

Runfn ajouter_valeur(v: &mut Vec<i32>, valeur: i32) {
    v.push(valeur);
}

struct X {
    v: i32,
}

impl X {
    fn addition(&self, a: i32) -> i32 {
        self.v + a
    }
}

Il s'agit de variables passées par référence. En Rust, cela a une grande importance. Il faut savoir que chaque variable ne peut avoir qu'un seul "propriétaire" à la fois, ce qui est l'une des grandes forces de ce langage. Par exemple :

Runfn une_fonction(v: Vec<i32>) {
    // le contenu n'a pas d'importance
}

let v = vec![5, 12];

une_fonction(v);
println!("{}", v[0]); // error ! "use of moved value"

Un autre exemple encore plus simple :

Runlet original = vec![1, 2, 3];
let non_original = original;

println!("original[0] is: {}", original[0]); // même erreur

"Mais c'est complètement idiot ! Comment on fait pour modifier la variable depuis plusieurs endroits ?!"

C'est justement pour éviter ça que ce système d'ownership (propriété donc) existe. C'est ce qui vous posera sans aucun doute le plus de problème quand vous développerez vos premiers programmes.

Dans un chapitre précédent, je vous ai parlé des traits. Hé bien sachez que l'un d'entre eux s'appelle Copy et permet de copier (sans rire !) un type sans en devenir le propriétaire. Tous les types de "base" (aussi appelés primitifs) (i8, i16, i32, isize, f32, etc...) l'implémentent. Ce code est donc tout à fait valide :

Runlet original: i32 = 8;
let copy = original;

println!("{}", original);

Il est cependant possible de "contourner" ce problème de copie de la manière suivante :

Runfn fonction(v: Vec<i32>) -> Vec<i32> {
    v // on "rend" la propriété de l'objet en le renvoyant
}

fn main() {
    let v = vec![5, 12];

    let v = fonction(v); // et on la re-récupère ici
    println!("{}", v[0]);
}

Bof, n'est-ce pas ? Et encore c'est un code simple. Imaginez quelque chose comme ça :

Runfn fonction(v1: Vec<i32>, v2: Vec<i32>, v3: Vec<i32>, v4: Vec<i32>) -> (Vec<i32>, Vec<i32>, Vec<i32>, Vec<i32>) {
    (v1, v2, v3, v4)
}

let v1 = vec![5, 12, 3];
let v2 = vec![5, 12, 3];
let v3 = vec![5, 12, 3];
let v4 = vec![5, 12, 3];

let (v1, v2, v3, v4) = fonction(v1, v2, v3, v4);

Ça devient difficile de suivre, hein ? Vous l'aurez donc compris, ce n'est pas du tout une bonne idée.

"Mais alors comment on fait ? On implémente le trait Copy sur tous les types ?"

Non, et heureusement ! La copie de certains types pourrait avoir un lourd impact sur les performances de votre programme, tandis que d'autres ne peuvent tout simplement pas être copiés ! C'est ici que les références rentrent en jeu.

Jusqu'à présent, vous vous en êtes servies sans que je vous explique à quoi elles servaient. Je pense que maintenant vous vous en doutez. Ajoutons une référence à notre premier exemple :

Runfn une_fonction(v: &Vec<i32>) {
    // le contenu n'a pas d'importance
}

let v = vec![5, 12];

une_fonction(&v);
println!("{}", v[0]); // Pas de souci !

On peut donc dire que les références permettent d'emprunter une variable sans en prendre la propriété, et c'est très important de s'en souvenir !

Tout comme les variables, les références aussi peuvent être mutables. "&" signifie référence constante et "&mut" signifie référence mutable. Il y a cependant plusieurs choses à savoir :

  • Une référence ne doit pas "vivre" plus longtemps que la variable qu'elle référence.
  • On peut avoir autant de référence constante que l'on veut sur une variable.
  • On ne peut avoir qu'une seule référence mutable sur une variable.
  • On ne peut avoir une référence mutable que sur une variable mutable.
  • On ne peut avoir une référence constante et une référence mutable en même temps sur une variable.

Pour bien comprendre cela, il faut bien avoir en tête comment la durée de vie d'une variable fonctionne :

Runfn f() {
    let mut v = 10i32; // on crée une variable

    v += 12; // on fait des opérations dessus
    v *= 2;
    // ...

    // quand on sort de la fonction, v n'existe plus
}

fn main() {
    let v: i32 = 12; // cette variable n'a rien à voir avec celle dans la fonction f
    let v2: f32 = 0;

    f();
    // on quitte la fonction, v et v2 n'existent plus    
}

Ainsi, ce code devient invalide :

Runfn main() {
    let reference: &i32;
    let x = 5;
    reference = &x;

    println!("{}", reference);
}

Ici, le compilateur vous dira que la variable x ne vit pas assez longtemps. x ayant été déclarée après reference, elle est donc détruite en premier, rendant reference invalide ! Pour pallier à ce problème, rien de bien compliqué :

Runfn main() {
    let x = 5;
    let reference: &i32 = &x;

    println!("{}", reference);
}

Maintenant vous savez ce qui se cache derrière les références et vous avez des notions concernant la durée de vie des variables. Il est temps de voir ce deuxième point un peu plus en détail.