Rust đŠ
Ce document est une prise de notes personnelle de mes lectures sur le Rust.
Particularités du langage
- Runtime
Le runtime de Rust est lĂ©ger car il n’a pas de pas de garbage collector et gĂšre la mĂ©moire grĂące Ă son concept d’ownership et borrowing. Rust compile directement le code en code machine et n’utilise pas de machine virtuelle pour la gestion des ressources car tout se passe Ă la compilation (par opposition Ă Java et sa JVM par exemple).
- Pas de pointeurs “classiques”
Dans les langages comme C ou C++, les pointeurs sont des variables qui contiennent l’adresse mĂ©moire d’une autre variable. Cela permet de manipuler directement la mĂ©moire, mais c’est aussi source d’erreurs si un pointeur pointe vers une zone mĂ©moire invalide ou qui a Ă©tĂ© libĂ©rĂ©e (câest ce quâon appelle une rĂ©fĂ©rence dangling ou sauvage).
Rust, pour Ă©viter cela, utilise un systĂšme de rĂ©fĂ©rences sĂ©curisĂ©es avec des rĂšgles strictes de propriĂ©tĂ© (ownership), de prĂȘt (borrowing) et de durĂ©e de vie (lifetime). PlutĂŽt que de laisser les programmeurs manipuler directement les adresses mĂ©moire comme en C ou C++, Rust utilise des rĂ©fĂ©rences qui garantissent :
- quâune rĂ©fĂ©rence ne peut pas pointer vers de la mĂ©moire libĂ©rĂ©e
- qu’une rĂ©fĂ©rence mutable ne peut ĂȘtre utilisĂ©e que par un seul emprunteur Ă la fois, pour Ă©viter les conflits
- que le compilateur vérifie toutes ces rÚgles pour garantir la sécurité de la mémoire
- Pas de valeur “nulle” par dĂ©faut
La valeur nulle (ou NULL) est une notion qui existe dans de nombreux langages pour reprĂ©senter “l’absence de valeur”. Cependant, cette valeur est une grande source de bugs car les programmeurs oublient souvent de vĂ©rifier si une valeur est “nulle” avant de l’utiliser. Si un programme essaie de manipuler une valeur qui est “nulle”, cela peut causer une erreur qui fait planter le programme (on parle de null pointer exception).
Rust a choisi de ne pas avoir de valeur null par dĂ©faut. Ă la place, il utilise un type spĂ©cial appelĂ© Option pour reprĂ©senter une valeur qui peut ĂȘtre prĂ©sente ou absente.
Enums & Match
Cette dĂ©finition de l’Ă©numĂ©ration AdresseIp indique que chacune des variantes V4 et V6 auront des valeurs associĂ©es de type diffĂ©rents :
struct Ipv4Addr {}
struct Ipv6Addr {}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
ou bien:
enum Message {
Quitter,
Deplacer { x: i32, y: i32 }, // Structure anonyme
Ecrire(String), // Tuple
}
// Il est possible d'utiliser impl avec un enum:
impl Message {
fn appeler(&self) {}
}
let m = Message::Ecrire(String::from("hello"));
m.appeler();
Comme la valeur NULL n’existe pas en Rust ce manque est compensĂ© par l’implĂ©mentation de l’enum Option<T>. De cette façon une valeur peut-ĂȘtre soit prĂ©sente soit absente.
enum Option<T> {
None,
Some(T),
}
// Exemples
let un_nombre = Some(5);
let une_chaine = Some("une chaĂźne");
let nombre_absent: Option<i32> = None; // Nous spĂ©cifions le type de la variable car Rust ne peut pas infĂ©rer le type lui-mĂȘme.
L’Option<T> permet de garantir que la manipulation d’une variable contient bien une valeur valide. Rust nous oblige a Ă©valuer chaque variante de l’enum pour pouvoir en extraire la valeur.
Un match appliquĂ© Ă un enum permettra de s’assurer que chaque variante a Ă©tĂ© gĂ©rĂ©e.
enum PieceUs {
Penny,
Nickel,
Dime,
Quarter,
}
fn valeur_en_centimes(piece: PieceUs) -> u8 {
match piece {
PieceUs::Penny => 1,
PieceUs::Nickel => 5,
PieceUs::Dime => 10,
PieceUs::Quarter => 25,
}
}
Autre exemple avec une valeur générique :
let jete_de_de = 9;
match jete_de_de {
3 => ajouter_chapeau_fantaisie(),
7 => enleve_chapeau_fantaisie(),
_ => (),
}
fn ajouter_chapeau_fantaisie() {}
fn enleve_chapeau_fantaisie() {}
NB: Le if / let ne permettra pas de couvrir toutes les variantes de l’enum :
let une_valeur_u8 = Some(3u8);
if let Some(max) = une_valeur_u8 {
println!("Le maximum est réglé sur {}", max);
}
Packages & Crates
Un package peut contenir une librairie au maximum, et plusieurs binaires. Mais au miniumum il doit contenir une librairie ou un binaire.
monprojet/ <-------------- package
Cargo.toml <---------- definition du package
src/
main.rs <--------- un crate
lib.rs (et ou) <-- un crate
// autre cas
monprojet/
Cargo.toml <--
src/
lib.rs
bin/
main1.rs <---- un crate
main2.rs <---- un autre crate
Un module rassemble des elements Ă l’intĂ©rieur d’un crate pour donner de la cohĂ©rence Ă l’orgamisation du projet :
// src/libs.rs
mod salle_a_manger {
mod accueil {
fn ajouter_a_la_liste_attente() {}
fn installer_a_une_table() {}
}
mod service {
fn prendre_commande() {}
fn servir_commande() {}
fn encaisser() {}
}
}
Ce qui donne cette arborescence :
crate // lib.rs ou main.rs parent de toute l'arborescence
âââ salle_a_manger
âââ accueil
â âââ ajouter_a_la_liste_attente
â âââ installer_a_une_table
âââ service
âââ prendre_commande
âââ servir_commande
âââ encaisser
Grùce aux modules, nous pouvons regrouper ensemble des définitions qui sont liées et donner un nom à ce lien. Ils servent aussi à gérer la visibilité des fonctionnalités.
Pour indiquer Ă Rust oĂč trouver un Ă©lĂ©ment dans l’arborescence, il a deux types de chemins :
- Les chemins absolus –>
crateou le nom d’une crate. - Les chemins relatifs –>
self,superou un indicateur Ă l’intĂ©rieur du module.
// src/lib.rs
mod salle_a_manger {
pub mod accueil { // Les modules ne servent pas uniquement à organiser votre code. Ils définissent aussi les limites de visibilité de Rust.
pub fn ajouter_a_la_liste_attente() {} // Rendre le module public ne rend pas son contenu public
}
}
pub fn manger_au_restaurant() {
// Chemin absolu
crate::salle_a_manger::accueil::ajouter_a_la_liste_attente();
// Chemin relatif
salle_a_manger::accueil::ajouter_a_la_liste_attente(); // Commencer par un nom signifie que le chemin est relatif.
// On peut utiliser directement salle_a_manger car il est dĂ©fini au mĂȘme niveau que manger_au_restaurant().
}
NB: Le module salle_a_manger n’est pas public, mais comme la fonction manger_au_restaurant est dĂ©finie dans le mĂȘme module que salle_a_manger (car manger_au_restaurant et salle_a_manger sont frĂšres), nous pouvons utiliser salle_a_manger Ă partir de manger_au_restaurant
La visibilité en Rust fait en sorte que tous les éléments (fonctions, méthodes, structures, énumérations, modules et constantes) sont privés par défaut. Les éléments dans un module parent ne peuvent pas utiliser les éléments privés dans les modules enfants, mais les éléments dans les modules enfants peuvent utiliser les éléments dans les modules parents.
Super
Nous pouvons aussi crĂ©er des chemins relatifs qui commencent Ă partir du module parent en utilisant super au dĂ©but du chemin. C’est comme dĂ©buter un chemin dans un systĂšme de fichiers avec la syntaxe ...
Pub
Nous pouvons aussi utiliser pub pour dĂ©clarer des structures et des Ă©numĂ©rations publiquement, mais il y a d’autres points Ă prendre en compte. Si nous utilisons pub avant la dĂ©finition d’une structure, nous rendons la structure publique, mais les champs de la structure restent privĂ©s. Par contre, si nous rendons publique une Ă©numĂ©ration, toutes ses variantes seront publiques.
Use
Le mot-clef use permet de dĂ©finir une importation et rend accessible dans le fichier oĂč il est appelĂ© tous les objets publics prĂ©sents dans le chemin spĂ©cifiĂ©.
// lib/src.rs
mod salle_a_manger {
pub mod accueil {
pub fn ajouter_a_la_liste_attente() {}
}
}
use (crate::)salle_a_manger::accueil::ajouter_a_la_liste_attente;
pub fn manger_au_restaurant() {
ajouter_a_la_liste_attente();
ajouter_a_la_liste_attente();
ajouter_a_la_liste_attente();
}
Dans une portĂ©e, utiliser un use s’apparente Ă crĂ©er un lien symbolique dans le systĂšme de fichier.
NB: l’import de deux types ayant le mĂȘme nom dans la mĂȘme portĂ©e nĂ©cessite d’utiliser leurs modules parents.
use std::fmt;
use std::io;
fn fonction1() -> fmt::Result {}
fn fonction2() -> io::Result<()> {}
Une autre alternative si deux types ont le mĂȘme nom est d’utiliser as.
use std::fmt::Result;
use std::io::Result as IoResult;
fn fonction1() -> Result {}
fn fonction2() -> IoResult<()> {}
Il est possible de réexporter un élément privée en public:
//src/lib.rs
mod salle_a_manger {
pub mod accueil {
pub fn ajouter_a_la_liste_attente() {}
}
}
pub use crate::salle_a_manger::accueil;
pub fn manger_au_restaurant() {
accueil::ajouter_a_la_liste_attente();
accueil::ajouter_a_la_liste_attente();
accueil::ajouter_a_la_liste_attente();
}
GrĂące Ă pub use, le code externe peut maintenant appeler la fonction ajouter_a_la_liste_attente en utilisant accueil::ajouter_a_la_liste_attente. Si nous n’avions pas utilisĂ© pub use, la fonction manger_au_restaurant aurait pu appeler accueil::ajouter_a_la_liste_attente dans sa portĂ©e, mais le code externe n’aurait pas pu profiter de ce nouveau chemin.
Exemple d’imbrication d’importations:
use std::io;
use std::io::Write;
// =
use std::io::{self, Write};
// Pour importer tous les éléments:
use std::collections::*; // NB: cette méthode est déconseillée sauf dans le cas des tests.
Collections
Une collection est un regroupement de plusieurs Ă©lĂ©ments. La taille d’une collection n’est pas connue Ă la dĂ©claration ce qui veut dire que les donnĂ©es seront stockĂ©es dans la heap car Rust ne peut pas dĂ©terminer sa taille Ă la compilation car elle dĂ©pendra de l’exĂ©cution du programme. Parmi les collections les plus usitĂ©es nous retrouvons:
Vec, liste de variables du mĂȘme type.String, collection dechar.HashMapqui permet d’associer une variable Ă une clef.
Vec
Un vecteur peut stocker n’importe quel type mais la liste doit contenir des Ă©lĂ©ments de mĂȘme type.
let mut v: Vec<i32> = Vec::new(); // Initialisation avec annotation de type
v.push(5);
NĂ©anmoins, Rust peut infĂ©rer le type lui-mĂȘme selon les instructions qui vont suivre l’initialisation. Par exemple:
let v = vec![1, 2, 3];
NB: La mémoire sera libérée lorsque le vecteur sortira de la portée.
Il existe deux façons de désigner une valeur enregistrée dans un vecteur : via les indices ou en utilisant la méthode get.
let v = vec![1, 2, 3, 4, 5];
// Cette méthode peut paniquer
let troisieme: &i32 = &v[2]; // Renvoie une référence
println!("Le troisiÚme élément est {}", troisieme);
match v.get(2) { // Renvoie une Option<&T>
Some(troisieme) => println!("Le troisiÚme élément est {}", troisieme),
None => println!("Il n'y a pas de troisiÚme élément."),
}
Lorsque le programme obtient une rĂ©fĂ©rence valide, le vĂ©rificateur d’emprunt va faire appliquer les rĂšgles de possession et d’emprunt pour s’assurer que cette rĂ©fĂ©rence ainsi que toutes les autres rĂ©fĂ©rences au contenu de ce vecteur restent valides. On ne peut pas avoir des rĂ©fĂ©rences mutables et immuables dans la mĂȘme portĂ©e.
let mut v = vec![1, 2, 3, 4, 5];
let premier = &v[0]; // <-- erreur de compilation
v.push(6); // <------------ erreur de compilation
println!("Le premier élément est : {}", premier);
Pour ajouter un Ă©lĂ©ment dans un vecteur, Rust va utilisĂ© l’espace contigue dans la mĂ©moire. Si il n’y a pas assez de place il devra rĂ©allouer l’intĂ©gralitĂ© du vecteur dans un autre espace mĂ©moire. Dans ce cas, le pointeur du premier Ă©lĂ©ment du vecter fera rĂ©fĂ©rence Ă un emplacement mĂ©moire dĂ©sallouĂ©.
Il est possible d’utiliser un Ă©num, cela permet de stocker des Ă©lĂ©ments de type diffĂ©rents dans un mĂȘme vecteur.
String
HashMap
đ