19 avril 2025

Tutoriel Rust : Guide de Démarrage

Rust suscite constamment l'intérêt des développeurs, remportant le titre de langage de programmation “le plus apprécié” dans les sondages Stack Overflow plusieurs années consécutives. Ce n'est pas seulement un engouement ; Rust offre un mélange convaincant de performance, de sécurité et de fonctionnalités de langage modernes qui résolvent les problèmes courants rencontrés dans d'autres langages de programmation système. Si vous êtes curieux de savoir ce qui rend Rust spécial et que vous souhaitez commencer votre parcours, ce guide pour débutants fournit les connaissances fondamentales pour démarrer. Nous explorerons la syntaxe de base, les concepts uniques comme la propriété, et l'outillage essentiel qui alimente l'écosystème Rust.

Pourquoi apprendre la programmation Rust ?

Rust se positionne comme un langage pour construire des logiciels fiables et efficaces. Ses principaux avantages tournent autour de la sécurité de la mémoire sans dépendre d'un ramasse-miettes et de la possibilité d'une concurrence sans crainte. Voici pourquoi vous devriez envisager d'apprendre Rust :

  1. Performance : Rust compile directement en code machine natif, offrant des performances comparables à C et C++. Il atteint cette vitesse sans sacrifier la sécurité, ce qui le rend adapté aux applications critiques en termes de performance comme les moteurs de jeux, les systèmes d'exploitation, les composants de navigateur et les services web haute performance.
  2. Sécurité de la mémoire : La caractéristique phare de Rust est son système de propriété, complété par l'emprunt et les durées de vie. Ce système garantit la sécurité de la mémoire au moment de la compilation. Oubliez les pointeurs invalides, les dépassements de tampon ou les conflits de données qui affligent souvent les langages comme C/C++. Le compilateur Rust agit comme un gardien strict, empêchant ces erreurs courantes avant même que votre code ne s'exécute.
  3. Concurrence : La programmation concurrente (exécuter plusieurs tâches de manière apparemment simultanée) est notoirement difficile à maîtriser. Rust s'attaque à ce problème de front. Les systèmes de propriété et de types travaillent ensemble pour prévenir les conflits de données au moment de la compilation, rendant l'écriture d'applications multithread significativement plus facile et plus sûre. Cette “concurrence sans crainte” permet aux développeurs d'exploiter efficacement les processeurs multi-cœurs modernes sans les pièges courants.
  4. Outillage moderne : Rust est livré avec Cargo, un gestionnaire de paquets et un outil de build exceptionnels intégrés à l'expérience de base. Cargo gère la gestion des dépendances, les tests, la construction et la publication de crates (le terme de Rust pour les paquets ou bibliothèques) de manière transparente, rationalisant l'ensemble du flux de travail de développement.
  5. Écosystème et communauté en croissance : La communauté Rust est connue pour être dynamique, active et accueillante pour les nouveaux arrivants. L'écosystème des bibliothèques (crates) s'étend rapidement, couvrant divers domaines allant du développement web (frameworks comme Actix, Rocket, Axum) et des fonctionnalités réseau (comme Tokio pour les opérations asynchrones) aux systèmes embarqués, à la science des données et aux outils en ligne de commande.

Apprendre la programmation Rust : Outils essentiels

Avant d'écrire votre première ligne de code Rust, vous devez configurer la chaîne d'outils Rust. La manière standard et recommandée d'installer Rust est d'utiliser rustup, l'installateur de la chaîne d'outils Rust.

  1. rustup : Cet outil en ligne de commande gère vos installations Rust. Il vous permet d'installer, de mettre à jour et de basculer facilement entre différentes versions de Rust (comme stable, bêta ou nightly). Visitez le site officiel de Rust (https://www.rust-lang.org/tools/install) pour les instructions d'installation spécifiques à votre système d'exploitation.
  2. rustc : C'est le compilateur Rust. Après avoir écrit votre code source Rust dans des fichiers .rs, rustc les compile en binaires exécutables ou en bibliothèques que votre ordinateur peut comprendre. Bien que crucial, vous n'invoquerez généralement pas rustc directement très souvent dans votre flux de travail quotidien.
  3. cargo : C'est le système de build et le gestionnaire de paquets de Rust, et c'est l'outil avec lequel vous interagirez le plus fréquemment. Cargo orchestre de nombreuses tâches de développement courantes :
    • Créer de nouveaux projets (cargo new).
    • Construire votre projet (cargo build).
    • Exécuter votre projet (cargo run).
    • Exécuter les tests automatisés (cargo test).
    • Gérer les dépendances (récupérer et compiler automatiquement les bibliothèques externes, ou crates, listées dans votre fichier Cargo.toml).
    • Publier vos propres crates sur crates.io (le registre central de paquets Rust) pour que d'autres puissent les utiliser.

Pour une expérimentation rapide sans nécessiter d'installation locale, les plateformes en ligne comme le Rust Playground officiel ou les environnements de développement intégrés comme Replit sont d'excellentes options.

Votre premier programme Rust : Hello, Rust!

Commençons par le programme traditionnel "Hello, world!", un rite de passage pour l'apprentissage de tout nouveau langage. Créez un fichier nommé main.rs et ajoutez le code suivant :

fn main() {
    // Cette ligne affiche du texte dans la console
    println!("Hello, Rust!");
}

Pour compiler et exécuter ce programme simple en utilisant les outils de base :

  1. Compiler : Ouvrez votre terminal ou invite de commandes, naviguez jusqu'au répertoire contenant main.rs, et exécutez :
    rustc main.rs
    
    Cette commande invoque le compilateur Rust (rustc), qui crée un fichier exécutable (par exemple, main sous Linux/macOS, main.exe sous Windows).
  2. Exécuter : Exécutez le programme compilé depuis votre terminal :
    ./main
    # Sous Windows, utilisez : .\main.exe
    

Cependant, pour tout ce qui dépasse un seul fichier, utiliser Cargo est l'approche standard et beaucoup plus pratique :

  1. Créer un nouveau projet Cargo : Dans votre terminal, exécutez :
    cargo new hello_rust
    cd hello_rust
    
    Cargo crée un nouveau répertoire nommé hello_rust contenant un sous-répertoire src avec main.rs (déjà rempli avec le code "Hello, Rust!") et un fichier de configuration nommé Cargo.toml.
  2. Exécuter avec Cargo : Exécutez simplement :
    cargo run
    
    Cargo s'occupera de l'étape de compilation puis exécutera le programme résultant, affichant “Hello, Rust!” sur votre console.

Décortiquons l'extrait de code :

  • fn main() : Ceci définit la fonction principale. Le mot-clé fn signifie une déclaration de fonction. main est un nom de fonction spécial ; c'est le point d'entrée où chaque programme Rust exécutable commence son exécution. Les parenthèses () indiquent que cette fonction ne prend aucun paramètre d'entrée.
  • {} : Les accolades définissent un bloc de code ou une portée. Tout le code appartenant à la fonction va à l'intérieur de ces accolades.
  • println!("Hello, Rust!"); : Cette ligne effectue l'action d'afficher du texte dans la console.
    • println! est une macro Rust. Les macros sont similaires aux fonctions mais ont une différence clé : elles se terminent par un point d'exclamation !. Les macros effectuent une génération de code au moment de la compilation, offrant plus de puissance et de flexibilité que les fonctions régulières (comme la gestion d'un nombre variable d'arguments, ce que fait println!). La macro println! affiche le texte fourni dans la console et ajoute automatiquement un caractère de nouvelle ligne à la fin.
    • "Hello, Rust!" est un littéral de chaîne – une séquence fixe de caractères représentant du texte, entourée de guillemets doubles.
    • ; : Le point-virgule marque la fin de l'instruction. La plupart des lignes de code Rust exécutables (instructions) se terminent par un point-virgule.

Tutoriel Rust pour débutants : Concepts clés

Maintenant, plongeons dans les éléments fondamentaux du langage de programmation Rust.

Variables et Mutabilité

Les variables sont utilisées pour stocker des valeurs de données. En Rust, vous déclarez des variables en utilisant le mot-clé let.

let pommes = 5;
let message = "Prends-en cinq";

Un concept fondamental en Rust est que les variables sont immuables par défaut. Cela signifie qu'une fois qu'une valeur est liée à un nom de variable, vous ne pouvez plus changer cette valeur plus tard.

let x = 10;
// x = 15; // Cette ligne provoquera une erreur de compilation ! Impossible d'assigner deux fois à la variable immuable `x`.
println!("La valeur de x est : {}", x); // {} est un espace réservé pour la valeur de x

Cette immutabilité par défaut est un choix de conception délibéré qui vous aide à écrire du code plus sûr et plus prévisible en empêchant la modification accidentelle des données, qui peut être une source courante de bogues. Si vous avez besoin d'une variable dont la valeur peut changer, vous devez explicitement la marquer comme mutable en utilisant le mot-clé mut lors de la déclaration.

let mut compteur = 0; // Déclare 'compteur' comme mutable
println!("Compteur initial : {}", compteur);
compteur = 1; // Ceci est autorisé car 'compteur' a été déclaré avec 'mut'
println!("Nouveau compteur : {}", compteur);

Rust permet également le masquage (shadowing). Vous pouvez déclarer une nouvelle variable avec le même nom qu'une variable précédente dans la même portée. La nouvelle variable “masque” l'ancienne, ce qui signifie que les utilisations ultérieures du nom se réfèrent à la nouvelle variable. C'est différent de la mutation car nous créons une variable entièrement nouvelle, qui peut même avoir un type différent.

let espaces = "   "; // 'espaces' est initialement une tranche de chaîne (&str)
let espaces = espaces.len(); // 'espaces' est maintenant masqué par une nouvelle variable contenant la longueur (un entier, usize)
println!("Nombre d'espaces : {}", espaces); // Affiche la valeur entière

Types de données de base

Rust est un langage à typage statique. Cela signifie que le type de chaque variable doit être connu par le compilateur au moment de la compilation. Cependant, Rust a une excellente inférence de type. Dans de nombreuses situations, vous n'avez pas besoin d'écrire explicitement le type ; le compilateur peut souvent le déterminer en fonction de la valeur et de la façon dont vous l'utilisez.

let quantite = 10;         // Le compilateur infère i32 (le type entier signé par défaut)
let prix = 9.99;          // Le compilateur infère f64 (le type à virgule flottante par défaut)
let actif = true;         // Le compilateur infère bool (booléen)
let initiale = 'R';         // Le compilateur infère char (caractère)

Si vous voulez ou devez être explicite (par exemple, pour la clarté ou lorsque le compilateur a besoin d'aide), vous pouvez fournir des annotations de type en utilisant un deux-points : suivi du nom du type.

let score: i32 = 100;       // Explicitement un entier signé de 32 bits
let ratio: f32 = 0.5;       // Explicitement un flottant simple précision
let est_complet: bool = false; // Explicitement un booléen
let note: char = 'A';      // Explicitement un caractère (valeur scalaire Unicode)

Rust possède plusieurs types scalaires intégrés (représentant des valeurs uniques) :

  • Entiers : Les entiers signés (i8, i16, i32, i64, i128, isize) stockent des nombres entiers positifs et négatifs. Les entiers non signés (u8, u16, u32, u64, u128, usize) ne stockent que des nombres entiers non négatifs. Les types isize et usize dépendent de l'architecture de l'ordinateur (32 bits ou 64 bits) et sont principalement utilisés pour indexer les collections.
  • Nombres à virgule flottante : f32 (simple précision) et f64 (double précision). Le défaut est f64.
  • Booléens : Le type bool a deux valeurs possibles : true ou false.
  • Caractères : Le type char représente une seule valeur scalaire Unicode (plus complète que les simples caractères ASCII), entourée d'apostrophes simples (par exemple, 'z', 'π', '🚀').

Chaînes de caractères : String vs &str

La gestion du texte en Rust implique souvent deux types principaux, ce qui peut être déroutant pour les nouveaux arrivants :

  1. &str (prononcé “string slice” ou “tranche de chaîne”) : Il s'agit d'une référence immuable à une séquence d'octets encodés en UTF-8 stockée quelque part en mémoire. Les littéraux de chaîne (comme "Hello") sont de type &'static str, ce qui signifie qu'ils sont stockés directement dans le binaire du programme et vivent pendant toute la durée du programme. Les tranches fournissent une vue sur les données de chaîne sans les posséder. Elles ont une taille fixe.
  2. String : Il s'agit d'un type de chaîne de caractères possédé, redimensionnable, mutable, encodé en UTF-8. Les données String sont stockées sur le tas, ce qui leur permet d'être redimensionnées. Vous utilisez généralement String lorsque vous devez modifier des données de chaîne, ou lorsque la chaîne doit posséder ses propres données et gérer sa propre durée de vie (souvent lors du retour de chaînes depuis des fonctions ou de leur stockage dans des structures).
// Littéral de chaîne (stocké dans le binaire du programme, immuable)
let tranche_statique: &'static str = "Je suis immuable";

// Crée une String possédée, allouée sur le tas, à partir d'un littéral
let mut chaine_dynamique: String = String::from("Début");

// Modifie la String (possible car elle est mutable et possède ses données)
chaine_dynamique.push_str(" et croissance");
println!("{}", chaine_dynamique); // Sortie : Début et croissance

// Crée une tranche de chaîne qui référence une partie de la String
// Cette tranche emprunte des données à chaine_dynamique
let tranche_depuis_string: &str = &chaine_dynamique[0..5]; // Référence "Début"
println!("Tranche : {}", tranche_depuis_string);

Fonctions

Les fonctions sont fondamentales pour organiser le code en unités nommées et réutilisables. Nous avons déjà rencontré la fonction spéciale main.

// Définition de fonction
fn saluer(nom: &str) { // Prend un paramètre : 'nom', qui est une tranche de chaîne (&str)
    println!("Bonjour, {}!", nom);
}

// Fonction qui prend deux paramètres i32 et retourne un i32
fn additionner(a: i32, b: i32) -> i32 {
    // En Rust, la dernière expression dans le corps d'une fonction est automatiquement retournée,
    // tant qu'elle ne se termine pas par un point-virgule.
    a + b
    // Ceci est équivalent à écrire : return a + b;
}

fn main() {
    saluer("Alice"); // Appelle la fonction 'saluer'

    let somme = additionner(5, 3); // Appelle 'additionner', lie la valeur retournée à 'somme'
    println!("5 + 3 = {}", somme); // Sortie : 5 + 3 = 8
}

Points clés sur les fonctions :

  • Utilisez le mot-clé fn pour déclarer des fonctions.
  • Spécifiez les noms des paramètres et leurs types entre les parenthèses.
  • Spécifiez le type de retour de la fonction après une flèche ->. Si une fonction ne retourne pas de valeur, son type de retour est implicitement () (un tuple vide, souvent appelé “type unité”).
  • Les corps de fonction consistent en une série d'instructions (opérations qui effectuent une action, se terminant généralement par ;) et se terminent éventuellement par une expression (quelque chose qui s'évalue en une valeur).
  • La valeur de l'expression finale dans un bloc de fonction est automatiquement retournée si elle n'a pas de point-virgule. Le mot-clé return peut être utilisé pour des retours explicites et anticipés depuis n'importe où dans la fonction.

Flux de contrôle

Rust fournit des structures de flux de contrôle standard pour déterminer l'ordre dans lequel le code s'exécute :

  • if/else/else if : Utilisé pour l'exécution conditionnelle.
let nombre = 6;

if nombre % 4 == 0 {
    println!("le nombre est divisible par 4");
} else if nombre % 3 == 0 {
    println!("le nombre est divisible par 3"); // Cette branche s'exécute
} else {
    println!("le nombre n'est pas divisible par 4 ou 3");
}

// Fait important, 'if' est une expression en Rust, ce qui signifie qu'elle s'évalue en une valeur.
// Cela vous permet de l'utiliser directement dans les instructions 'let'.
let condition = true;
let valeur = if condition { 5 } else { 6 }; // valeur sera 5
println!("La valeur est : {}", valeur);
// Note : Les deux branches de l'expression 'if' doivent s'évaluer au même type.
  • Boucles : Utilisées pour l'exécution répétée.
    • loop : Crée une boucle infinie. Vous utilisez typiquement break pour sortir de la boucle, en retournant éventuellement une valeur.
    • while : Boucle tant qu'une condition spécifiée reste vraie.
    • for : Itère sur les éléments d'une collection ou d'une plage. C'est le type de boucle le plus couramment utilisé et souvent le plus sûr en Rust.
// Exemple de loop
let mut compteur = 0;
let resultat = loop {
    compteur += 1;
    if compteur == 10 {
        break compteur * 2; // Sort de la boucle et retourne 'compteur * 2'
    }
};
println!("Résultat de la boucle : {}", resultat); // Sortie : Résultat de la boucle : 20

// Exemple de while
let mut num = 3;
while num != 0 {
    println!("{}!", num);
    num -= 1;
}
println!("DÉCOLLAGE !!!");

// Exemple de for (itération sur un tableau)
let a = [10, 20, 30, 40, 50];
for element in a.iter() { // .iter() crée un itérateur sur les éléments du tableau
    println!("la valeur est : {}", element);
}

// Exemple de for (itération sur une plage)
// (1..4) crée une plage incluant 1, 2, 3 (excluant 4)
// .rev() inverse l'itérateur
for nombre in (1..4).rev() {
    println!("{}!", nombre); // Affiche 3!, 2!, 1!
}
println!("ENCORE DÉCOLLAGE !!!");

Commentaires

Utilisez des commentaires pour ajouter des explications et des notes à votre code que le compilateur ignorera.

// Ceci est un commentaire sur une seule ligne. Il s'étend jusqu'à la fin de la ligne.

/*
 * Ceci est un commentaire de bloc, sur plusieurs lignes.
 * Il peut s'étendre sur plusieurs lignes et est utile
 * pour des explications plus longues.
 */

 let nombre_chanceux = 7; // Vous pouvez aussi placer des commentaires à la fin d'une ligne.

Comprendre la Propriété : Le concept central de Rust

La propriété est la caractéristique la plus distinctive et centrale de Rust. C'est le mécanisme qui permet à Rust de garantir la sécurité de la mémoire au moment de la compilation sans avoir besoin d'un ramasse-miettes. Saisir la propriété est la clé pour comprendre Rust. Elle suit trois règles fondamentales :

  1. Propriétaire : Chaque valeur en Rust a une variable désignée comme son propriétaire.
  2. Un seul propriétaire : Il ne peut y avoir qu'un seul propriétaire d'une valeur spécifique à un moment donné.
  3. Portée & Libération (Drop) : Lorsque la variable propriétaire sort de la portée (par exemple, la fonction dans laquelle elle a été déclarée se termine), la valeur qu'elle possède est libérée (‘dropped’). Cela signifie que sa mémoire est automatiquement désallouée.
{ // s n'est pas valide ici, il n'est pas encore déclaré
    let s = String::from("hello"); // s est valide à partir de ce point ;
                                    // s 'possède' les données de la String allouées sur le tas.
    // Vous pouvez utiliser s ici
    println!("{}", s);
} // La portée se termine ici. 's' n'est plus valide.
  // Rust appelle automatiquement une fonction spéciale 'drop' pour la String que 's' possède,
  // libérant sa mémoire sur le tas.

Lorsque vous assignez une valeur possédée (comme une String, un Vec, ou une structure contenant des types possédés) à une autre variable, ou que vous la passez à une fonction par valeur, la propriété est déplacée. La variable originale devient invalide.

let s1 = String::from("original");
let s2 = s1; // La propriété des données de la String est DÉPLACÉE de s1 à s2.
             // s1 n'est plus considéré comme valide après ce point.

// println!("s1 est : {}", s1); // Erreur de compilation ! Valeur empruntée ici après déplacement.
                              // s1 ne possède plus les données.
println!("s2 est : {}", s2); // s2 est maintenant le propriétaire et est valide.

Ce comportement de déplacement empêche les erreurs de “double libération”, où deux variables pourraient accidentellement essayer de libérer le même emplacement mémoire lorsqu'elles sortent de la portée. Les types primitifs comme les entiers, les flottants, les booléens et les caractères implémentent le trait Copy, ce qui signifie qu'ils sont simplement copiés au lieu d'être déplacés lors de l'assignation ou du passage aux fonctions.

Emprunt et Références

Que faire si vous voulez laisser une fonction utiliser une valeur sans transférer la propriété ? Vous pouvez créer des références. Créer une référence s'appelle emprunter. Une référence vous permet d'accéder aux données possédées par une autre variable sans en prendre possession.

// Cette fonction prend une référence (&) vers une String.
// Elle emprunte la String mais ne prend pas possession.
fn calculer_longueur(s: &String) -> usize {
    s.len()
} // Ici, s (la référence) sort de la portée. Mais comme elle ne possède pas
  // les données de la String, les données ne sont PAS libérées lorsque la référence sort de la portée.

fn main() {
    let s1 = String::from("hello");

    // Nous passons une référence à s1 en utilisant le symbole '&'.
    // s1 possède toujours les données de la String.
    let len = calculer_longueur(&s1);

    // s1 est toujours valide ici car la propriété n'a jamais été déplacée.
    println!("La longueur de '{}' est {}.", s1, len);
}

Les références sont immuables par défaut, tout comme les variables. Si vous voulez modifier les données empruntées, vous avez besoin d'une référence mutable, indiquée par &mut. Cependant, Rust applique des règles strictes concernant les références mutables pour prévenir les conflits de données :

Les Règles d'Emprunt :

  1. À tout moment donné, vous pouvez avoir soit :
    • Une référence mutable (&mut T).
    • N'importe quel nombre de références immuables (&T).
  2. Les références doivent toujours être valides (elles ne peuvent pas survivre aux données qu'elles pointent – ceci est géré par les durées de vie, souvent implicitement).

Ces règles sont appliquées par le compilateur.

// Cette fonction prend une référence mutable vers une String
fn changer(une_chaine: &mut String) {
    une_chaine.push_str(", world");
}

fn main() {
    // 's' doit être déclaré mutable pour permettre l'emprunt mutable
    let mut s = String::from("hello");

    // Exemple d'application des règles d'emprunt (décommentez les lignes pour voir les erreurs) :
    // let r1 = &s; // emprunt immuable - OK
    // let r2 = &s; // autre emprunt immuable - OK (plusieurs emprunts immuables autorisés)
    // let r3 = &mut s; // ERREUR ! Impossible d'emprunter `s` comme mutable pendant que des emprunts immuables sont actifs
    //                  // Rust impose : soit plusieurs lecteurs (&T) OU un seul écrivain (&mut T), jamais les deux
    // println!("{}, {}", r1, r2); // Utiliser r1/r2 les maintient actifs, déclenchant l'erreur
    //                             // Sans ce println, les NLL (Non-Lexical Lifetimes) de Rust libéreraient r1/r2 plus tôt,
    //                             // rendant &mut s valide ici

    // Un emprunt mutable est autorisé ici car aucun autre emprunt n'est actif
    changer(&mut s);
    println!("{}", s); // Sortie : hello, world
}

Types de données composés

Rust fournit des moyens de regrouper plusieurs valeurs en types plus complexes.

Structures (Structs)

Les structures (abréviation de structures) vous permettent de définir des types de données personnalisés en regroupant des champs de données associés sous un seul nom.

// Définit une structure nommée User
struct User {
    active: bool,
    username: String, // Utilise le type String possédé
    email: String,
    sign_in_count: u64,
}

fn main() {
    // Crée une instance de la structure User
    // Les instances doivent fournir des valeurs pour tous les champs
    let mut user1 = User {
        email: String::from("quelquun@example.com"),
        username: String::from("unutilisateur123"),
        active: true,
        sign_in_count: 1,
    };

    // Accède aux champs de la structure en utilisant la notation pointée
    // L'instance doit être mutable pour changer les valeurs des champs
    user1.email = String::from("autreemail@example.com");

    println!("Email utilisateur : {}", user1.email);

    // Utilisation d'une fonction d'aide pour créer une instance User
    let user2 = build_user(String::from("user2@test.com"), String::from("user2"));
    println!("Statut actif de l'utilisateur 2 : {}", user2.active);

    // Syntaxe de mise à jour de structure : Crée une nouvelle instance en utilisant
    // certains champs d'une instance existante pour les champs restants.
    let user3 = User {
        email: String::from("user3@domain.com"),
        username: String::from("user3"),
        ..user2 // prend les valeurs de 'active' et 'sign_in_count' de user2
    };
    println!("Nombre de connexions de l'utilisateur 3 : {}", user3.sign_in_count);
}

// Fonction qui retourne une instance User
fn build_user(email: String, username: String) -> User {
    User {
        email, // Raccourci d'initialisation de champ : si le nom du paramètre correspond au nom du champ
        username,
        active: true,
        sign_in_count: 1,
    }
}

Rust supporte également les structures-tuples, qui sont des tuples nommés (par exemple, struct Color(i32, i32, i32);), et les structures unitaires, qui n'ont pas de champs et sont utiles lorsque vous devez implémenter un trait sur un type mais n'avez pas besoin de stocker de données (par exemple, struct AlwaysEqual;).

Énumérations (Enums)

Les énumérations (Enums) vous permettent de définir un type en énumérant ses variantes possibles. Une valeur d'enum ne peut être que l'une de ses variantes possibles.

// Enum simple définissant les types d'adresses IP
enum IpAddrKind {
    V4, // Variante 1
    V6, // Variante 2
}

// Les variantes d'enum peuvent aussi contenir des données associées
enum IpAddr {
    V4(u8, u8, u8, u8), // La variante V4 contient quatre valeurs u8
    V6(String),         // La variante V6 contient une String
}

// Une enum très courante et importante dans la bibliothèque standard de Rust : Option<T>
// Elle encode le concept d'une valeur qui peut être présente ou absente.
// enum Option<T> {
//     Some(T), // Représente la présence d'une valeur de type T
//     None,    // Représente l'absence de valeur
// }

// Une autre enum cruciale de la bibliothèque standard : Result<T, E>
// Utilisée pour les opérations qui peuvent réussir (Ok) ou échouer (Err).
// enum Result<T, E> {
//     Ok(T),   // Représente le succès, contenant une valeur de type T
//     Err(E),  // Représente l'échec, contenant une valeur d'erreur de type E
// }

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    // Création d'instances de l'enum IpAddr avec des données associées
    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));

    // Exemple avec Option<T>
    let some_number: Option<i32> = Some(5);
    let no_number: Option<i32> = None;

    // L'opérateur de flux de contrôle 'match' est parfait pour travailler avec les enums.
    // Il vous permet d'exécuter différents codes en fonction de la variante de l'enum.
    match some_number {
        Some(i) => println!("Obtenu un nombre : {}", i), // Si c'est Some, lie la valeur interne à i
        None => println!("Rien obtenu."),           // Si c'est None
    }

    // 'match' doit être exhaustif : vous devez gérer toutes les variantes possibles.
    // Le caractère de soulignement '_' peut être utilisé comme motif joker pour attraper
    // toutes les variantes non explicitement listées.
}

Option<T> et Result<T, E> sont au cœur de l'approche de Rust pour gérer de manière robuste les valeurs potentiellement manquantes et les erreurs.

Introduction à Cargo : L'outil de build et gestionnaire de paquets Rust

Cargo est une partie indispensable de l'écosystème Rust, rationalisant le processus de construction, de test et de gestion des projets Rust. C'est l'une des fonctionnalités que les développeurs apprécient souvent.

  • Cargo.toml : C'est le fichier manifeste de votre projet Rust. Il est écrit au format TOML (Tom's Obvious, Minimal Language). Il contient des métadonnées essentielles sur votre projet (comme son nom, sa version et ses auteurs) et, surtout, liste ses dépendances (d'autres crates externes dont votre projet dépend).
    [package]
    name = "mon_projet"
    version = "0.1.0"
    edition = "2021" # Spécifie l'édition Rust à utiliser (influence les fonctionnalités du langage)
    
    # Les dépendances sont listées ci-dessous
    [dependencies]
    # Exemple : Ajouter la crate 'rand' pour la génération de nombres aléatoires
    # rand = "0.8.5"
    # Lorsque vous construisez, Cargo téléchargera et compilera 'rand' et ses dépendances.
    
  • cargo new <nom_projet> : Crée une nouvelle structure de projet d'application binaire (exécutable).
  • cargo new --lib <nom_bibliotheque> : Crée une nouvelle structure de projet de bibliothèque (crate destinée à être utilisée par d'autres programmes).
  • cargo build : Compile votre projet et ses dépendances. Par défaut, il crée un build de débogage non optimisé. La sortie est placée dans le répertoire target/debug/.
  • cargo build --release : Compile votre projet avec les optimisations activées, adapté à la distribution ou aux tests de performance. La sortie est placée dans le répertoire target/release/.
  • cargo run : Compile (si nécessaire) et exécute votre projet binaire.
  • cargo check : Vérifie rapidement votre code pour les erreurs de compilation sans produire réellement l'exécutable final. C'est généralement beaucoup plus rapide que cargo build et utile pendant le développement pour un retour rapide.
  • cargo test : Exécute tous les tests définis dans votre projet (généralement situés dans le répertoire src ou dans un répertoire tests séparé).

Lorsque vous ajoutez une dépendance à votre fichier Cargo.toml puis exécutez une commande comme cargo build ou cargo run, Cargo gère automatiquement le téléchargement de la crate requise (et de ses dépendances) depuis le dépôt central crates.io et compile tout ensemble.

Prochaines étapes dans votre parcours Rust

Ce guide de démarrage a couvert les bases absolues pour vous lancer. Le langage Rust offre de nombreuses autres fonctionnalités puissantes à explorer au fur et à mesure de votre progression :

  • Gestion des erreurs : Maîtriser l'enum Result, la propagation des erreurs à l'aide de l'opérateur ?, et la définition de types d'erreurs personnalisés.
  • Collections : Travailler efficacement avec des structures de données courantes comme Vec<T> (tableaux/vecteurs dynamiques), HashMap<K, V> (tables de hachage), HashSet<T>, etc.
  • Génériques : Écrire du code flexible et réutilisable qui peut fonctionner sur différents types de données en utilisant des paramètres de type (<T>).
  • Traits : Définir des comportements et des interfaces partagés que les types peuvent implémenter (similaires aux interfaces dans d'autres langages, mais plus puissants).
  • Durées de vie : Comprendre comment Rust garantit que les références sont toujours valides, nécessitant parfois des annotations de durée de vie explicites ('a).
  • Concurrence : Explorer les threads, les canaux pour le passage de messages, l'état partagé avec Mutex et Arc, et la puissante syntaxe async/await de Rust pour la programmation asynchrone.
  • Macros : Apprendre à écrire vos propres macros pour une génération de code avancée au moment de la compilation.
  • Modules : Organiser des projets plus importants en unités logiques, contrôler la visibilité (publique/privée) des éléments.
  • Tests : Écrire des tests unitaires, d'intégration et de documentation efficaces en utilisant le framework de test intégré de Rust.

Conclusion

Rust présente une proposition unique et convaincante : la performance brute attendue des langages de bas niveau comme C++, combinée à de fortes garanties de sécurité au moment de la compilation qui éliminent des classes entières de bogues courants, en particulier autour de la gestion de la mémoire et de la concurrence. Bien que la courbe d'apprentissage implique d'internaliser de nouveaux concepts comme la propriété et l'emprunt (et d'écouter le compilateur parfois strict), le gain est un logiciel hautement fiable, efficace et souvent plus facile à maintenir à long terme. Avec son excellent outillage via Cargo et une communauté solidaire, Rust est un langage gratifiant à apprendre.

Commencez petit, expérimentez en utilisant Cargo, acceptez les messages d'erreur utiles (bien que parfois verbeux) du compilateur comme des guides, et vous serez bien parti pour maîtriser ce langage puissant et de plus en plus populaire. Bon codage en Rust !

Articles liés