Discussion du code propriete
Dans cette partie nous discutons de ce code.
Concepts
Les concepts abordés dans cet exemple sont:
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:
- Elle alloue l'espace pour un entier 32 bits.
- Initialise la mémoire avec la valeur
2
. - 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