Pourquoi choisir Rust ?
- Rust est un langage de programmation innovant qui permet à chacun d'écrire des logiciels fiables et performants
- Il peut remplacer C/C++, offrant des performances similaires tout en éliminant de nombreux bogues courants durant la compilation
- C'est un langage polyvalent particulièrement adapté aux scénarios suivants :
- Besoin de haute performance d'exécution
- Sécurité mémoire critique
- Utilisation optimale des processeurs multi-cœurs
Comparaison avec d'autres langages
- C/C++ : excellentes performances mais système de types et gestion mémoire peu sûrs
- Java/C# : dispose d'un ramasse-miettes garantissant la sécurité mémoire et offrant de nombreuses fonctionnalités, mais performances limitées
- Rust :
- Sûr
- Aucun ramasse-miettes requis (hautes performances)
- Maintenance, débogage et sécurité du code facilités
Domaines d'expertise de Rust
- Services Web hautes performances (API Web)
- WebAssembly
- Outils en ligne de commande
- Programmation réseau
- Équipements embarqués
- Programmation système
Rust et Firefox
- Rust est initialement un projet de recherche de Mozilla. Firefox est un exemple important de son application commerciale.
- Mozilla a toujours utilisé Rust pour créer Servo, un moteur de navigateur expérimental où tout est exécuté en parallèle.
- Des parties de Servo ont été intégrées dans Firefox
- La version Quantum de Firefox incluait le moteur de rendu CSS de Servo, permettant d'importantes améliorations de performance grâce à Rust
Utilisateurs et cas d'utilisation de Rust
- Google : nouveau système d'exploitation Fuchsia, environ 30% du code écrit en Rust
- Amazon : système d'exploitation basé sur Linux capable d'exécuter des conteneurs directement sur le matériel ou dans des machines virtuelles
- System76, Baidu, Huawei, Ant Financial...
Avantages de Rust
- Performance
- Sécurité
- Concurrence sans crainte
Inconvénients de Rust
- Courbe d'apprentissage élevée ("difficile à apprendre")
Remarque importante
- Rust introduit de nombreux concepts uniques qu'il faut découvrir progressivement
Installation de Rust
Site officiel : https://www.rust-lang.org/learn/get-started
Windows : Suivre les instructions du site officiel
Installation Mac :
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Mise à jour et désinstallation de Rust
-
Mettre à jour Rust : ``` rustup update
-
Désinstaller Rust : ``` rustup self uninstall
Vérification de l'installation
rustc --version- Format : rustc x.y.z(abcdbcdbc yyyy-mm-dd)
- Affiche la dernière version stable : numéro de version, hash commit, date du commit
Documentation locale
- Lors de l'installation de Rust, la documentation est également installée localement pour navigation hors-ligne
- Exécuter
rustup docpour ouvrir la documentation locale dans le navigateur
cargo --version
cargo 1.67.1 (8ecd4f20a 2023-01-10)
~
➜ rustc --version
rustc 1.67.1 (d5a82bbd2 2023-02-07)
~
➜ rustup doc
Outils de développement
- Visual Studio Code
- Extension Rust
- PyCharm (suite IntelliJ IDEA)
- Extension Rust
Exemple Hello World
Écriture d'un programme Rust
- Extension des fichiers programmes : rs
- Convention de nommage : hello_world.rs
mkdir rust
~
➜ cd rust
~/rust
➜ mkdir hello_world
~/rust
➜ cd hello_world
~/rust/hello_world
➜ code .
~/rust/hello_world
➜ pwd
/Users/qiaopengjun/rust/hello_world
~/rust/hello_world via 🦀 1.67.1
➜ mv hello_world.rs main.rs
~/rust/hello_world via 🦀 1.67.1
➜ rustc main.rs
~/rust/hello_world via 🦀 1.67.1
➜ ls
main main.rs
➜ ./main
Hello World!
Fichier main.rs
fn main() {
println!("Hello World!");
}
Anatomie d'un programme Rust
- Définition de fonction :
fn main(){}- Aucun paramètre, aucune valeur de retour
- La fonction
mainest spéciale : c'est le point d'entrée de chaque programme Rust exécutable - Affichage de texte :
println!("Hello, world!");- L'indentation en Rust utilise 4 espaces au lieu de tabulations
println!est une macro Rust (macro)- S'il s'agissait d'une fonction, il n'y aurait pas de !
- "Hello World" est une chaîne de caractères, qui est un paramètre de
println! - Cette ligne se termine par ;
Compilation et exécution sont des étapes séparées
- Avant d'exécuter un programme Rust, il doit être compilé avec la commande :
rustcnom_du_fichier_sourcerustc main.rs
- Après compilation réussie, un fichier binaire est généré
- Sous Windows, un fichier .pdb est également créé, contenant des informations de débogage
- Rust est un langage compilé à l'avance (ahead-of-time compilation)
- Le programme peut être compilé puis l'exécutable transmis à d'autres pour exécution (sans installation de Rust)
rustcconvient uniquement aux programmes Rust simples...
Bienvenue dans Cargo
Cargo
- Cargo est le système de construction et le gestionnaire de paquets de Rust
- Construction du code, téléchargement des bibliothèques dépendantes, construction de ces bibliothèques...
- L'installation de Rust inclut Cargo
Cargo --version
~/rust/hello_world via 🦀 1.67.1
➜ cargo --version
cargo 1.67.1 (8ecd4f20a 2023-01-10)
Création d'un projet avec Cargo
- Création du projet :
cargo new hello_cargo- Le nom du projet est également
hello_cargo - Un nouveau répertoire
hello_cargoest créé- Cargo.toml
- Répertoire src
main.rs
- Initialisation d'un nouveau dépôt Git
.gitignore- Vous pouvez utiliser un autre système de contrôle de version ou aucun : utilisez le drapeau --vcs avec cargo new
- Le nom du projet est également
Cargo.toml
- Format TOML (Tom's Obvious, Minimal Language), format de configuration de Cargo
- [package], est un titre de section, indiquant que le contenu ci-dessous configure le paquet (package)
- name nom du projet
- version version du projet
- authors auteurs du projet
- edition version de Rust utilisée
- [dependencies] début d'une autre section, listant les dépendances du projet
- Dans Rust, les paquets de code sont appelés crates
src/main.rs
- Le main.rs généré par Cargo se trouve dans le répertoire src
- Cargo.toml se trouve au niveau racine du projet
- Le code source doit se trouver dans le répertoire src
- Le répertoire racine peut contenir : README, informations de licence, fichiers de configuration et autres fichiers non liés au code source
- Si le projet n'a pas été créé avec cargo, vous pouvez le convertir pour utiliser cargo :
- Déplacer les fichiers sources dans src
- Créer Cargo.toml et renseigner les configurations appropriées
Construction d'un projet Cargo cargo build
- cargo build
- Crée un exécutable : target/debug/hello_cargo ou target\debug\hello_cargo.exe (Windows)
- Exécuter l'exécutable :
./target/debug/hello_cargoou.\target\debug\hello_cargo.exe(Windows)
- La première exécution de
cargo buildgénère un fichier cargo.lock au niveau racine- Ce fichier suit les versions précises des dépendances du projet
- Ne pas modifier ce fichier manuellement
Construction et exécution d'un projet cargo cargo run
- cargo run compile le code + exécute le résultat
- Si la compilation précédente a réussi et que le code source n'a pas changé, exécute directement le fichier binaire
cargo check
- cargo check vérifie le code pour s'assurer qu'il compila correctement, mais ne produit aucun fichier exécutable
- cargo check est beaucoup plus rapide que cargo build
- Pendant l'écriture du code, vous pouvez utiliser cargo check continuellement pour vérifier votre code et améliorer l'efficacité
Compilation pour la production
- cargo build --release
- Compile avec optimisations
- Le code s'exécutera plus rapidement, mais la compilation prendra plus de temps
- Génère l'exécutable dans target/release au lieu de target/debug
- Compile avec optimisations
- Deux configurations
- Développement
- Production
Utilisez de préférence Cargo
Jeu de devinette - Une tentative
Jeu de devinette - Objectif
- Générer un nombre aléatoire entre 1 et 100
- Demander au joueur d'entrer une supposition
- Après avoir deviné, le programme indique si la supposition est trop petite ou trop grande
- Si la supposition est correcte, afficher un message de félicitations et quitter le programme
Écriture du code
use std::io; // prelude
fn main() {
println!("Deviner le nombre !");
println!("Devinez un nombre");
// let mut foo = 1;
// let bar = foo; // immutable
// foo = 2;
let mut user_input = String::new();
io::stdin().read_line(&mut user_input).expect("Impossible de lire la ligne");
// io::Result Ok Err
println!("Votre supposition est : {}", user_input);
}
Jeu de devinette - Génération du nombre mystère
[rang]https://crates.io/crates/rand
cd rust/guessing_game
guessing_game on main [✘!] is 📦 0.1.0 via 🦀 1.67.1
➜ cargo build
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.17
Compiling libc v0.2.139
Compiling getrandom v0.2.8
Compiling rand_core v0.6.4
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (/Users/qiaopengjun/rust/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.08s
guessing_game on main [✘!] is 📦 0.1.0 via 🦀 1.67.1
➜ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
guessing_game on main [✘!] is 📦 0.1.0 via 🦀 1.67.1
➜ cargo update
Nombres aléatoires
Code :
use std::io; // prelude
use rand::Rng; // trait
fn main() {
println!("Deviner le nombre !");
let secret_value = rand::thread_rng().gen_range(1..101);
println!("Le nombre secret est : {}", secret_value);
println!("Devinez un nombre");
// let mut foo = 1;
// let bar = foo; // immutable
// foo = 2;
let mut user_input = String::new();
io::stdin().read_line(&mut user_input).expect("Impossible de lire la ligne");
// io::Result Ok Err
println!("Votre supposition est : {}", user_input);
}
Jeu de devinette - Comparaison de la supposition avec le nombre mystère
use std::io; // prelude
use rand::Rng; // trait
use std::cmp::Ordering; // type énuméré trois variantes (valeurs)
fn main() {
println!("Deviner le nombre !");
let secret_value = rand::thread_rng().gen_range(1..101);
println!("Le nombre secret est : {}", secret_value);
println!("Devinez un nombre");
// let mut foo = 1;
// let bar = foo; // immutable
// foo = 2;
let mut user_input = String::new();
io::stdin().read_line(&mut user_input).expect("Impossible de lire la ligne");
// io::Result Ok Err
// shadow
let user_input: u32 = user_input.trim().parse().expect("Veuillez entrer un nombre !"); // \n
println!("Votre supposition est : {}", user_input);
match user_input.cmp(&secret_value) {
Ordering::Less => println!("Trop petit !"), // arm
Ordering::Greater => println!("Trop grand !"),
Ordering::Equal => println!("Vous avez gagné !"),
}
}
Jeu de devinette - Permettre plusieurs tentatives
use std::io; // prelude
use rand::Rng; // trait
use std::cmp::Ordering; // type énuméré trois variantes (valeurs)
fn main() {
println!("Deviner le nombre !");
let secret_value = rand::thread_rng().gen_range(1..101);
// println!("Le nombre secret est : {}", secret_value);
loop {
println!("Devinez un nombre");
// let mut foo = 1;
// let bar = foo; // immutable
// foo = 2;
let mut user_input = String::new();
io::stdin().read_line(&mut user_input).expect("Impossible de lire la ligne");
// io::Result Ok Err
// shadow
let user_input: u32 = match user_input.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Veuillez entrer un nombre valide");
continue;
}
};
println!("Votre supposition est : {}", user_input);
match user_input.cmp(&secret_value) {
Ordering::Less => println!("Trop petit !"), // arm
Ordering::Greater => println!("Trop grand !"),
Ordering::Equal => {
println!("Vous avez gagné !");
break;
}
}
}
}
Concepts de programmation généraux
- Variables et mutabilité
- Types de données
- Types scalaires
- Types composés
- Fonctions
- Commentaires
1 Variables et mutabilité
- Déclaration de variable avec le mot-clé
let - Par défaut, les variables sont immuables (Immutable)
- (exemple variables)
- Lors de la déclaration d'une variable, ajouter
mutavant la variable la rend mutable.
~/rust
➜ cargo new variables
Created binary (application) `variables` package
~/rust
➜ cd var*
variables on master [?] via 🦀 1.67.1
➜ code .
variables on master [?] via 🦀 1.67.1
➜
Code :
fn main() {
println!("Hello, world!");
let mut x = 5;
println!("La valeur de x est {}", x);
x = 6;
println!("La valeur de x est {}", x);
}
Variables et constantes
- Constantes (constant), après liaison à une valeur, les constantes sont également immuables, mais elles diffèrent des variables immuables :
- Ne peut pas utiliser
mut, les constantes sont toujours immuables - Déclaration avec le mot-clé
const, le type doit être annoté - Peut être déclarée dans n'importe quel scope, y compris le scope global
- Ne peut être liée qu'à des expressions constantes, pas à des résultats d'appel de fonction ou à des valeurs calculées à l'exécution
- Ne peut pas utiliser
- Pendant l'exécution du programme, les constantes restent valides dans leur scope de déclaration
- Convention de nommage : en Rust, les constantes utilisent des majuscules complètes, les mots séparés par des tirets bas, par exemple :
MAX_POINTS - Exemple :
const MAX_POINTS: u32 = 100_000;
Shadowing (Masquage)
- Vous pouvez déclarer une nouvelle variable avec le même nom, la nouvelle variable masquera (shadow) la variable précédemment déclarée avec le même nom
- Dans le code ultérieur, ce nom représentera la nouvelle variable
- Shadow diffère de marquer une variable comme mut :
- Si vous n'utilisez pas le mot-clé let, réassigner une valeur à une variable non mut entraînera une erreur de compilation
- Avec let, la nouvelle variable homonyme est également immuable
- La nouvelle variable homonyme déclarée avec let peut avoir un type différent du précédent
// const MAX_POINTS: u32 = 100_000;
fn main() {
// const MAX_POINTS: u32 = 100_000;
println!("Hello, world!");
let mut x = 5;
println!("La valeur de x est {}", x);
x = 6;
println!("La valeur de x est {}", x);
let x = x + 1;
let x = x * 2;
println!("La valeur de x est {}", x);
let spaces = " ";
let spaces = spaces.len();
println!("La longueur de spaces est {}", spaces);
let user_guess: u32 = "42".parse().expect("Pas un nombre");
println!("La supposition est {}", user_guess);
}
2 Types de données
- Types scalaires et composés
- Rust est un langage compilé statiquement, tous les types de variables doivent être connus à la compilation
- Selon les valeurs utilisées, le compilateur peut généralement déduire leur type spécifique
- Mais si les types possibles sont nombreux (comme la méthode parse qui convertit une String en entier), vous devez ajouter des annotations de type ou la compilation échouera
Types scalaires
- Un type scalaire représente une seule valeur
- Rust a quatre types scalaires principaux :
- Types entiers
- Types flottants
- Type booléen
- Type caractère
Types entiers
- Les types entiers n'ont pas de partie décimale
- Par exemple u32 est un type entier non signé occupant 32 bits
- Les types entiers non signés commencent par u
- Les types entiers signés commencent par i
- Liste des types entiers Rust :
- Chacun a une version i et u, avec un nombre fixe de bits
- Plage signée : -(2^(n-1)) à 2^(n-1)-1
- Plage non signée : 0 à 2^n-1
| longueur | signé | non signé |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
Types isize et usize
- Le nombre de bits des types isize et usize est déterminé par l'architecture de l'ordinateur sur lequel le programme s'exécute :
- Sur un ordinateur 64 bits, ce sont des types 64 bits
- ...
- Scénario principal d'utilisation de isize ou usize : effectuer des opérations d'indexation sur certaines collections.
Littéraux entiers
Décimal, hexadécimal, octal, binaire, byte
- À l'exception du type byte, tous les littéraux numériques acceptent l'utilisation de suffixes de type. Par exemple 57u8
- Si vous n'êtes pas sûr du type à utiliser, vous pouvez utiliser le type par défaut correspondant de Rust
- Le type entier par défaut est i32, globalement rapide même sur les systèmes 64 bits
Dépassement d'entier
- Par exemple : la plage de u8 est 0-255, si vous définissez la valeur d'une variable u8 à 256, alors :
- Compilation en mode débogage : Rust vérifie le dépassement d'entier, si dépassement se produit, le programme paniquera à l'exécution
- Compilation en mode release (--release) : Rust ne vérifie pas le dépassement d'entier susceptible de provoquer un plantage
- Si dépassement : Rust effectue une opération de "roulement" : 256 devient 0, 257 devient 1... mais le programme ne panique pas
Types flottants
- Rust a deux types flottants de base, c'est-à-dire des types contenant une partie décimale
- f32, 32 bits, simple précision
- f64, 64 bits, double précision
- Les types flottants de Rust utilisent la norme IEEE-754 pour la représentation
- f64 est le type par défaut, car sur les CPU modernes, f64 et f32 ont des vitesses similaires, mais f64 offre une meilleure précision.
Opérations numériques
- Addition, soustraction, multiplication, division, modulo, etc.
Type booléen
- Le type booléen de Rust a également deux valeurs : true et false
- Taille d'un octet
- Symbole bool
Type caractère
- Dans le langage Rust, le type char est utilisé pour décrire le caractère unique le plus élémentaire du langage
- Les littéraux de caractères utilisent des guillemets simples
- Taille de 4 octets
- Est une valeur scalaire Unicode, pouvant représenter bien plus de caractères que ASCII : pinyin, chinois-japonais-coréen, caractères blancs de longueur nulle, émojis, etc.
- U+0000 à U+D7FF
- U+E000 à U+10FFFF
- Mais Unicode ne contient pas le concept de "caractère", donc ce que vous considérez intuitivement comme un caractère peut ne pas correspondre au concept de Rust
// const MAX_POINTS: u32 = 100_000;
fn main() {
// const MAX_POINTS: u32 = 100_000;
println!("Hello, world!");
let mut x = 5;
println!("La valeur de x est {}", x);
x = 6;
println!("La valeur de x est {}", x);
let x = x + 1;
let x = x * 2;
println!("La valeur de x est {}", x);
let spaces = " ";
let spaces = spaces.len();
println!("La longueur de spaces est {}", spaces);
let user_guess: u32 = "42".parse().expect("Pas un nombre");
println!("La supposition est {}", user_guess);
// let x = 2.0; // f64
// let y: f32 = 3.0; // f32
// let sum = 5 + 10;
// let difference = 95.5 - 4.3;
// let product = 4 * 30;
// let quotient = 56.7 / 32.2;
// let reminder = 54 % 5;
// let t = true;
// let f: bool = false;
// let x = 'z';
// let y: char = 'a';
// let z = '😘';
}
Types composés
- Les types composés peuvent placer plusieurs valeurs dans un seul type
- Rust fournit deux types composés de base : tuples (Tuple), tableaux
Tuple
- Un tuple peut placer plusieurs valeurs de plusieurs types dans un seul type
- La longueur d'un tuple est fixe : une fois déclaré, elle ne peut pas changer
Création d'un tuple
- Dans des parenthèses, séparer les valeurs par des virgules
- Chaque position d'un tuple correspond à un type, les types des éléments du tuple n'ont pas besoin d'être identiques
Obtention des valeurs d'éléments d'un tuple
- Vous pouvez utiliser la correspondance de motifs pour déstructurer (destructure) un tuple afin d'obtenir les valeurs des éléments
Accès aux éléments d'un tuple
- Utiliser la notation point sur la variable tuple, suivie de l'indice de l'élément
fn main() {
let tuple_data: (i32, f64, u8) = (500, 6.4, 1);
println!("{}, {}, {}", tuple_data.0, tuple_data.1, tuple_data.2);
let (x, y, z) = tuple_data;
println!("{}, {}, {}", x, y, z);
}
Tableau
- Un tableau peut également placer plusieurs valeurs dans un seul type
- Chaque élément du tableau doit être du même type
- La longueur du tableau est également fixe
Déclaration d'un tableau
- Dans des crochets, séparer les valeurs par des virgules
Utilité des tableaux
- Si vous voulez que vos données soient stockées sur la pile (stack) plutôt que sur le tas (heap), ou si vous voulez garantir un nombre fixe d'éléments, utiliser un tableau est plus avantageux
- Les tableaux ne sont pas aussi flexibles que les vecteurs
- Les vecteurs sont similaires aux tableaux, fournis par la bibliothèque standard
- La longueur des vecteurs peut changer
- Si vous n'êtes pas sûr d'utiliser un tableau ou un vecteur, vous devriez probablement utiliser un vecteur
Type de tableau
- Le type d'un tableau est représenté sous cette forme : [type; longueur] par exemple :
let a: [i32; 5] = [1, 2, 3, 4, 5];
Autre méthode de déclaration d'un tableau
- Si chaque élément du tableau a la même valeur, vous pouvez :
- Dans les crochets, spécifier la valeur initiale
- Puis un ";
- Enfin la longueur du tableau
- Par exemple :
let a = [3; 5];équivaut à :let a = [3, 3, 3, 3, 3];
Accès aux éléments d'un tableau
- Le tableau est alloué sur un seul bloc en pile
- Vous pouvez accéder aux éléments du tableau en utilisant des indices
- Si l'indice dépasse la plage du tableau, alors :
- La compilation passera
- À l'exécution, une erreur se produira (panique à l'exécution)
- Rust n'autorisera pas l'accès à la mémoire correspondante
fn main() {
// let a = [1, 2, 3, 4, 5];
let months = [
"Janvier",
"Février",
"Mars",
"Avril",
"Mai",
"Juin",
"Juillet",
"Août",
"Septembre",
"Octobre",
"Novembre",
"Décembre",
];
// let first = months[0];
// let second = months[1];
// let index = 15;
let index = [12, 13, 14, 15];
let month = months[index[1]];
println!("{}", month);
}
2 Fonctions
- Déclaration de fonction avec le mot-clé fn
- Par convention, pour les noms de fonctions et de variables, Rust utilise la convention snake_case
- Toutes les lettres sont minuscules, les mots séparés par des traits de soulignement
fn main() {
another_function();
}
fn another_function() {
println!("Une autre fonction");
}
Paramètres de fonciton
- paramètres, arguments (paramètres formels, paramètres réels)
- Dans la signature de la fonction, le type de chaque paramètre doit être déclaré
fn main() {
another_function(5, 6); // argument
}
fn another_function(x: i32, y: i32) { // paramètre
println!("la valeur de x est {}", x);
println!("la valeur de y est {}", y);
}
Instructions et expressions dans le corps d'une fonction
- Le corps d'une fonction est composé d'une série d'instructions, optionnellement terminé par une expression
- Rust est un langage basé sur les expressions
- Les instructions sont des commandes qui effectuent certaines actions
- Les expressions évaluent et produisent une valeur
- La définition d'une fonction est également une instruction
- Les instructions ne retournent pas de valeur, donc vous ne pouvez pas utiliser let pour affecter une instruction à une variable
fn main() {
let y = 6; // instruction
// let x = (let y = 6); // erreur
let x = 5;
let y = {
let x = 1;
x + 3
};
println!("La valeur de y est {}", y)
}
Valeur de retour d'une fonction
- Déclarer le type de retour d'une fonction après le symbole ->, mais on ne peut pas nommer la valeur de retour
- Dans Rust, la valeur de retour est la valeur de la dernière expression dans le corps de la fonction
- Pour retourner prématurément, utilisez le mot-clé return et spécifiez une valeur
- La plupart des fonctions utilisent par défaut la dernière expression comme valeur de retour
fn plus_five(x: i32) -> i32 {
x + 5
}
fn main() {
let x = plus_five(6);
println!("La valeur de x est {}", x);
}
Commentaires
- Rust a également des "commentaires de documentation"
// Ceci est une fonction
fn five(x: i32) -> i32 {
x + 5
}
/* sdgagaf
sfgagaga */
// Ceci est la fonction principale
// Le point d'entrée
fn main() {
// appeler five()
let x = five(6); // 5 + 6
println!("La valeur de x est : {}", x);
}
3 Flux de contrôle
Expression if
- L'expression if vous permet d'exécuter différentes branches de code selon une condition
- Cette condition doit être de type bool
- Dans l'expression if, le bloc de code associé à la condition s'appelle la branche (arm)
- Optionnellement, vous pouvez ajouter une expression else à la suite
fn main() {
println!("Hello, world!");
let number = 7;
if number < 5 {
println!("la condition était vraie");
} else {
println!("la condition était fausse");
}
}
Gestion des conditions multiples avec else if
- Si vous utilisez plus d'un else if, il vaut mieux refactoriser le code en utilisant match
fn main() {
let number = 6;
if number % 4 == 0 {
println!("le nombre est divisible par 4");
} else if number % 3 == 0 {
println!("le nombre est divisible par 3");
} else if number % 2 == 0 {
println!("le nombre est divisible par 2");
} else {
println!("le nombre n'est pas divisible par 4, 3 ou 2");
}
}
Utilisation de match
fn main() {
let number = 6;
match number {
number if number % 4 == 0 => println!("le nombre est divisible par 4"),
number if number % 3 == 0 => println!("le nombre est divisible par 3"),
number if number % 2 == 0 => println!("le nombre est divisible par 2"),
_ => println!("le nombre n'est pas divisible par 4, 3 ou 2"),
}
}
Utilisation de If dans une instruction let
- Puisque if est une expression, vous pouvez la placer à droite du signe égal dans une instruction let
fn main() {
let condition = true;
let number = if condition {5} else {6};
println!("La valeur de number est {}", number);
}
Boucles Rust
- Rust fournit 3 types de boucles : loop, while et for
Boucle loop
- Le mot-clé loop indique à Rust d'exécuter un bloc de code en boucle jusqu'à ce que vous arrêtiez
- Vous pouvez utiliser le mot-clé break dans une boucle loop pour indiquer au programme quand arrêter la boucle
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("Le résultat est {}", result);
}
Boucle conditionnelle while
- Vérifier la condition avant chaque exécution du corps de la boucle
fn main() {
let mut number = 3;
while number != 0 {
println!("{}", number);
number = number - 1;
}
println!("DÉCOLLAGE !!!");
}
Parcourir une collection avec une boucle for
- Vous pouvez utiliser while ou loop pour parcourir une collection, mais c'est sujet à erreur et inefficace
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("l'index est {}", a[index]);
index = index + 1;
}
}
- La boucle for est plus concise et compacte, elle peut exécuter du code pour chaque élément de la collection
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("la valeur est {}", element);
}
}
- Grâce à sa sécurité et sa concision, la boucle for est la plus utilisée en Rust
Plage
- Fournie par la bibliothèque standard
- Spécifier un nombre de départ et un nombre de fin, une plage peut générer les nombres entre eux (sans inclure la fin)
- La méthode rev inverse une plage
fn main() {
for number in (1..4).rev() {
println!("{}", number);
}
println!("DÉCOLLAGE !!!");
}
4 Propriété
- La propriété est la caractéristique la plus distinctive de Rust, elle permet à Rust de garantir la sécurité mémoire sans ramasse-miettes
4.1 Qu'est-ce que la propriété
- La caractéristique centrale de Rust est la propriété
- Tous les programmes doivent gérer la manière dont ils utilisent la mémoire de l'ordinateur pendant l'exécution
- Certains langages ont un ramasse-miettes, pendant l'exécution, ils recherchent constamment la mémoire qui n'est plus utilisée
- Dans d'autres langages, le programmeur doit explicitement allouer et libérer la mémoire
- Rust adopte une troisième approche :
- La mémoire est gérée par un système de propriété, comprenant un ensemble de règles que le compilateur vérifie à la compilation
- Lors de l'exécution, la propriété n'affecte pas la vitesse d'exécution du programme
Stack VS Heap
Mémoire Stack vs Mémoire Heap
- Dans un langage de programmation système comme Rust, savoir si une valeur est sur la stack ou sur le heap a un impact plus important sur le comportement du langage et sur vos décisions
- Lors de l'exécution de votre code, la stack et le heap sont toutes deux des mémoires disponibles, mais leur structure est très différente
Stockage des données
- La stack stocke les valeurs dans l'ordre de leur réception, les retire dans l'ordre inverse (dernier entré, premier sorti, LIFO)
- Ajouter des données s'appelle empiler sur la stack
- Retirer des données s'appelle dépiler de la stack
- Toutes les données stockées sur la stack doivent avoir une taille fixe connue
- Les données dont la taille est inconnue à la compilation ou susceptible de changer à l'exécution doivent être stockées sur le heap
- La mémoire heap est moins organisée
- Quand vous placez des données sur le heap, vous demandez un certain espace
- Le système d'exploitation trouve un espace suffisamment grand dans le heap, le marque comme utilisé et retourne un pointeur, c'est-à-dire l'adresse de cet espace
- Ce processus s'appelle allocation sur le heap, parfois simplement "allocation"
- Empiler une valeur sur la stack ne s'appelle pas allocation
- Comme le pointeur a une taille fixe connue, vous pouvez le stocker sur la stack
- Mais si vous voulez les données réelles, vous devez utiliser le pointeur pour localiser
- Empiler des données sur la stack est beaucoup plus rapide que d'allouer sur le heap
- Car le système d'exploitation n'a pas besoin de chercher un espace pour stocker les nouvelles données, cet emplacement est toujours au sommet de la stack
- L'allocation d'espace sur le heap nécessite plus de travail
- Le système d'exploitation doit d'abord trouver un espace suffisamment grand pour contenir les données, puis faire des enregistrements pour les allocations futures
Accès aux données
- L'accès aux données dans le heap est plus lent que l'accès aux données dans la stack, car vous devez utiliser un pointeur pour trouver les données dans le heap
- Pour les processeurs modernes, à cause du cache, plus les instructions sautent dans la mémoire, plus la vitesse est lente
- Si les données sont stockées proches les unes des autres, la vitesse de traitement du processeur est plus rapide (sur la stack)
- Si les données sont éloignées les unes des autres, la vitesse de traitement est plus lente (sur le heap)
- L'allocation de beaucoup d'espace sur le heap prend également du temps
Appel de fonction
- Quand votre code appelle une fonction, les valeurs sont transmises à la fonction (y compris les pointeurs vers le heap). Les variables locales de la fonction sont empilées sur la stack. À la fin de la fonction, ces valeurs sont dépilées de la stack
Raison de l'existence de la propriété
- La propriété résout des problèmes :
- Suivre quelles parties du code utilisent quelles données du heap
- Minimiser la quantité de données dupliquées sur le heap
- Nettoyer les données inutilisées sur le heap pour éviter le manque d'espace
- Une fois que vous comprenez la propriété, vous n'avez plus besoin de penser souvent à la stack ou au heap
- Mais comprendre que la gestion des données heap est la raison de l'existence de la propriété aide à expliquer pourquoi elle fonctionne ainsi
Règles de propriété
- Chaque valeur a une variable, cette variable est le propriétaire de cette valeur
- Chaque valeur ne peut avoir qu'un seul propriétaire à la fois
- Quand le propriétaire sort de portée (scope), la valeur est supprimée
Portée des variables
- Scope est la plage effective d'un élément dans le programme
fn main() {
// s n'est pas disponible
let s = "bonjour"; // s est disponible
// peut effectuer des opérations sur s
} // la portée de s se termine ici, s n'est plus disponible
Type String
- String est plus complexe que les types de données scalaires de base
- Littéraux de chaîne : les valeurs de chaîne codées directement dans le programme. Elles sont immuables
- Rust a un deuxième type de chaîne : String
- Alloué sur le heap. Peut stocker une quantité de texte inconnue à la compilation
Création d'une valeur de type String
- Vous pouvez utiliser la fonction from pour créer un type String à partir d'un littéral de chaîne
- let s = String::from("bonjour");
- "::" indique que from est une fonction du type String
- Ces chaînes peuvent être modifiées
fn main() {
let mut s = String::from("Bonjour");
s.push_str(", Monde");
println!("{}", s);
}
- Pourquoi les valeurs de type String peuvent être modifiées, mais pas les littéraux de chaîne ?
- Parce qu'elles gèrent la mémoire différemment
Mémoire et allocation
- Littéraux de chaîne, le contenu est connu à la compilation, le texte est directement codé dans le fichier exécutable final
- Rapide et efficace. Grâce à leur immuabilité
- Type String, pour prendre en charge la mutabilité, nécessite l'allocation de mémoire sur le heap pour stocker le contenu de texte inconnu à la compilation :
- Le système d'exploittaion doit demander de la mémoire à l'exécution
- Cette étape est réalisée en appelant
String::from
- Cette étape est réalisée en appelant
- Quand vous avez fini avec String, vous devez utiliser un moyen pour retourner la mémoire au système d'exploitation
- Cette étape, dans les langages avec ramasse-miettes, le ramasse-miettes suit et nettoie la mémoire non utilisée
- Sans ramasse-miettes, vous devez identifier quand la mémoire n'est plus utilisée et appeler le code pour la retourner
- Si vous oubliez, vous gaspillez de la mémoire
- Si vous le faites trop tôt, la variable devient illégale
- Si vous le faites deux fois, c'est un bug, une allocation doit correspondre à une libération
- Le système d'exploittaion doit demander de la mémoire à l'exécution
- Rust adopte une approche différente : pour une valeur donnée, quand la variable qui la possède sort de portée, la mémoire est immédiatement rendue automatiquement au système d'exploitation
- Fonction DROP (quand une variable sort de portée, Rust appelle automatiquement cette fonction)
Moyens d'interaction entre variables et données : déplacement (Move)
- Plusieurs variables peuvent interagir avec les mêmes données d'une manière unique
let x = 5;
let y = x;
- Les entiers sont des valeurs simples de taille connue et fixe, les deux 5 sont empilés sur la stack
Version String
let s1 = String::from("bonjour");
let s2 = s1;
- Un String est composé de 3 parties :
- Un pointeur vers la mémoire contenant le contenu de la chaîne (ptr)
- Une longueur (len)
- Une capacité (capacity)
- Ces choses sont placées sur la stack
- La partie contenant le contenu de la chaîne est sur le heap
- Longueur len, c'est le nombre d'octets nécessaires pour le contenu de la chaîne
- Capacité capacity est le nombre total d'octets que String obtient du système d'exploitation
- Quand s1 est affecté à s2, les données String sont copiées :
- Une copie du pointeur, de la longueur et de la capacité est faite sur la stack
- Les données du heap pointées par le pointeur ne sont pas copiées
- Quand une variable quitte la portée, Rust appelle automatiquement la fonction DROP et libère la mémoire heap utilisée par la variable
- Quand s1, s2 quittent la portée, elles essaieront toutes deux de libérer la même mémoire :
- Bug de double libération (double free)
- Pour garantir la sécurité mémoire :
- Rust n'essaie pas de copier la mémoire allouée
- Rust rend s1 invalide
- Quand s1 quitte la portée, Rust n'a rien à libérer
- Copie superficielle (shallow copy)
- Copie profonde (deep copy)
- Vous pourriez considérer la copie du pointeur, de la longueur, de la capacité comme une copie superficielle, mais comme Rust rend s1 invalide, nous utilisons un nouveau terme : déplacement (Move)
- Un principe de conception implicite : Rust ne crée pas automatiquement de copie profonde des données
- Pour les performances d'exécution, toute opération d'affectation automatique est économique
Moyens d'interaction entre variables et données : clonage (Clone)
- Si vous voulez vraiment effectuer une copie profonde des données String sur le heap, et non seulement des données sur la stack, vous pouvez utiliser la méthode Clone
fn main() {
let s1 = String::from("Bonjour");
let s2 = s1.clone();
println!("{}, {}", s1, s2);
}
Données sur la Stack : copie
fn main() {
let x = 5;
let y = x;
println!("{}, {}", x, y);
}
- Copy trait, peut être utilisé pour les types comme les entiers qui sont entièrement stockés sur la stack
- Si un type implémente le trait Copy, l'ancienne variable reste utilisable après l'affectation
- Si un type ou une partie de celui-ci implémente le trait Drop, Rust n'autorise pas ce type à implémenter le trait Copy
Quelques types ayant le trait Copy
- Tout type composite de scalaires simples peut être Copy
- Tout type nécessitant l'allocation de mémoire ou d'autres ressources n'est pas Copy
- Quelques types ayant le trait Copy :
- Tous les types entiers, par exemple u32
- bool
- char
- Tous les types flottants, par exemple f64
- Tuples, si tous leurs champs sont Copy
- (i32, i32) l'est
- (i32, String) ne l'est pas
Propriété et fonctions
- Sémantiquement, passer des valeurs à une fonction est similaire à affecter des valeurs à des variables :
- Passer une valeur à une fonction entraîne un déplacement ou une copie
fn main() {
let s = String::from("Bonjour, Monde !");
take_ownership(s);
let x = 5;
makes_copy(x);
println!("x: {}", x);
}
fn take_ownership(some_string: String) {
println!("{}", some_string)
}
fn makes_copy(some_number: i32) {
println!("{}", some_number);
}
Valeurs de retour et portée
- Les fonctions transfèrent également la propriété lors du retour de valeurs
fn main() {
let s1 = gives_ownership();
let s2 = String::from("bonjour");
let s3 = takes_and_gives_back(s2);
}
fn gives_ownership() -> String {
let some_string = String::from("bonjour");
some_string
}
fn takes_and_gives_back(a_string: String) -> String {
a_string
}
- La propriété d'une variable suit toujours le même modèle :
- Un déplacement se produit quand une valeur est affectée à une autre variable
- Quand une variable contenant des données heap quitte la portée, ses données sont nettoyées par la fonction Drop, sauf si la propriété des données a été transférée à une autre variable
Comment permettre à une fonction d'utiliser une valeur sans acquérir sa propriété ?
fn main() {
let s1 = String::from("bonjour");
let (s2, len) = calculate_length(s1);
println!("La longueur de '{}' est {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length)
}
- Rust a une caractéristique appelée "référence (Reference)"
4.2 Références et emprunt
fn main() {
let s1 = String::from("Bonjour");
let len = calculate_length(&s1);
println!("La longueur de '{}' est {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
- Le type du paramètre est &String au lieu de String
- Le symbole & indique une référence : vous permet de référencer certaines valeurs sans en acquérir la propriété
Emprunt
- Nous appelons emprunt le fait de passer une référence comme paramètre de fonction
- Peut-on modifier ce qui est emprunté ? Non
- Comme les variables, les références sont immuables par défaut
Référence mutable
- Les références mutables ont une restriction importante : dans une portée spécifique, il ne peut y avoir qu'une seule référence mutable vers des données
- Cela permet d'empêcher les conflits de données à la compilation
- Les conflits de données se produisent dans les trois comportements suivants :
- Deux ou plusieurs pointeurs accèdent simultanément aux mêmes données
- Au moins un pointeur est utilisé pour écrire des données
- Aucun mécanisme n'est utilisé pour synchroniser l'accès aux données
- Vous pouvez créer de nouvelles portées pour autoriser la création non simultanée de plusieurs références mutables
fn main() {
let mut s = String::from("Bonjour");
{
let s1 = &mut s;
}
let s2 = &mut s;
}
Une autre restriction
- Vous ne pouvez pas avoir simultanément une référence mutable et une référence immuable
- Plusieurs références immuables sont possibles
fn main() {
let mut s = String::from("Bonjour");
let r1 = &s;
let r2 = &s;
let s1 = &mut s; // erreur
println!("{} {} {}", r1, r2, s1);
}
Références pendantes Dangling References
- Pointeur pendu (Dangling Pointer) : un pointeur qui référence une adresse mémoire qui a peut-être déjà été libérée et attribuée à d'autres.
- En Rust, le compilateur garantit que les références ne sont jamais des références pendantes :
- Si vous référencez certaines données, le compilateur garantit que les données ne quitteront pas la portée avant que la référence ne quitte la portée
Règles des références
- À tout moment donné, une seule de ces conditions peut être satisfaite :
- Une référence mutable
- Un nombre arbitraire de références immuables
- Les références doivent toujours être valides
4.3 Tranches
- Une autre structure de données qui ne possède pas la propriété en Rust : tranche (slice)
- Un problème, écrire une fonction :
- Elle reçoit une chaîne comme paramètre
- Elle retourne le premier mot trouvé dans cette chaîne
- Si la fonction ne trouve aucun espace, la chaîne entière est retournée
- Tentative de solution
fn main() {
let mut s = String::from("Bonjour monde");
let word_index = first_world(&s);
println!("{}", word_index);
}
fn first_world(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
Tranches de chaîne
- Une tranche de chaîne est une référence vers une partie du contenu d'une chaîne
- Forme : [début..fin]
- Indice de début est l'indice de la position de début de la tranche
- Indice de fin est l'indice suivant la position de fin de la tranche
fn main() {
let s = String::from("Bonjour monde");
let hello = &s[0..5];
let world = &s[6..11];
println!("{}, {}", hello, world);
}
Note
- Les indices de plage pour les tranches de chaîne doivent se produire dans les limites des caractères UTF-8 valides
- Si vous essayez de créer une tranche de chaîne à partir d'un caractère multibyte, le programme signalera une erreur et se terminera
Réécriture de l'exemple avec des tranches de chaîne
fn main() {
let s = String::from("Bonjour monde");
let word_index = first_world(&s);
println!("{}", word_index);
}
fn first_world(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
Les littéraux de chaîne sont des tranches
- Les littéraux de chaîne sont stockés directement dans le programme binaire
- let s = "Bonjour, Monde !";
fn main() {
let s = "Bonjour monde";
println!("{}", s)
}
- Le type de la variable s est &str, c'est une tranche pointant vers un emplacement spécifique du programme binaire
- &str est une référence immuable, donc les littéraux de chaîne sont également immuables
Passer des tranches de chaîne comme paramètres
fn first_word(s: &String) -> &str {- Les développeurs expérimentés en Rust préfèrent utiliser &str comme type de paramètre, car cela permet d'accepter à la fois les types String et &str :
fn first_word(s: &str) -> &str {- Utiliser des tranches de chaîne, appeler directement cette fonction
- Utiliser String, créer une tranche complète de String pour appeler cette fonction
- Utiliser des tranches de chaîne au lieu de références de chaîne dans la définition des fonctions rend nos API plus générales, sans perdre de fonctionnalité
- Les littéraux de chaîne sont des tranches de chaîne
fn main() {
let my_string = String::from("Bonjour monde");
let word_index = first_world(&my_string[..]);
println!("{}", word_index);
let my_string_literal = "bonjour monde";
let word_literal = first_world(my_string_literal);
println!("word_literal: {}", word_literal);
}
fn first_world(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
Autres types de tranches
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
}