Rust est un langage de pro­gram­ma­tion de Mozilla. Il peut être utilisée pour écrire des outils en ligne de commande, des ap­pli­ca­tions Web et des pro­grammes de réseau. Le langage est également adapté à la pro­gram­ma­tion pour hardware.

Dans ce tutoriel sur Rust, nous vous montrons les ca­rac­té­ris­tiques les plus im­por­tantes du langage. Ce faisant, nous exa­mi­ne­rons les si­mi­li­tudes et les dif­fé­rences avec d’autres langues si­mi­laires. Nous vous guiderons à travers l’ins­tal­la­tion de Rust et vous ap­pren­drez comment écrire et compiler le code Rust sur votre propre système.

Aperçu du langage de pro­gram­ma­tion Rust

Rust est un langage compilé, ce qui lui confère des per­for­mances élevées ; en parallèle, le langage propose des abs­trac­tions so­phis­ti­quées qui fa­ci­li­tent le travail du pro­gram­meur. Rust s’intéresse tout par­ti­cu­liè­re­ment à la sécurité de la mémoire. Cela donne au langage un avantage par­ti­cu­lier par rapport aux anciens langages tels que C et C++.

Utiliser Rust sur son système

Comme Rust est un logiciel open-source gratuit (FOSS), tout le monde peut té­lé­char­ger la chaîne d’outils Rust et l’utiliser sur son système personnel. Con­trai­re­ment à Python ou Ja­vaS­cript, Rust n’est pas un langage in­ter­prété. Au lieu d’un in­ter­pré­teur, on utilise un com­pi­la­teur, comme en C, C++ et Java. En pratique, cela signifie qu’il y a deux étapes pour exécuter le code :

  1. Compiler le code source. Cela permet de produire un exé­cu­table binaire.
  2. Exécuter le binaire résultant.

Il est possible que les deux étapes soient sim­ple­ment con­trô­lées depuis la ligne de commande.

Conseil
Dans un autre article du Digital Guide, nous examinons de plus près la dif­fé­rence entre un compiler et un in­ter­pre­ter.

Avec Rust, des bi­blio­thèques peuvent être créées en plus des fichiers binaires exé­cu­tables. Si le code compilé est un programme di­rec­te­ment exé­cu­table, une fonction main() doit être définie dans le code source. Comme en C / C++, cela sert de point d’entrée dans l’exécution du code.

Installer Rust pour le tutoriel sur le système local

Pour utiliser Rust, il faut d’abord effectuer une ins­tal­la­tion locale. Sous macOS, vous pourrez utiliser le ges­tion­naire de paquets Homebrew. Le homebrew fonc­tionne également sous Linux. Ouvrez une ligne de commande (« Terminal.App » sur Mac), copiez la ligne de code suivante dans le terminal et exécutez-la :

brew install rust
Remarque
Pour installer Rust sur Windows ou tout autre système sans Homebrew, utilisez l’outil officiel Rustup.

Pour vérifier que l’ins­tal­la­tion de Rust a réussi, ouvrez une nouvelle fenêtre sur la ligne de commande et exécutez le code suivant :

rustc --version

Si Rust est cor­rec­te­ment installé sur votre système, la version du com­pi­la­teur Rust vous sera présentée. Si un message d’erreur apparaît à la place, re­dé­mar­rez l’ins­tal­la­tion.

Compiler du code Rust

Pour compiler le code Rust, vous avez besoin d’un fichier de code source Rust. Ouvrez la ligne de commande et exécutez les codes suivants. Nous allons d’abord créer un dossier pour le tutoriel Rust sur le bureau et passer sur ce dossier :

cd "$HOME/Desktop/"
mkdir tutoriel-rust && cd tutoriel-rust

Ensuite, nous créons le fichier de code source Rust pour un exemple simple avec « Hello, World » :

cat << EOF > ./tutoriel-rust.rs
fn main() {
    println!("Hello, World!");
}
EOF
Remarque
Les fichiers de code source Rust se terminent par l’abré­via­tion .rs.

Enfin, nous allons compiler le code source Rust et exécuter le binaire qui en résulte :

# Compiler du code source Rust
rustc tutoriel-rust.rs
# Exécuter le binaire résultant
./tutoriel-rust
Conseil
Utilisez la commande rustc tutoriel-rust.rs && ./tutoriel-rust, pour combiner les deux étapes. Pour compiler et exécuter de nouveau votre programme avec la ligne de commande, appuyez sur la flèche qui part vers le haut puis la touche Entrée.

Gérer les paquets Rust avec Cargo

Outre le langage Rust pro­pre­ment dit, il existe un certain nombre de paquets externes. Ces « crates » peuvent être obtenues dans le Rust Package Registry. L’outil Cargo installé avec Rust est alors utilisé. La commande cargo est utilisée en ligne de commande pour installer des paquets et en créer de nouveaux. Vérifiez que le Cargo a été installé cor­rec­te­ment :

cargo --version

Apprendre les bases de Rust

Pour apprendre Rust, nous vous re­com­man­dons d’essayer vous-même les exemples de codes. Vous pouvez utiliser le fichier tutoriel-rust.rs déjà créé à cet effet. Copiez un échan­til­lon de code dans le fichier, compilez-le et exécutez le binaire résultant. Pour que cela fonc­tionne, l’extrait de code doit être inséré dans la fonction main() !

Vous pouvez également utiliser le Rust Play­ground di­rec­te­ment dans votre na­vi­ga­teur pour essayer le code Rust.

Ins­truc­tions et blocs

Les ins­truc­tions cons­ti­tuent la base du code Rust. Une ins­truc­tion se termine par un point-virgule (;) et, con­trai­re­ment à une ex­pres­sion, ne renvoie pas de valeur. Plusieurs ins­truc­tions peuvent être re­grou­pées en un seul bloc. Les blocs sont délimités par des accolades "{}", comme en C/C++ et Java.

Com­men­taires dans Rust

Les com­men­taires sont une ca­rac­té­ris­tique im­por­tante de tout langage de pro­gram­ma­tion. Ils sont utilisés à la fois pour do­cu­men­ter le code et pour planifier le dé­rou­le­ment futur de votre code. Rust utilise la même syntaxe de com­men­taire que C, C++, Java et Ja­vaS­cript : tout texte après une double barre oblique est in­ter­prété comme un com­men­taire et ignoré par le com­pi­la­teur :

// Ceci est un commentaire
// Un commentaire,
// peut être écrit
// sur plusieurs lignes.

Variables et cons­tantes

Dans Rust, on utilise le mot-clé « let » pour déclarer une variable. Une variable existante peut être déclarée de nouveau dans Rust et donc « éclipsée ». Con­trai­re­ment à de nom­breuses autres langues, la valeur d’une variable ne peut pas être modifiée fa­ci­le­ment :

// Déclarer la variable « age » et y associer la valeur « 42 »
let age = 42;
// La valeur de la variable « age » ne peut pas être modifiée
age = 49; // Erreur compiler
// avec de nouveau « let », la variable peut être écrasée
let age = 49;

Pour marquer la valeur d’une variable comme mo­di­fiable ul­té­rieu­re­ment, Rust a défini le mot-clé « mut ». La valeur d’une variable déclarée avec « mut » se modifie donc fa­ci­le­ment :

let mut poids = 78;
poids = 75;

Le mot-clé « const » génère une constante. La valeur d’une constante de Rust doit être connue au moment de la com­pi­la­tion. Le type doit également être spécifié ex­pli­ci­te­ment :

const VERSION: &str = "1.46.0";

La valeur d’une constante ne peut pas être modifiée - une constante ne peut pas non plus être déclarée comme « mut ». En outre, une constante ne peut pas être re-déclarée :

// Définir une constante
const MAX_NUM: u8 = 255;
MAX_NUM = 0; // Erreur de compilation, car la valeur d’une constante ne peut être modifiée
const MAX_NUM = 0; // Erreur de compilation, car la constante ne peut pas être déclarée à nouveau

La notion de propriété dans Rust

L’une des ca­rac­té­ris­tiques dé­ter­mi­nantes de Rust est le concept de propriété (angl. « Ownership »). La propriété est étroi­te­ment liée à la valeur des variables, à leur durée de vie et à la gestion de la mémoire des objets en tas (heap). Lorsqu’une variable quitte son champs d’ap­pli­ca­tion (scope), sa valeur est détruite et la mémoire est libérée. Rust peut donc se passer du garbage col­lec­tion, ce qui permet d’accroître ses per­for­mances.

Chaque valeur dans Rust ap­par­tient à une variable - le pro­prié­taire. Il ne peut y avoir qu’un seul pro­prié­taire pour chaque valeur. Si le pro­prié­taire transmet la valeur, alors il n’est plus pro­prié­taire :

let nom = String::from("Jean Dupont");
let _nom = nom;
println!("{}, world!", nom); // Erreur de compilation, car la valeur de « nom » a été transmise à « _nom »

Un soin par­ti­cu­lier doit être apporté à la dé­fi­ni­tion des fonctions : si une variable est passée à une fonction, le pro­prié­taire de la valeur change. La variable ne peut pas être réu­ti­li­sée après l’appel de fonction. Ici, Rust utilise une astuce : au lieu de passer la valeur à la fonction elle-même, une référence est déclarée avec le symbole de l’es­per­luette (&). Cela permet d’"emprunter" la valeur d’une variable. Voici un exemple :

let nom = String::from("Jean Dupont");
// le type du paramètre « nom » est défini comme « String » et non « &String »
// la variable « nom » ne peut plus être utilisée après l’appel de la fonction
fn hello(nom: &String) {
    println!("Hello, {}", nom);
}
// l’argument de la fonction doit également être
// marqué avec "&" pour référence
hello(&nom);
// cette ligne sans utilisation de la référence conduit à une erreur de compilation
println!("Hello, {}", nom);

Struc­tures de contrôle

Une par­ti­cu­la­rité fon­da­men­tale du langage est de rendre le dé­rou­le­ment du programme non linéaire. Un programme peut se ramifier, et les com­po­sants du programme peuvent être exécutés plusieurs fois. Ce n’est qu’à travers cette va­ria­bi­lité qu’un programme devient vraiment utile.

Rust dispose des struc­tures de contrôle dis­po­nibles dans la plupart des langages de pro­gram­ma­tion. Il s’agit notamment des boucles "for" et "while", ainsi que des ra­mi­fi­ca­tions "if" et "else". Rust présente également des ca­rac­té­ris­tiques par­ti­cu­lières. La cons­truc­tion "match" permet d’assigner des modèles, tandis que "loop" crée une boucle sans fin. Dans la pratique, cette dernière sera utilisée avec « break ».

L’itération

L’exécution répétée d’un bloc de code au moyen de boucles est également connue sous le nom d’itération. L’itération est souvent effectuée sur les éléments d’un conteneur. Comme Python, Rust connaît le concept d’"itérateur". Un itérateur abstrait l’accès successif aux éléments d’un conteneur. Prenons un exemple :

// Liste de noms
let noms = ["Jim", "Jack", "John"];
// Boucle « for » avec itérateur dans la liste
for nom in noms.iter() {
    println!("Hello, {}", nom);
}

Et main­te­nant, comment écrire une boucle "for" dans le style C/C++ ou Java ? Vous allez donc spécifier un chiffre de début et un chiffre de fin et faire défiler toutes les valeurs in­ter­mé­diaires. Dans ce cas, il existe l’objet "Range" dans Rust, comme dans Python. Cela déclenche à son tour un itérateur sur lequel le mot-clé "for" fonc­tionne :

// Emettre les chiffres de 1 à 10
// Boucle « for » avec pour itérateur « range »
// Attention : le dernier chiffre n’est pas compris !
for nombre in 1..11 {
    println!("Nombre: {}", nombre);
}
// autre code pour un même résultat
for nombre in 1..=10 {
    println!("Nombre: {}", nombre);
}

Une boucle « while » fonc­tionne dans Rust comme dans la plupart des autres langages. Une condition est fixée et le corps de la boucle est exécuté tant que la condition est vraie :

// Emettre les chiffres de 1 à 10 avec la boucle ‚while’
let mut nombre = 1;
while (nombre <= 10) {
    println!(Nombre: {}, nombre);
    nombre += 1;
}

Il est possible pour tous les langages de pro­gram­ma­tion de créer une boucle sans fin avec « while ». Nor­ma­le­ment, il s’agit d’une erreur, mais il y a aussi des cas d’uti­li­sa­tion qui l’exigent. Rust propose la procédure suivante dans ce cas :

// Boucle infinie avec « while »
while true {
    // …
}
// Boucle infinie avec « loop »
loop {
    // …
}

Dans les deux cas, le mot-clé « break » peut être utilisé pour sortir de la boucle.

Con­di­tion­nel

Les ra­mi­fi­ca­tions avec « if » et « else » fonc­tionne également dans Rust comme dans d’autres langages si­mi­laires :

const limit: u8 = 42;
let nombre = 43;
if nombre < limit {
    println!("Sous la limite.");
}
else if nombre == limit {
    println!("Limite atteinte…");
}
else {
    println!("Au-dessus de la limite!");
}

Le mot-clé « match » de Rust est très in­té­res­sant. Il a une fonction similaire à celle de « switch » présent dans d’autres langues. Pour exemple, regardez la fonction carte_symbole() dans la section « Types de données composées » (voir plus loin dans cet article).

Fonctions, pro­cé­dures et méthodes

Dans la plupart des langages de pro­gram­ma­tion, les fonctions sont la base de la pro­gram­ma­tion modulaire. Les fonctions sont définies dans Rust avec le mot-clé « fn ». Aucune dis­tinc­tion stricte n’est faite entre les concepts connexes de fonction et de procédure. Les deux sont définis de manière presque identique.

Une fonction au sens propre du terme renvoie une valeur. Comme beaucoup d’autres langages de pro­gram­ma­tion, Rust comprend aussi des pro­cé­dures, c’est-à-dire des fonctions qui ne renvoient pas de valeur. La seule res­tric­tion fixe est que le type de retour d’une fonction doit être ex­pli­ci­te­ment spécifié. Si aucun type de retour n’est spécifié, la fonction ne peut pas renvoyer une valeur ; elle est alors définie comme une procédure.

fn procedure() {
    println!("Cette procedure ne retourne pas de valeur.");
}
// Type de retour après l’opérateur ‚->‘
fn moins(chiffreentier: i8) -> i8 {
    return chiffreentier * -1;
}

En plus des fonctions et des pro­cé­dures, Rust connaît également les méthodes de la pro­gram­ma­tion orientée objet. Une méthode est une fonction qui est liée à une structure de données. Comme en Python, les méthodes Rust sont définies avec pour premier paramètre « self ». Une méthode est appelée selon le schéma habituel objet.methode(). Voici un exemple de la méthode surface(), liée à une structure de données « struct » :

// Définition « struct »
struct Rectangle {
    largeur: u32,
    longueur: u32,
}
// Impémentation « struct »
impl Rectangle {
    fn surface(&self) -> u32 {
        return self.largeur * self.longueur;
    }
}
let rectangle = Rectangle {
    largeur: 30,
    longueur: 50,
};
println!("La surface du rectangle est de {}.", rectangle.surface());

Types de données et struc­tures de données

Rust est une langue de typage statique. Con­trai­re­ment aux langages dits dy­na­miques comme Python, Ruby, PHP ou Ja­vaS­cript, Rust exige que le type de chaque variable soit connu au moment de la com­pi­la­tion.

Types de données élé­men­taires (Pri­mi­tives)

Comme beaucoup de langages de pro­gram­ma­tion, Rust connaît aussi quelques types de données élé­men­taires. Les instances de types de données élé­men­taires ou pri­mi­tives sont dis­tri­buées sur la mémoire du stack, ce qui permet d’augmenter les per­for­mances. De plus, les valeurs des types de données élé­men­taires peuvent être définies en utilisant une syntaxe « littérale ». Cela signifie que les valeurs peuvent être sim­ple­ment écrites.

Type de données Ex­pli­ca­tions An­no­ta­tion
Integer Nombre entier i8, u8, etc.
Floating point Nombre à virgule f64, f32
Boolean Valeur True ou False bool
Character Lettre Unicode unique char
String Chaîne de ca­rac­tères Unicode str

Bien que Rust soit une langue de typage statique, le type d’une valeur ne doit pas toujours être déclaré ex­pli­ci­te­ment. Dans de nombreux cas, le type peut être déduit par le com­pi­la­teur grâce au contexte (« type inference »). Autrement, le type est ex­pli­ci­te­ment spécifié par une an­no­ta­tion. Dans certains cas, cela peut même être obli­ga­toire :

  • Le type retour d’une fonction doit toujours être clai­re­ment spécifié.
  • Le type d’une constante doit toujours être clai­re­ment spécifié.
  • Les chaînes lit­té­rales doivent être spé­cia­le­ment ma­ni­pu­lées pour que leur taille soit connue au moment de la com­pi­la­tion.

Voici quelques exemples clairs d’ins­tan­cia­tion de types de données élé­men­taires avec une syntaxe littérale :

// ici, le compilateur reconnaît automatiquement le type de variable
let cents = 42;
// Type annotation : chiffre positif (‚u8’ = "unsigned, 8 bits")
let age: u8 = -15; // Erreur de compilation, car la valeur donnée est négative
// Chiffre à virgule
let angle = 38.5;
// équivalent à
let angle: f64 = 38.5;
// valeur true
let connexion_utilisateur = true;
// équivalent à
let connexion_utilisateur: bool = true;
// guillemets simples pour les lettres
let lettre = ‘a’;
// Guillemets doubles pour les chaînes statiques
let nom = "Dupont";
// avec type explicite
let nom: &’static str = "Dupont";
// autrement : « String » dynamique avec ‚String::from()’
let nom: String = String::from("Dupont");

Types de données composées

Les types de données élé­men­taires cor­res­pon­dent à des valeurs in­di­vi­duelles, tandis que les types de données composées re­grou­pent plusieurs valeurs. Rust fournit au pro­gram­meur une poignée de types de données composées.

Les instances de types de données composées sont at­tri­buées au stack comme les instances de types de données élé­men­taires. Pour que cela fonc­tionne, les instances doivent avoir une taille fixe. Cela signifie également qu’elles ne peuvent pas être modifiées ar­bi­trai­re­ment après l’ins­tan­cia­tion. Voici un aperçu des types de données composées les plus im­por­tants de Rust :

Type de données Ex­pli­ca­tions Type d’éléments Syntaxe littérale
Array Liste de plusieurs valeurs Type similaire [a1, a2, a3]
Tuple Ar­ran­ge­ment de plusieurs valeurs Tout type (t1, t2)
Struct Re­grou­pe­ment de plusieurs valeurs nommées Tout type
Enum Liste Tout type

Examinons d’abord une structure de données avec « struct ». Nous dé­fi­nis­sons une personne avec trois champs nommés :

struct Personne = {
    prénom: String,
    nomfamille: String,
    age: u8,
}

Pour re­pré­sen­ter une personne concrète, nous ins­tan­cions « struct » :

let joueur = Personne {
    prenom: String::from("Jean"),
    nomfamille: String::from("Dupont"),
    age: 42,
};
// accéder au champs d’une instance « struct »
println!("L’age du joueur est: {}", joueur.age);

« enum » (abré­via­tion de « énu­mé­ra­tion ») re­pré­sente les variantes possibles d’une propriété. Nous l’il­lus­trons ici par un exemple simple des quatre couleurs possibles d’une carte à jouer :

enum CouleurCarte {
    Trefle,
    Pique,
    Coeur,
    Caro,
}
// la couleur d’une carte à jouer spécifique
let couleur = CouleurCarte::Trefle;

Rust comprend le mot clé « match » comme pattern matching. La fonc­tion­na­lité est com­pa­rable à la mention « switch » d’autres langues. Voici un exemple :

// déterminer le symbole appartenant à la couleur d’une carte
fn carte_symbole(Couleur: CouleurCarte) -> &’static str {
    match couleur {
        CouleurCarte::Trefle => "♣︎",
        CouleurCarte::Pique => "♠︎",
        CouleurCarte::Coeur => "♥︎",
        CouleurCarte::Caro => "♦︎",
    }
}
println!("Symbol: {}", carte_symbole(CouleurCarte::Trefle)); // renvoie le symbole ♣︎

Un tuple est un ar­ran­ge­ment de plusieurs valeurs, qui peuvent être de dif­fé­rents types. Chacune des valeurs du tuple peut être attribuée à plusieurs variables grâce à une dés­truc­tu­ra­tion. Si l’une des valeurs n’est pas né­ces­saire, le trait de sou­lig­ne­ment (_) est utilisé comme caractère de rem­pla­ce­ment, comme il est d’usage dans Haskell, Python et Ja­vaS­cript. Voici un exemple :

// Définir un jeu de cartes comme tuple
let jeu: (CouleurCarte, u8) = (CouleurCarte::Coeur, 7);
// attribuer les valeurs d’un tuple à plusieurs variables
let (couleur, valeur) = jeu;
// si l’on n’a besoin que de la valeur
let (_, valeur) = jeu;

Comme les valeurs des tuples sont ordonnées, elles sont également ac­ces­sibles via un index. L’in­dexa­tion ne se fait pas entre crochets, mais à l’aide d’une syntaxe de points. Dans la plupart des cas, la dés­truc­tu­ra­tion devrait permettre d’obtenir un code plus lisible :

let nom = ("Jean", "Dupont");
let prenom = nom.0;
let nomfamille = nom.1;

Apprendre les cons­truc­tions de pro­gram­ma­tion su­pé­rieure de Rust

Struc­tures de données dy­na­miques

Les types de données composées déjà in­tro­duits ont en commun que leurs instances sont assignées sur le stack. La bi­blio­thèque standard de Rust contient également un certain nombre de struc­tures de données dy­na­miques cou­ram­ment utilisées. Les instances de ces struc­tures de données sont at­tri­buées sur le heap. Cela signifie que la taille des instances peut être modifiée par la suite. Voici un bref aperçu des struc­tures de données dy­na­miques fré­quem­ment utilisées :

Type de données Ex­pli­ca­tions
Vector liste dynamique de plusieurs valeurs du même type
String séquence dynamique de lettres Unicode
HashMap at­tri­bu­tion dynamique de paires clés-valeurs

Voici un exemple de vecteur en crois­sance dynamique :

// Déclarer le vecteur comme modifiable avec « mut »
let mut noms = Vec::new();
// ajouter une valeur au vecteur
noms.push("Jim");
noms.push("Jack");
noms.push("John");

La pro­gram­ma­tion orientée objet (POO) avec Rust

Con­trai­re­ment aux langages tels que C++ et Java, Rust ne connaît pas la notion de classe. Néanmoins, il est possible de pro­gram­mer selon la mé­tho­do­lo­gie OOP. Cette dernière a pour base les types de données déjà présentés. Le type « struct », en par­ti­cu­lier, peut être utilisé pour définir la structure des objets.

De plus, Rust a aussi des « traits ». Un trait regroupe un ensemble de méthodes qui peuvent ensuite être mises en œuvre de n'importe quel type. Un trait comprend les dé­cla­ra­tions de méthodes, mais peut aussi contenir des im­plé­men­ta­tions.

Un trait existant peut être im­plé­menté par dif­fé­rents types. En outre, un type peut im­plé­men­ter plusieurs traits. Rust permet donc de composer des fonc­tion­na­li­tés pour dif­fé­rents types sans avoir un héritage commun.

Mé­ta­pro­gram­ma­tion

Comme de nombreux autres langages de pro­gram­ma­tion, Rust permet d'écrire du code pour la mé­ta­pro­gram­ma­tion. On peut le résumé par du code qui génère un autre code. Dans Rust, cela inclut d'une part les « macros » que vous con­nais­sez peut-être en C/C++. Les macros se terminent par un point d'ex­cla­ma­tion (!); la macro « println! » pour la sortie de texte sur la ligne de commande a déjà été utilisée plusieurs fois dans cet article.

D'autre part, Rust connaît aussi les « generics ». Ceux-ci vous per­met­tent d'écrire des codes qui résument plusieurs types. Les gé­né­riques sont assez com­pa­rables aux templates en C++ ou à ce qui est également désigné par le terme generics en Java. Un générique souvent utilisé dans Rust est « Option<T> » , qui reprend la dualité « None »/ « Some(T) » pour tout type "T".

En résumé

Rust, en tant que langages de pro­gram­ma­tion système per­for­mant, a le potentiel de remplacer les favoris bien implantés que sont C et C++.

Aller au menu principal