TP 01 - Rust pour les débutants
Assignment
Vous devez accepter la tâche d'ici et travailler avec ce dépôt: TP1.
Resources
- The Rust Programming Language, Chapitre 1, 2, 3 et 5 (version anglaise)
- Tour of Rust tutoriel étape par étape
Concepts basiques de langages de programmation pour Rust
Bibliothèque standard
La bibliothèque standard est divisée en trois niveaux :
Niveau | Description | Besoin |
---|---|---|
core | Fournit les éléments de langage requis dont Rust a besoin pour la compilation, comme les traits Display et Debug . Les données ne peuvent être que des éléments globaux (stockés dans .data) ou sur la pile. | Hardware |
alloc | Fournit tout, depuis le niveau core ainsi que les structures de données allouées tas comme Box et Vec . Le développeur doit fournir un allocateur de mémoire, comme embedded_alloc. | Memory Allocator |
std | Fournit tout, depuis le niveau alloc , ainsi que de nombreuses fonctionnalités qui dépendent de la plate-forme, y compris les fils d'exécution et les E/S. Il s'agit du niveau par défaut pour les applications Windows, Linux, macOS et systèmes d'exploitation similaires. | Operating System |
Par défaut, Rust a un ensemble d'éléments définis dans la bibliothèque standard qu'il introduit dans le cadre d'application de chaque programme. Cet ensemble s'appelle le prélude, et vous pouvez voir tout ce qu'il contient dans la documentation standard de la bibliothèque.
Si un type que vous voulez utiliser n'est pas dans le prélude, vous devez mettre ce type dans la portée explicitement avec une instruction use. L'utilisation de la bibliothèque std :: io vous offre un certain nombre de fonctionnalités utiles, notamment la possibilité d'accepter les entrées de l'utilisateur.
use std::io;
La fonction main
La fonction main
est le point d'entrée de notre programme.
fn main() {
println!("Hello, world!");
}
On utilise println!()
pour imprimer des messages sur l'écran.
Pour insérer une marque substitutive dans la macro println!
, utilisez une paire d'accolades {}
. Nous fournissons le nom ou l'expression de la variable pour remplacer la marque substitutive fournie en dehors de la chaîne.
fn main() {
let name = "Mary";
let age = 26;
let color = "blue";
println!("Hello, {}. You are {} years old", name, age);
println!("What is your favourite color? Is it {color}?");
}
Variables et mutabilité
On utilise le mot-clé let
pour créer une variable.
let a = 5;
Par défaut, en Rust, les variables sont immuables, ça veut dire qu'une fois une valeur affectuée à un nom, vous ne pouvez pas modifier cette valeur.
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
Dans ce cas, on va obtenir une erreur de compilation parce qu’on essaye de modifier la valeur de x
de 5 à 6, mais x
est immuable, donc on ne peut pas faire cette modification.
Bien que les variables soient immuables par défaut, vous pouvez les rendre modifiables en ajoutant mut
devant le nom de la variable. L'ajout de mut
transmet également l'intention aux futurs lecteurs du code en indiquant que d'autres parties de code modifieront la valeur de cette variable.
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
Maintenant, la valeur de x
peut devenir 6
.
Constantes
Comme les variables immuables, les constantes sont des valeurs qui sont liées à un nom et ne sont pas autorisées à changer, mais il existe quelques différences entre les constantes et les variables.
Les variables const
sont des variables dont la valeur doit être connue à la compilation à la différence des variables let
dont la valeur est déterminée au moment de l'exécution du programme.
Tout d'abord, vous n'êtes pas autorisés à utiliser mut
avec des constantes. Les constantes ne sont pas seulement immuables par défaut, elles sont toujours immuables. Vous déclarez des constantes en utilisant le mot clé const au lieu du mot clé let.
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
Pour comprendre mieux, lire le chapitre 3 qui se trouve dans la documentation au début du TP!
Types des données
Types scalaire
Un type scalaire représente une valeur unique. Rust a quatre types scalaires principaux : entiers, nombres à virgule flottante, booléens et caractères.
Integer → Chaque variante peut être signée ou non signée et a une taille explicite.
let x: i8 = -2;
let y: u16 = 25;
Longueur | Signé | Non signé | Équivalent en Java |
---|---|---|---|
8-bit | i8 | u8 | byte / Byte 1 |
16-bit | i16 | u16 | short / Short 1 |
32-bit | i32 | u32 | int / Integer 1 |
64-bit | i64 | u64 | long / Long 1 |
128-bit | i128 | u128 | N/A |
arch | isize | usize | N/A |
Virgule flottante → Les types à virgule flottante de Rust sont f32 et f64, qui ont respectivement une taille de 32 bits et 64 bits. Le type par défaut est f64 car sur les processeurs modernes, les calculs utilisant f64 ont à peu près la même vitesse que f32, mais sont plus précis. Tous les types à virgule flottante sont signés.
Longueur | Virgule flottante | Équivalent en Java |
---|---|---|
32-bit | f32 | float |
64-bit | f64 | double |
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
Boolean → Les booléens ont une taille d'un octet. Le type booléen dans Rust est spécifié à l'aide de bool.
let t = true;
let f: bool = false; // with explicit type annotation
Caractere → Le type char de Rust est le type alphabétique le plus primitif du langage.
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
Types composés
Tuple → Un tuple est une manière générale de regrouper un certain nombre de valeurs avec une variété de types en un seul type composé. Les tuples ont une longueur fixe : une fois déclarés, leur taille ne peut pas augmenter ou diminuer.
let tup: (i32, f64, u8) = (500, 6.4, 1);
Array → Contrairement à un tuple, chaque élément d'un tableau doit avoir le même type. Contrairement aux tableaux de certains autres langages, les tableaux de Rust ont une longueur fixe.
let a = [1, 2, 3, 4, 5];
Pour comprendre mieux, lire le chapitre 3 qui se trouve dans la documentation au début du TP!
Fonctions
Nous définissons une fonction dans Rust en tapant fn suivi d'un nom de fonction et d'un ensemble de parenthèses. Les accolades indiquent au compilateur où commence et se termine le corps de la fonction.
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
Paramètres
Nous pouvons définir des fonctions avec des paramètres, qui sont des variables spéciales qui font partie de la signature d'une fonction. Lorsqu'une fonction a des paramètres, vous pouvez lui fournir des valeurs concrètes pour ces paramètres.
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
}
Dans les signatures de fonctions, vous devez déclarer le type de chaque paramètre!
Déclarations vs. expressions
Les corps de fonction sont constitués d'une série d'instructions se terminant éventuellement par une expression.
Les déclarations sont des instructions qui effectuent une action et ne renvoient pas de valeur.
Les expressions évaluent une valeur résultante.
Pour comprendre mieux, lire le chapitre 3 qui se trouve dans la documentation au début du TP!
Fonctions avec valeurs de retour
Les fonctions peuvent renvoyer des valeurs au code qui les appelle. Nous ne nommons pas les valeurs de retour, mais nous devons déclarer leur type après une flèche (->). En Rust, la valeur de retour de la fonction est synonyme de la valeur de l'expression finale dans le bloc du corps d'une fonction. Vous pouvez revenir plus tôt à partir d'une fonction en utilisant le mot-clé return et en spécifiant une valeur, mais la plupart des fonctions renvoient implicitement la dernière expression.
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");// "The value of x is: 5"
}
Pour comprendre mieux, lire le chapitre 3 qui se trouve dans la documentation au début du TP!
Flux de contrôle
if-else
Toutes les expressions if
commencent par le mot-clé if
, suivies d'une condition. Facultativement, nous pouvons également inclure une expression else
.
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
Vous pouvez utiliser plusieurs conditions en combinant if
et else
dans une expression else if
:
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
Parce que if
est une expression, nous pouvons l'utiliser sur le côté droit d'une instruction let
pour affecter le résultat à une variable.
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");//"The value of the number is 5"
}
Loop
Le mot-clé loop
indique à Rust d'exécuter un bloc de code encore et encore pour toujours ou jusqu'à ce que vous lui disiez explicitement de s'arrêter.
fn main() {
loop {
println!("again!");
}
}
L'une des utilisations d'une boucle est de réessayer une opération dont vous savez qu'elle pourrait échouer, comme vérifier si un thread a terminé son travail. Vous devrez peut-être également transmettre le résultat de cette opération hors de la boucle au reste de votre code. Pour ce faire, vous pouvez ajouter la valeur que vous souhaitez renvoyer après l'expression break que vous utilisez pour arrêter la boucle ; cette valeur sera renvoyée hors de la boucle afin que vous puissiez l'utiliser:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
While
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
For
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
Pour en savoir plus, lisez le chapitre 3 qui se trouve dans la documentation au début du TP!
Structures
Les structures sont similaires aux tuples : elles contiennent tous les deux plusieurs valeurs liées. Comme les tuples, les parties composantes d'une structure peuvent avoir de différents types. Contrairement aux tuples, dans une structure, vous nommez chaque élément de données afin que la signification des valeurs soit claire.
Pour définir une structure, nous entrons le mot-clé struct
et nommons la structure entière. Le nom d'une structure doit décrire la signification des éléments de données regroupés. Ensuite, entre accolades, nous définissons les noms et les types des données, que nous appelons champs.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
Pour utiliser une structure après l'avoir définie, nous créons une instance de cette structure en spécifiant des valeurs concrètes pour chacun des champs. Nous créons une instance en indiquant le nom de la structure, puis ajoutons des accolades contenant des paires clé : valeur, où les clés sont les noms des champs et les valeurs sont les données que nous voulons stocker dans ces champs.
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
Pour acceder a un certain membre du struct on utilise cette syntaxe:
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
}
Notez que l'instance entière doit être modifiable ; Rust ne nous permet pas de marquer uniquement certains champs comme mutables!
Comme pour toute expression, nous pouvons construire une nouvelle instance de la structure en tant que dernière expression dans le corps de la fonction pour renvoyer implicitement cette nouvelle instance.
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
Tuple structs
Rust prend également en charge les structures qui ressemblent aux tuples, appelées tuple structs. Les structures de tuple ont la signification supplémentaire fournie par le nom de la structure mais n'ont pas de noms associés à leurs champs ; au lieu de cela, ils ont juste les types des champs. Les structures de tuple sont utiles lorsque vous souhaitez donner un nom à l'ensemble du tuple et lui donner un type différent des autres tuples, et lorsque nommer chaque champ comme dans une structure régulière serait verbeux ou redondant.
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
Pour en savoir davantage, lisez le chapitre 5 qui se trouve dans la documentation au début du TP!
Enums
Les énumérations, également appelées enum
, vous permettent de définir un type en énumérant ses variantes possibles.
Définition d'une énumération :
enum IpAddrKind {
V4,
V6,
}
Match
Rust dispose d'une construction de flux de contrôle extrêmement puissante appelée « match » qui vous permet de comparer une valeur à une série de modèles, puis d'exécuter du code en fonction du modèle qui correspond. Les modèles peuvent être constitués de valeurs littérales, de noms de variables, de caractères génériques et bien d'autres éléments.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
Lorsque l'expression de correspondance s'exécute, elle compare la valeur résultante au modèle pour chaque bras, dans l'ordre. Si un modèle correspond à la valeur, le code associé à ce modèle est exécuté. Si ce modèle ne correspond pas à la valeur, l'exécution passe au bras suivant.
Le code associé à chaque bras est une expression et la valeur résultante de l'expression dans le bras correspondant est la valeur renvoyée pour l'ensemble de l'expression correspondante.
Dans la section précédente, nous voulions extraire la valeur T
interne du cas Some lors de l'utilisation de Option<T>
; nous pouvons également gérer Option<T>
en utilisant match , comme nous l'avons fait avec l'énumération Coin
! Au lieu de comparer des parties, nous comparerons des variantes de Option<T>
, mais le fonctionnement de l'expression match
reste le même.
fn get_option(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i),
}
}
let five = Some(5);
let six = get_option(five);
let none = get_option(None);
Pour une meilleure compréhension, veuillez lire le chapitre 6 de la documentation.
String
Rust n'a qu'un seul type de chaîne dans le langage principal, qui est la tranche de chaîne str
qui est généralement vue sous sa forme empruntée &str
.
Le type String
, qui est fourni par la bibliothèque standard Rust plutôt que codé dans le langage principal, est un type de chaîne codé en UTF-8 évolutif, mutable et détenu(owned).
Créer une nouvelle String
let mut s = String::new();
Cette ligne crée une nouvelle chaîne vide appelée s
, dans laquelle nous pouvons ensuite charger des données.
Nous pouvons utiliser la fonction String::from
ou la fonction to_string
pour créer une chaîne à partir d'une chaîne littérale :
let s = String::from("initial contents");
let data = "initial contents";
let s = data.to_string();
// the method also works on a literal directly:
let s = "initial contents".to_string();
Ajouter des données à une string
Nous pouvons développer une chaîne en utilisant la méthode push_str
pour ajouter une tranche de chaîne.
let mut s = String::from("foo");
s.push_str("bar");
La méthode push prend un seul caractère comme paramètre et l'ajoute à la chaîne.
let mut s = String::from("lo");
s.push('l');
Méthodes d'itération sur les Strings
La meilleure façon d’opérer sur des morceaux de chaînes est d’indiquer explicitement si vous voulez des caractères ou des octets. Pour les valeurs scalaires Unicode individuelles, utilisez la méthode chars
.
for c in "Зд".chars() {
println!("{c}");
}
Exécuter le programme
Pour exécuter le programme, nous pouvons être n'importe où dans le dossier de la caisse et exécuter la commande.
cargo run
Exercises
Avant d'aborder les exercices, jetez un œil et parcourez les chapitres 1, 2 et 3 des tutoriels Tour of Rust.
Si Rust n'est pas installé, vous pouvez utiliser Rust Playground pour résoudre les sujets.
- Écrivez un programme qui imprime votre nom.
- Définissez deux variables et attribuez-leur une valeur numérique. Affichez la valeur maximale entre les deux sans utiliser de variable temporaire.
- Écrivez une fonction qui vérifie si un nombre est divisible par n.
- Définissez un tableau de nombres et écrivez le code pour en afficher la valeur maximale.
- Définissez une structure appelée Ordinateur qui définit la marque, le nom du processeur et la taille de la mémoire d'un ordinateur. a. Ecrivez une fonction associée (statique) appelée new qui crée une instance de la structure. b. Écrivez une méthode appelée display qui imprime toutes les informations.
- Définissez un tableau avec des éléments de type Ordinateur. Écrivez un programme qui affiche un menu avec les options suivantes: a. imprimer tous les ordinateurs, b. imprimer l'ordinateur avec la plus grande quantité de mémoire. Lisez les touches du clavier et exécutez l'option sélectionnée jusqu'à ce que vous lisez quelque chose de différent de a et b.
Utilisez io::stdin().read_line(&mut buffer) pour lire a partir du clavier.