Aller au contenu principal
Version: 2025

TP 01 - Rust pour les débutants

Assignment

info

Vous devez accepter la tâche d'ici et travailler avec ce dépôt: TP1.

Resources

  1. The Rust Programming Language, Chapitre 1, 2, 3 et 5 (version anglaise)
  2. 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 :

NiveauDescriptionBesoin
coreFournit 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
allocFournit 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
stdFournit 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;
remarque

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;
LongueurSignéNon signéÉquivalent en Java
8-biti8u8byte/ Byte1
16-biti16u16short / Short1
32-biti32u32int / Integer1
64-biti64u64long / Long1
128-biti128u128N/A
archisizeusizeN/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.

LongueurVirgule flottanteÉquivalent en Java
32-bitf32float
64-bitf64double
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];
remarque

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}");
}

remarque

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.

info

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"
}

remarque

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}");
}
}
remarque

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");
}
attention

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);
}
remarque

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);
info

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

remarque

Avant d'aborder les exercices, jetez un œil et parcourez les chapitres 1, 2 et 3 des tutoriels Tour of Rust.

astuce

Si Rust n'est pas installé, vous pouvez utiliser Rust Playground pour résoudre les sujets.

  1. Écrivez un programme qui imprime votre nom.
  2. Définissez deux variables et attribuez-leur une valeur numérique. Affichez la valeur maximale entre les deux sans utiliser de variable temporaire.
  3. Écrivez une fonction qui vérifie si un nombre est divisible par n.
  4. Définissez un tableau de nombres et écrivez le code pour en afficher la valeur maximale.
  5. 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.
  6. 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.
astuce

Utilisez io::stdin().read_line(&mut buffer) pour lire a partir du clavier.

Footnotes

  1. À partir de Java 8, les classes Number ont des méthodes d'assistance, comme compareUnsigned et toUnsigned... qui permettent l'utilisation et la manipulation de nombres non signés. 2 3 4