Discussion du code propriete

Dans cette partie nous discutons de ce code.

Concepts

Les concepts abordés dans cet exemple sont:

  1. Bind et move.
  2. Ownership et borrowing.

Discussion

Bind et move

On l'a déjà brièvement mentionné dans la partie 2 l'instruction

#![allow(unused)]
fn main() {
let a = 2;
}

fait plusieurs choses:

  1. Elle alloue l'espace pour un entier 32 bits.
  2. Initialise la mémoire avec la valeur 2.
  3. Lie (bind en anglais) la variable (l'identifiant) a avec cet espace mémoire.

Quand on sort de la portée (scope en anglais) la variable est plus accessible et la mémoire est automatiquement libérée (l'espace mémoire où est contenue la valeur 2 est libéré automatiquement). Cela se passe aussi en C, sauf quand on a fait un malloc() explicite.

Si on écrit le code un peu plus compliqué

enum SomethingOrNothing<T> {
    Nothing,
    Something(T),
}
impl<T> SomethingOrNothing<T> {
    fn new(val: T) -> SomethingOrNothing<T> {
        SomethingOrNothing::Something(val)
    }
}
fn main() {
    let a = SomethingOrNothing::new(1);
    let _b = a;
    let _c = a;
}

Ici ce code ne compile pas, car nous ne pouvons lier une valeur qu'à une seule variable par défaut. On dit que les données ne peuvent avoir qu'un seul propriétaire (owner en anglais). Ainsi, quand on fait

let a = SomethingOrNothing::new(1);
let _b = a;

on donne la propriété de la valeur SomethingOrNothing::new(1) à la variable a. Puis, quand on transfère la propriété de la valeur SomethingOrNothing::new(1) à la variable _b. Ainsi, a n'est plus lié à SomethingOrNothing::new(1) et ne peut plus rien transférer comme données. Donc quand on fait

let _c = a;

le compilateur refuse de nous laisser faire, car les données liées à a ont déjà été déplacées (moved en anglais) dans _b.

Pour certains types, comme les i32 (ou tous les types qui implémentent le trait Copy) une copie implicite est faite, et on ne déplace ainsi pas la propriété de la valeur mais on en crée une nouvelle qui est liée aux variables.

Cette règle est très importante et a des implications très fortes en Rust (c'est aussi le cas dans tous les langages mais ça passe un peu plus inaperçu, car le compilateur ne se plaint pas). On dit que Rust empêche l'aliasing. On retrouve cette difficulté quand on passe un argument à une fonction. Si le type de la variable passé en argument n'est pas copiable, la propriété de la valeur est passée à l'argument de la fonction. Ainsi au moment de quitter la fonction la valeur est détruite et la mémoire liée à la valeur libérée comme dans le code suivant

enum SomethingOrNothing<T> {
    Nothing,
    Something(T),
}
impl<T> SomethingOrNothing<T> {
    fn new(val: T) -> SomethingOrNothing<T> {
        SomethingOrNothing::Something(val)
    }
}
fn print<T: std::fmt::Display>(val: SomethingOrNothing<T>) {
    match val {
        SomethingOrNothing::Nothing => println!("Nothing."),
        SomethingOrNothing::Something(val) => println!("Something is: {}", val),
    }
}
fn main() {
    let a = SomethingOrNothing::new(1);
    print(a);
    print(a);
}

Ici lors du premier appel à print(a) la propriété de la valeur SomethingOrNothing::new(1) est transférée à l'argument val de la fonction print(val). Lorsqu'on sort de la portée de la fonction la variable val est détruite et toutes les données qui vont avec. Comme la variable a n'est plus propriétaire d'aucune donnée, le second appel à print(a) échoue à la compilation.

Une solution est d'implémenter le trait Copy pour SomethingOrNothing<T>, mais bien que cela puisse être acceptable pour un petit type comme celui-ci, en général le coût de performance serait énorme on devait répéter des copies qui prennent beaucoup de place en mémoire.

Mais alors comment faire pour afficher un SomethingOrNothing<T> sans implémenter le trait Copy?

Ownership et borrowing

Le Rust nous offre un moyen d'emprunter (borrow) les variables pour éviter d'avoir à faire des copies dans tous les sens ou de devoir toujours retourner les variables passées en argument.

enum SomethingOrNothing<T> {
    Nothing,
    Something(T),
}
impl<T> SomethingOrNothing<T> {
    fn new(val: T) -> SomethingOrNothing<T> {
        SomethingOrNothing::Something(val)
    }
}
fn print<T: std::fmt::Display>(val: &SomethingOrNothing<T>) {
    match val {
        SomethingOrNothing::Nothing => println!("Nothing."),
        SomethingOrNothing::Something(val) => println!("Something is: {}", val),
    }
}
fn main() {
    let a = SomethingOrNothing::new(1);
    print(&a);
    print(&a);
}

Dans ce code on voit que la fonction print a la signature suivante

fn print<T: std::fmt::Display>(val: &SomethingOrNothing<T>)

et le type de val est une référence vers un SomethingOrNothing<T> (il faut noter le & avant le type). La référence en Rust est un moyen d'emprunter la propriété d'une valeur. Ainsi quand on va sortir de la portée de la fonction, la variable val, qui est une référence va être détruite, mais les données qui sont liées à cette référence ne le seront pas. C'est pour cela que contrairement à ce qui se passe dans la section Bind et move on peut appeler deux fois la fonction print(&val). Il faut bien noter que la référence a la syntaxe &<variable> comme en C.

Exercice

Corrigez ce code pour qu'il compile (sans enlever de lignes évidemment)

enum SomethingOrNothing<T> {
    Nothing,
    Something(T),
}
impl<T> SomethingOrNothing<T> {
    fn new(val: T) -> SomethingOrNothing<T> {
        SomethingOrNothing::Something(val)
    }
}
fn main() {
    let a = SomethingOrNothing::new(1);
    {
        let b = a;
        // imagine that some thing may happen here with b
    }
    let c = a;
}

On constate dans notre code qu'il y a assez peu de modifications pour éviter les copies implicites et passer par des emprunts. Pour résumer, on a changé la signature de trois fonctions: print(), print_tab(), et find_min().

La fonction print() a déjà été discutée plus haut, et nécessite simplement l'ajout d'une référence vers un SomethingOrNothing<T> pour que la valeur soit prêtée à val dans la fonction

fn print<T: std::fmt::Display>(val: &SomethingOrNothing<T>) {
    match val {
        SomethingOrNothing::Nothing => println!("Nothing."),
        SomethingOrNothing::Something(val) => println!("Something is: {}", val),
    }
}

Ainsi, les valeurs de SomethingOrNothing<T> ne sont pas détruites à l'appel de print().

La fonction print_tab(), de façon similaire, nécessite le passage de la référence vers le tableau statique &[T; SIZE]

fn print_tab<T: std::fmt::Display>(tab: &[T; SIZE]) {
    for t in tab {
        print!("{} ", t);
    }
    println!();
}

La boucle for nécessite une discussion plus approfondie. En effet, dans cette fonction tab est une référence vers un tableau. Ainsi, on ne peut pas itérer sur ses éléments de façon standard. En effet, cela nécessiterait de "move" ou transférer la propriété de la valeur sur laquelle on itère.

Le compilateur de Rust ne l'autoriserait jamais. Le langage nous facilite donc la vie en itérant sur les références des valeurs contenues dans le tableau (dont le type est générique, rappelons le). Donc si on annotait explicitement le type de t dans la boucle for on aurait:

for t: &T in tab

On constate aussi que le langage devine qu'on ne veut pas afficher la valeur de la référence (son adresse mémoire) mais affiche directement la valeur elle même quand on appelle la macro print!()

print!("{} ", t);

Finalement, nous avons également modifié la fonction find_min().

fn find_min<T: Minimum>(tab: &[T; SIZE]) -> SomethingOrNothing<T> {
    let mut current_minimum = SomethingOrNothing::Nothing;
    // Here is T is not Copyable tab is consumed and cannot be returned
    for t in tab {
        current_minimum = current_minimum.min(SomethingOrNothing::new(*t));
    }
    current_minimum
}

Outre l'argument qui est une référence à un tableau statique comme dans les deux autres fonctions discutées précédemment, on a une nouvelle syntaxe dans la boucle for

for t in tab {
    current_minimum = current_minimum.min(SomethingOrNothing::new(*t));
}

On constante qu'on doit déréférencer t (appeler *t), car t est une référence vers T (il est de type &T), car la fonction new() s'attend à un T et non à un &T. L'opérateur *<variable> est l'opérateur de déréférencement et cette syntaxe est la même que celle du C. Si par malheur on oubliait le *t on aurait une erreur de compilation, car le vérificateur de type du Rust ne laisse pas passer ce genre de choses.

Le code


/*!
propriete illustrates the concepts of **Ownership** and **Borrowing**. It also
presents the manual implementation of [Clone] and [Copy].
*/
enum SomethingOrNothing<T> {
    Nothing,
    Something(T),
}
impl<T> SomethingOrNothing<T> {
    fn new(val: T) -> SomethingOrNothing<T> {
        SomethingOrNothing::Something(val)
    }
}

fn print<T: std::fmt::Display>(val: &SomethingOrNothing<T>) {
    match val {
        SomethingOrNothing::Nothing => println!("Nothing."),
        SomethingOrNothing::Something(val) => println!("Something is: {}", val),
    }
}

// Manual implementation of [Clone]
impl<T: Clone> Clone for SomethingOrNothing<T> {
    fn clone(&self) -> Self {
        match self {
            SomethingOrNothing::Nothing => SomethingOrNothing::Nothing,
            SomethingOrNothing::Something(val) => SomethingOrNothing::Something(val.clone()),
        }
    }
}

// Manual implementation of [Copy]
impl<T: Copy> Copy for SomethingOrNothing<T> {}

// If we remove Copy, we have a problem with the t in tab
// in the computation of the minimum.
trait Minimum: Copy {
    fn min(self, rhs: Self) -> Self;
}

impl<T: Minimum> Minimum for SomethingOrNothing<T> {
    fn min(self, rhs: Self) -> Self {
        match (self, rhs) {
            (SomethingOrNothing::Nothing, SomethingOrNothing::Nothing) => {
                SomethingOrNothing::Nothing
            }
            (SomethingOrNothing::Something(lhs), SomethingOrNothing::Something(rhs)) => {
                SomethingOrNothing::Something(lhs.min(rhs))
            }
            (SomethingOrNothing::Nothing, SomethingOrNothing::Something(rhs)) => {
                SomethingOrNothing::Something(rhs)
            }
            (SomethingOrNothing::Something(lhs), SomethingOrNothing::Nothing) => {
                SomethingOrNothing::Something(lhs)
            }
        }
    }
}

impl Minimum for i32 {
    fn min(self, rhs: Self) -> Self {
        if self < rhs {
            self
        } else {
            rhs
        }
    }
}

const SIZE: usize = 9;

// Poorly emulates the parsing of a command line.
fn read_command_line() -> [i32; SIZE] {
    [10, 32, 12, 43, 52, 53, 83, 2, 9]
}

// Prints all the elements of the `tab`.
// Tab is borrowed here
fn print_tab<T: std::fmt::Display>(tab: &[T; SIZE]) {
    for t in tab {
        print!("{} ", t);
    }
    println!();
}

// Computes the minimum of a borrowed Array of a type T which implements the [Minimum] trait.
// Returns a [SomethingOrNothing::Something] containing the the minimum value
// or [SomethingOrNothing::Nothing] if no minimum value was found.
fn find_min<T: Minimum>(tab: &[T; SIZE]) -> SomethingOrNothing<T> {
    let mut current_minimum = SomethingOrNothing::Nothing;
    // Here is T is not Copyable tab is consumed and cannot be returned
    for t in tab {
        current_minimum = current_minimum.min(SomethingOrNothing::new(*t));
    }
    current_minimum
}

fn main() {
    let tab = read_command_line();
    println!("Among the Somethings in the list:");
    print_tab(&tab);
    let min = find_min(&tab);
    print(&min);
}

Rustlings

Les rustlings à faire dans ce chapitre sont les suivants:

La move semantic

$ rustlings run move_semantics1
$ rustlings run move_semantics2
$ rustlings run move_semantics3
$ rustlings run move_semantics4
$ rustlings run move_semantics5
$ rustlings run move_semantics6