diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 17b6abb350430e8ffb0986b5faf19cb6a93ca26e..e3ee109776442d822fe11371f8181ec9f7d553e0 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -6,5 +6,4 @@ - [Part 01](./part01.md) - [Part 02](./part02.md) - [Part 03](./part03.md) - - +- [Part 04](./part04.md) diff --git a/book/src/part04.md b/book/src/part04.md new file mode 100644 index 0000000000000000000000000000000000000000..e9184b7641a624e8a5809be5be6a9858dc061c1b --- /dev/null +++ b/book/src/part04.md @@ -0,0 +1,190 @@ +# Discussion du code `part04` + +Dans cette partie nous discutons de [ce code](#le-code). + +## Concepts + +Les concepts abordés dans cet exemple sont: + +1. [Bind et move.](#bind-et-move) +2. [Ownership et borrowing.](#ownership-et-borrowing) + +## Discussion + +### Bind et move + +On l'a déjà brièvement mentionné dans la [partie 2](part02.md) l'instruction + +```rust,no_run +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é +```rust,compile_fail +{{#include ../../codes/rust_lang/part04/src/main.rs:something_or_nothing}} +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 +```rust,ignore +let a = SomethingOrNothing::new(1); +let _b = a; +``` +on donne la propriété de la valeur `SomethingOrNothing::new(1)` à la variable `a`. Puis, quand 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 + +```rust,ignore +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 +```rust,compile_fail +{{#include ../../codes/rust_lang/part04/src/main.rs:something_or_nothing}} +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. + +```rust +{{#include ../../codes/rust_lang/part04/src/main.rs:something_or_nothing}} +{{#include ../../codes/rust_lang/part04/src/main.rs:print}} +fn main() { + let a = SomethingOrNothing::new(1); + print(&a); + print(&a); +} +``` + +Dans ce code on voit que la fonction `print` a la signature suivante +```rust,ignore +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](#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) + +```rust,editable,compile_fail +{{#include ../../codes/rust_lang/part04/src/main.rs:something_or_nothing}} +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 +```rust,ignore +{{#include ../../codes/rust_lang/part04/src/main.rs:print}} +``` +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]` + +```rust,ignore +{{#include ../../codes/rust_lang/part04/src/main.rs:print_tab}} +``` + +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: +```rust,ignore +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!()` +```rust,ignore +print!("{} ", t); +``` + +Finalement, nous avons également modifié la fonction `find_min()`. +```rust,ignore +{{#include ../../codes/rust_lang/part04/src/main.rs:find_min}} +``` +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` +```rust,ignore +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 + +```rust +{{#rustdoc_include ../../codes/rust_lang/part04/src/main.rs:main}} +``` + diff --git a/codes/rust_lang/part04/src/main.rs b/codes/rust_lang/part04/src/main.rs index 08d81e3bd4c5714ca811ecdaf24cff439bea1bc0..551847bdd0f249696f219da2dc3c298f1518caa1 100644 --- a/codes/rust_lang/part04/src/main.rs +++ b/codes/rust_lang/part04/src/main.rs @@ -1,34 +1,29 @@ +/* ANCHOR: all */ + /*! Part04 illustrates the concepts of **Ownership** and **Borrowing**. It also presents the manual implementation of [Clone] and [Copy]. */ - -// Rust basics: -// - Ownership -// - Borrowing -// - Clone -// - Copy - -// An generic enumerated type that has two variants. -// -// - Nothing -// - Something +// ANCHOR: something_or_nothing enum SomethingOrNothing<T> { - // A [SomethingOrNothing::Nothing] Nothing, - // A [SomethingOrNothing::Something] encapsulating a T Something(T), } +impl<T> SomethingOrNothing<T> { + fn new(val: T) -> SomethingOrNothing<T> { + SomethingOrNothing::Something(val) + } +} +// ANCHOR_END: something_or_nothing -impl<T: std::fmt::Display> SomethingOrNothing<T> { - // A static function that prints the content of a SomethingOrNothing. - fn print(&self) { - match self { - SomethingOrNothing::Nothing => println!("Nothing."), - SomethingOrNothing::Something(val) => println!("Something is: {}", val), - } +// ANCHOR: print +fn print<T: std::fmt::Display>(val: &SomethingOrNothing<T>) { + match val { + SomethingOrNothing::Nothing => println!("Nothing."), + SomethingOrNothing::Something(val) => println!("Something is: {}", val), } } +// ANCHOR_END: print // Manual implementation of [Clone] impl<T: Clone> Clone for SomethingOrNothing<T> { @@ -45,10 +40,13 @@ 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. +// ANCHOR: minimum trait Minimum: Copy { fn min(self, rhs: Self) -> Self; } +// ANCHOR_END: minimum +// ANCHOR: minimum_impl impl<T: Minimum> Minimum for SomethingOrNothing<T> { fn min(self, rhs: Self) -> Self { match (self, rhs) { @@ -67,6 +65,7 @@ impl<T: Minimum> Minimum for SomethingOrNothing<T> { } } } +// ANCHOR_END: minimum_impl impl Minimum for i32 { fn min(self, rhs: Self) -> Self { @@ -87,31 +86,37 @@ fn read_command_line() -> [i32; SIZE] { // Prints all the elements of the `tab`. // Tab is borrowed here -fn print_tab(tab: &[i32; SIZE]) { +// ANCHOR: print_tab +fn print_tab<T: std::fmt::Display>(tab: &[T; SIZE]) { for t in tab { print!("{} ", t); } println!(); } +// ANCHOR_END: print_tab // 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. +// ANCHOR: find_min fn find_min<T: Minimum>(tab: &[T; SIZE]) -> SomethingOrNothing<T> { - let mut minimum = SomethingOrNothing::Nothing; - // Here is T is Copyable. Which means that t is not moved in the loop + let mut current_minimum = SomethingOrNothing::Nothing; + // Here is T is not Copyable tab is consumed and cannot be returned for t in tab { - minimum = minimum.min(SomethingOrNothing::Something(*t)); + current_minimum = current_minimum.min(SomethingOrNothing::new(*t)); } - minimum + current_minimum } +// ANCHOR_END: find_min +// ANCHOR: main fn main() { let tab = read_command_line(); println!("Among the Somethings in the list:"); print_tab(&tab); - // There are alternatives to access fields of tuples let min = find_min(&tab); - // The first field is not used therefore we can replace it with "_" - min.print(); + print(&min); } +// ANCHOR_END: main + +/* ANCHOR_END: all */