--- lang: fr-fr --- # Formation Rust ![Ferris the Crab](https://rustacean.net/assets/rustacean-orig-noshadow.png =250x) [Rust](https://www.rust-lang.org) est un langage de programmation jeune (v1.0 en mai 2015), moderne, sûr et performant initialement créé chez Mozilla. Il permet à tous de faire de la programmation performante bas niveau, sans avoir à gérer sa mémoire manuellement (comme en Python) et sans faire de concessions sur la vitesse et la sécurité des programmes (comme en C). ## Installation (promis c'est rapide) Suivez les instructions de la [page d'installation](https://www.rust-lang.org/fr/tools/install) officielle. * Pas de dépendances à installer sous WSL, Ubuntu ou MacOS. * Sous Windows, on vous demande d'installer en plus le compilateur C/C++ de Microsoft: je vous recommande donc pour cette fois de faire l'installation sous WSL pour gagner du temps et ne pas y passer trop longtemps. ## Hello, world! Lancez une invite de commande, et exécutez ```bash cargo new formation ``` Cette commande crée un dossier de projet en utilisant `cargo`, le gestionnaire de packages officiel de Rust. Ouvrez Visual Studio Code dans le dossier créé : ```bash cd formation code . ``` > Conseil: installez l'extension "Rust Analyzer" pour avoir des fonctionnalités spécifiques à Rust dans VS Code On observe alors la structure suivante: ``` ├─ src │ └─ main.rs └─ Cargo.toml ``` Executez `cargo run`: le code contenu dans `main.rs` est compilé et s'exécute. ```rust fn main() { println!("Hello, world!"); } ``` (Ici, `println` n'est pas une fonction, mais une macro, d'où le point d'exclamation à la fin pour différencier un appel de macro d'un appel de fonction.) Les commandes cargo utiles sont: * `cargo run` pour exécuter votre code. * `cargo build` pour compiler votre code. * `cargo check` pour vérifier que votre code compile, mais sans le compiler réellement: plus rapide. * `cargo fmt` pour formatter votre code avec `rustfmt`. ## Syntaxe de base ### Variables On déclare des variables avec `let`: ```rust let answer = 42; ``` Les variables sont immutables par défaut, on les déclare mutables avec le mot `mut`: ```rust let mut x = 3; x = x + 7; ``` Toutes les variables en Rust ont un type fixe. La plupart du temps, vous n'aurez pas besoin de le préciser, le compilateur est assez intelligent pour le faire pour vous. Pour préciser le type d'une variable au moment de la déclaration, on utilise: ```rust let x: i32 = 5; ``` `i32` désigne ici un entier (signé) de longueur 32 bits. On verra d'autres types plus tard dans la formation, comme les types chaine de caractères (`&str` et `String`) ou les tableaux (`Vec<_>`). ### Fonctions Une fonction est dénotée par le mot `fn`: ```rust fn hello(name: &str) { println!("Hello, {name}!"); } fn main() { // Affiche "Hello, DaTA!" hello("DaTA"); } ``` Une fonction qui retourne une valeur doit toujours préciser son type: ```rust fn five() -> i32 { return 5; } ``` En Rust, tout est une expression. Par exemple: ```rust let x = 2; let y = if x == 2 { 5 } else { 3 }; ``` On assigne ici à `y` la valeur de l'expression `if x == 2 { 5 } else { 3 }`. Une fonction retourne par défaut la valeur de sa dernière expression : ```rust fn five(x: i32) -> i32 { if x == 2 { 5 } else { 3 } } ``` La plupart du temps, le mot `return` est donc omis. ### Conditions Pas de surprise ici: ```rust let x = 5; if x > 3 { println!("Plus grand que 3"); } else { println!("Plus petit ou égal à 3"); } ``` ### Boucles ```rust // Boucle for avec un nombre const N: i32 = 10; // déclaration d'une constante for i in 0..N { println!("Itération: {i}"); } // Boucle for dans un tableau let v: Vec<&str> = vec!["parcourir", "une", "liste"]; for s in v { println!("{s}"); } // Boucle while simple let mut x = 1; while x < 1024 { println!("{x}"); x *= 2; } ``` :::info **Exercice:** FizzBuzz ! Ecrivez une fonction qui prend en entrée un entier n, et qui pour les entiers de 1 à n affiche: * si l'entier est divisible par 3: "Fizz" * si l'entier est divisible par 5: "Buzz" * si l'entier est divisible par 3 et 5: "FizzBuzz" * sinon, simplement l'entier lui même. ::: ## Sémantique de Rust ### Move semantics et références Vous connaissez les pointeurs en C, l'équivalent Rust sont les références. Mais pas de panique ! Ici, pas de gestion manuelle de la mémoire, pas de `Segmentation Fault` parce qu'on a mal géré les sorties d'une fonction ou la taille d'un tableau : le compilateur `rustc` nous empêche de compiler un programme qui peut avoir des erreurs de mémoire. Ce comportement magique n'est pas gratuit, cependant. Rust impose des règles un peu différentes et un peu plus strictes que la plupart des langages. #### A retenir: * Toute valeur a un unique 'propriétaire', aka un `Owner`. * On peut déplacer une valeur d'un `Owner` vers un autre (c'est ce qu'on appelle un `move`); auquel cas l'ancien `Owner` ne peut plus accéder à la valeur. * Un `Owner` peut 'prêter' la valeur à une fonction ou une autre variable: on dit alors que cette valeur est 'empruntée', aka `borrowed`. * On peut avoir SOIT un borrow mutable SOIT plusieurs borrows immutables: * Cela permet d'empêcher à une valeur d'être modifiée par quelqu'un en même temps qu'elle est lue par quelqu'un d'autre, ce qui mène très souvent à des bugs (surtout dans un contexte de programmation parallèle). Le compilateur Rust peut, grâce à ces règles, gérer votre mémoire à votre place: dites adieu aux `malloc` et `free` et aux memory leaks. ### En pratique Définissons un type d'exemple: ```rust struct Vec2D { x: i32, y: i32 } ``` Pour le visualiser facilement, on ajoute au dessus une annotation: ```rust #[derive(Debug)] struct Vec2D { x: i32, y: i32 } ``` On peut maintenant afficher un vecteur 2D en mode "debug": ```rust let x = Vec2D {x: 1, y: 2}; println!("{x:?}"); ``` A présent, expérimentons un peu. ```rust fn print_vec(v: &Vec2D) { println!("{v:?}"); } fn update_vec(v: &mut Vec2D) { (v.x, v.y) = (v.y, v.x); } ``` La fonction `print_vec` fait un **borrow immutable** de la valeur passée en argument. La fonction `update_vec` fait un **borrow mutable** de la valeur en argument. :::info **Exercice:** Essayez de compiler les codes suivants un par un dans votre fonction `main`: ```rust let x = Vec2D { x: 1, y: 2 }; let y = x; print_vec(&x); // l'accès à x est invalide // sa valeur a été déplacée dans y. ``` Autre test: ```rust let x = Vec2D { x: 1, y: 2 }; let y = &x; // on prend une référence immutable à x print_vec(y); print_vec(&x); // on peut avoir plusieurs références immutables à x ``` A présent, on va y rajouter une ligne juste avant d'utiliser `y`: ```rust let mut x = Vec2D { x: 1, y: 2 }; let y = &x; update_vec(&mut x); // on modifie x avec une référence mutable print_vec(y); ``` Essayez de faire `cargo run` et regardez le message d'erreur qui s'affiche ! On ne peut pas avoir une référence immutable en même temps qu'une référence mutable. Essayons de rendre la référence `y` mutable: ```rust let mut x = Vec2D { x: 1, y: 2 }; let y = &mut x; // on rend la référence y mutable update_vec(&mut x); print_vec(y); ``` Regardez le nouveau message d'erreur à la compilation : on ne peut pas avoir plusieurs références mutables en même temps ! ::: Ce mécanisme de Rust peut paraître inutilement compliqué au premier abord, mais sauve des vies dès que les projets commencent à devenir un peu compliqué. Et pas de panique: les types entiers (i32, u8, i64, etc) sont des types `Copy`, c'est à dire que faire un `move` de ces types ne fait que les copier, sans empecher d'accéder à l'ancien `Owner`. Par exemple : ```rust let x = 5; let y = x; // move de x println!("{x}"); // l'accès à x est toujours valide. ``` ### `impl`, méthodes Reprenons notre type `Vec2D` défini plus haut. On aimerait pouvoir faire des opérations dessus. On peut par exemple définir un constructeur ou une méthode pour obtenir la valeur x: ```rust impl Vec2D { fn x(&self) -> i32 { self.x } } ``` Cette définition signifie que la méthode `x` prend en argument une référence au type sur laquelle on l'implémente. Le mot clé `self` désigne la valeur sur laquelle on appelle la méthode. On peut donc avoir `self` (on prend l'ownership de la valeur), `&self` (on prend une référence), ou `&mut self` (on prend une référence mutable). En un certain sens, elle est équivalente à : ```rust fn x(v: &Vec2D) -> i32 { v.x } ``` A la différence que la première version peut être appelée comme suit : ```rust let v = Vec2D { x: 6, y: 9 }; let v_x = v.x(); // v_x contient 6 ``` Un peu comme dans les langages orientés objet, cela permet de chainer des méthodes entre elles, notamment avec les itérateurs, qu'on verra plus tard dans la formation. :::info **Exercice:** ecrivez une méthode `scale` qui multiplie les deux composantes du vecteur par un entier donné en argument. ::: ### Traits et généricité On aimerait aussi pouvoir obtenir des copies d'un vecteur: implémentons donc le trait `Clone`. Mais qu'est-ce qu'un trait? Un trait (ou interface dans d'autres langages) est un ensemble de comportements partagés entre plusieurs types. Par exemple, le trait `Clone`, un des plus utilisés, est défini par la librairie standard: ```rust trait Clone { fn clone(&self) -> Self; } ``` ![](https://markdown.data-ensta.fr/uploads/upload_2e427d156fdfb81d0ebc1f2aa6a45ca3.png =300x) Cette définition signifie que tout type qui implémente le trait `Clone` dispose d'une méthode `clone`, qui prend en paramètre une référence et renvoie une copie de la valeur originale. Un trait peut contenir des déclarations de fonction, de type associés, ainsi que des constantes. Implémentons `Clone` pour `Vec2D` : ```rust impl Clone for Vec2D { fn clone(&self) -> Self { // on peut écrire soit Self, un raccourci pour le type utilisé, soit Vec2D directement. Vec2D { x: self.x, y: self.y } } } ``` Les traits nous permettent de définir des comportements partagés, et donc d'implémenter des fonctions qui prennent plusieurs types d'arguments ! Par exemple: ```rust fn cloner_deux_fois<T: Clone>(x: &T) -> (T, T) { (x.clone(), x.clone()) } ``` Cela permet aussi d'avoir des types génériques, comme `Vec<T>`, qui est un tableau d'un type générique ; `Vec<T>` ne peut être cloné que si `T: Clone`. :::info **Exercice:** Implémentez le trait `Add`, défini comme suit, sur notre type `Vec2D`: ```rust pub trait Add { type Output; fn add(self, rhs: Self) -> Self::Output; } ``` Vous aurez besoin au préalable d'importer `Add` depuis `std::ops` (un module de la librairie standard), comme suit: ```rust use std::ops::Add; ``` Une fois ce trait implémenté, définissez une fonction générique `add_to_vec` avec la signature suivante, qui ajoute un élément `x` à tous les éléments d'un tableau `vec`: ```rust fn add_to_vec<T: Add + Clone>(vec: &[T], x: T) -> Vec<T::Output> ``` N'hésitez pas à vous aider de la [documentation de la librairie standard](https://doc.rust-lang.org/std) ! Enfin, essayez votre fonction : ```rust let v = vec![ Vec2D { x: 1, y: 2 }, Vec2D { x: 3, y: 4 }, Vec2D { x: 5, y: 6 } ]; let x = Vec2D { x: -1, y: 3 }; dbg!(add_to_vec(&v, x)); ``` ::: ### Enum, pattern matching Les `struct` sont un exemple d'un type "produit" (au sens ensembliste). Rust possède également des types "somme", appelés `enum`. Au lieu d'avoir une valeur de chaque champ, comme un `struct`, ils prennent une valeur parmi un nombre prédéfini de types. Par exemple: ```rust enum Objet { Nombre(i32), Point(Vec2D), Segment(Vec2D, Vec2D), } use Objet::*; let obj = Nombre(4); match obj { Nombre(n) => println!("obj est le nombre {n}"), Point(p) => println!("obj est le point ({}, {})", p.x, p.y), Segment(a, b) => println!( "obj est le segment qui va de ({}, {}) à ({}, {})", a.x, a.y, b.x, b.y ), } ``` On introduit ici le pattern matching, avec le mot `match`, qui permet de différencier entre les différents types d'un `enum`, mais aussi de destructurer des objets de toute sorte : ```rust fn split_first<T>(v: &[T]) -> (&T, &[T]) { match v { [first, rest @ ..] => (first, rest), [] => panic!("Not enough elements!"), } } ``` Quelques enums très utiles de la librairie standard : ```rust // Permet de signifier la présence ou non d'une valeur enum Option<T> { None, // rien Some(T), // quelque chose } // Le résultat d'une fonction qui peut émettre une erreur enum Result<T, E> { Ok(T), Err(E) } ``` **Voilà! Ca devrait suffire à vous lancer sur la suite: un mini projet guidé en Rust, pour montrer les capacités et les forces du langage. Ce sera moins compliqué ensuite.** ## Mini projet de formation