Variables & fonctions

Variables & fonctions

1. Les variables en Rust

En Rust, les variables sont déclarées avec le mot-clé let. Une des particularités de Rust est que les variables sont immutables par défaut.

1.1 — Déclaration de base

// Déclaration simple
let x = 5;
println!("La valeur de x est : {}", x);
// Déclaration avec type explicite
let y: i32 = 10;
println!("La valeur de y est : {}", y);
// Déclaration de plusieurs variables
let a = 1;
let b = 2;
let c = 3;

Note : en Rust, on utilise println! (avec un !) pour afficher du texte. Le {} est remplacé par la valeur de la variable.

1.2 — Immutabilité par défaut

Par défaut, les variables Rust sont immutables : impossible de changer leur valeur après leur déclaration.

let x = 5;
println!("x = {}", x);
x = 6;  // ERREUR DE COMPILATION !
// error[E0384]: cannot assign twice to immutable variable `x`

Pourquoi ? L'immutabilité par défaut aide à éviter les bugs. Pour modifier une variable, il faut le déclarer explicitement avec mut.

1.3 — Variables mutables

Pour rendre une variable modifiable, on ajoute mut après let.

let mut x = 5;
println!("x = {}", x);  // Affiche : x = 5
x = 6;                   // OK !
println!("x = {}", x);  // Affiche : x = 6
// Modification multiple
let mut compteur = 0;
compteur = compteur + 1;
compteur = compteur + 1;
println!("compteur = {}", compteur);  // Affiche : compteur = 2

Conseil : utiliser mut seulement quand nécessaire — rend le code plus sûr et plus facile à comprendre.

1.4 — Les constantes

Les constantes sont déclarées avec const et sont toujours immutables. Elles doivent avoir un type explicite et sont évaluées à la compilation.

// Constante (MAJUSCULES par convention)
const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159;

fn main() {
    println!("Le maximum est : {}", MAX_POINTS);
    // Impossible avec une constante :
    // const ne peut pas être mut
    // const doit avoir un type explicite
}

Différences entre let et const :

letconst
Immutable par défaut✅ (sauf si mut)✅ (toujours)
Peut être mut✅ Oui❌ Jamais
Type explicite requis❌ Non (inférence possible)✅ Oui, toujours
PortéeBlocGlobale
Valeur calculéeÀ l'exécutionÀ la compilation
Convention de nomsnake_caseUPPER_CASE

1.5 — Le shadowing

Le shadowing permet de redéclarer une variable avec le même nom — différent de la mutabilité.

let x = 5;
println!("x = {}", x);  // 5

let x = x + 1;  // Nouvelle variable qui masque la précédente
println!("x = {}", x);  // 6

let x = x * 2;  // Encore une nouvelle variable
println!("x = {}", x);  // 12

// Le shadowing permet de changer le type !
let espaces = "   ";           // Type : &str
let espaces = espaces.len();   // Type : usize
println!("Il y a {} espaces", espaces);  // 3

Shadowing vs mut :

  • mut : modifie la valeur, même type obligatoire.
  • let (shadowing) : crée une nouvelle variable, peut changer de type.

2. Les types de données

Rust est un langage statiquement typé : chaque variable a un type connu à la compilation. Rust peut souvent inférer le type, mais on peut aussi l'annoter explicitement.

2.1 — Types scalaires

Les types scalaires représentent une valeur unique. Rust en a quatre types principaux :

// 1. ENTIERS (integers)
let a: i32 = 42;        // Entier signé 32 bits
let b: u64 = 100;       // Entier non signé 64 bits
let c = 5;              // i32 par défaut

// Différentes tailles : i8, i16, i32, i64, i128, isize
//                       u8, u16, u32, u64, u128, usize
// 2. FLOTTANTS (floating-point)
let x: f64 = 2.5;       // 64 bits (défaut)
let y: f32 = 3.14;      // 32 bits
// 3. BOOLÉENS (boolean)
let vrai: bool = true;
let faux: bool = false;
// 4. CARACTÈRES (char)
let lettre: char = 'A';
let emoji: char = '🦀';  // Unicode !

Types d'entiers en Rust :

TypeTailleMinimumMaximum
i88 bits-128127
u88 bits0255
i3232 bits-2 milliards2 milliards
u3232 bits04 milliards
i6464 bitsTrès grand négatifTrès grand positif
isizeTaille du systèmeVariableVariable

2.2 — Types composés

Les types composés regroupent plusieurs valeurs dans un seul type.

// 1. TUPLES - Types différents, taille fixe
let personne: (&str, i32, bool) = ("Alice", 25, true);

// Accès par déstructuration
let (nom, age, actif) = personne;
println!("{} a {} ans", nom, age);

// Accès par index
println!("Nom : {}", personne.0);
println!("Age : {}", personne.1);
// 2. TABLEAUX - Même type, taille fixe
let nombres: [i32; 5] = [1, 2, 3, 4, 5];

// Accès par index
let premier = nombres[0];  // 1
let dernier = nombres[4];  // 5

// Tableau avec valeur répétée
let zeros = [0; 10];  // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// 3. SLICES - Vue sur une partie d'un tableau
let slice = &nombres[1..3];  // [2, 3]

2.3 — Inférence de type

// Rust infère le type automatiquement
let x = 5;           // i32 (défaut pour entiers)
let y = 2.5;         // f64 (défaut pour flottants)
let z = true;        // bool
let nom = "Alice";   // &str
// Rust peut inférer selon l'utilisation
let mut nombres = Vec::new();  // Type inconnu
nombres.push(1);               // Maintenant Rust sait : Vec<i32>

2.4 — Annotation de type

// Parfois nécessaire pour lever l'ambiguïté
let nombre: u32 = "42".parse().expect("Pas un nombre !");
// Types complexes
let vecteur: Vec<i32> = vec![1, 2, 3];
let hashmap: std::collections::HashMap<String, i32>
    = std::collections::HashMap::new();

3. Les fonctions

Les fonctions sont omniprésentes en Rust. Elles sont déclarées avec fn et utilisent la convention snake_case pour les noms.

3.1 — Déclaration de base

// Fonction sans paramètre ni retour
fn dire_bonjour() {
    println!("Bonjour !");
}

fn main() {
    dire_bonjour();  // Appel de fonction
    dire_bonjour();  // On peut l'appeler plusieurs fois
}
// Convention de nommage
fn ma_fonction() { }        // snake_case
fn MaFonction() { }         // à éviter
fn calculer_total() { }     // snake_case, correct
fn calculerTotal() { }      // à éviter (camelCase)

3.2 — Paramètres

Les paramètres doivent toujours avoir un type explicite.

// Un paramètre
fn afficher_nombre(x: i32) {
    println!("Le nombre est : {}", x);
}

// Plusieurs paramètres
fn additionner(a: i32, b: i32) {
    let somme = a + b;
    println!("{} + {} = {}", a, b, somme);
}

// Appel
fn main() {
    afficher_nombre(42);
    additionner(5, 7);
}

// Paramètres de types différents
fn presenter(nom: &str, age: i32, actif: bool) {
    println!("{} a {} ans, actif : {}", nom, age, actif);
}

3.3 — Valeur de retour

Une fonction retourne une valeur avec -> suivi du type. La dernière expression est retournée automatiquement (pas de return nécessaire).

// Fonction avec retour
fn additionner(a: i32, b: i32) -> i32 {
    a + b  // PAS de point-virgule !
}
fn main() {
    let resultat = additionner(5, 3);
    println!("5 + 3 = {}", resultat);  // 8
}
// Avec return explicite (utile pour retour anticipé)
fn valeur_absolue(x: i32) -> i32 {
    if x < 0 {
        return -x;  // Retour anticipé
    }
    x  // Retour normal
}
// Plusieurs retours possibles
fn diviser(a: i32, b: i32) -> Option<i32> {
    if b == 0 {
        return None;  // Division par zéro
    }
    Some(a / b)
}

Attention ! En Rust, une expression sans point-virgule est retournée. Avec point-virgule, c'est une instruction qui ne retourne rien.

3.4 — Expressions vs instructions

// Expression (retourne une valeur)
fn plus_un(x: i32) -> i32 {
    x + 1  // Pas de ;
}
// Instruction (ne retourne rien)
fn plus_un_incorrect(x: i32) -> i32 {
    x + 1;  // Avec ; = ne retourne rien !
    // error: mismatched types (expected i32, found ())
}
// Les blocs sont des expressions
fn exemple() -> i32 {
    let x = {
        let y = 3;
        y + 1  // Cette expression est la valeur du bloc
    };  // x vaut 4

    x * 2  // Retourne 8
}

3.5 — Fonctions avec références

Pour éviter de copier les données, on peut passer des références aux fonctions.

// Sans référence (copie la valeur)
fn afficher_nombre(x: i32) {
    println!("Nombre : {}", x);
}

// Avec référence (emprunte la valeur)
fn afficher_chaine(s: &String) {
    println!("Chaîne : {}", s);
}

// Référence mutable (peut modifier)
fn incrementer(x: &mut i32) {
    *x += 1;  // * = déréférencement
}

fn main() {
    let nombre = 42;
    afficher_nombre(nombre);  // Copie

    let texte = String::from("Hello");
    afficher_chaine(&texte);  // Emprunte

    let mut compteur = 0;
    incrementer(&mut compteur);
    println!("Compteur : {}", compteur);  // 1
}

4. Ownership et borrowing (introduction)

L'ownership est le concept le plus important et unique de Rust. C'est ce qui permet à Rust d'être sûr sans ramasse-miettes (garbage collector).

4.1 — Le concept d'ownership

Règles de base :

  1. Chaque valeur a un propriétaire unique.
  2. Il ne peut y avoir qu'un seul propriétaire à la fois.
  3. Quand le propriétaire sort du scope, la valeur est libérée.
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 est "déplacé" vers s2
    // println!("{}", s1);  // ERREUR ! s1 n'est plus valide
    println!("{}", s2);     // OK
}

// Exemple avec fonction
fn prend_ownership(s: String) {
    println!("{}", s);
}  // s est libéré ici
fn main() {
    let texte = String::from("hello");
    prend_ownership(texte);
    // println!("{}", texte);  // ERREUR ! texte a été déplacé dans la fonction
}

Types Copy : les types simples (i32, f64, bool, char) sont Copy — ils sont copiés au lieu d'être déplacés.

4.2 — Les références (`&`)

Les références permettent d'emprunter une valeur sans en prendre l'ownership.

fn calculer_longueur(s: &String) -> usize {
    s.len()
}  // s sort du scope, mais ne possède pas la String
fn main() {
    let texte = String::from("hello");
    let longueur = calculer_longueur(&texte);
    println!("'{}' a {} caractères", texte, longueur); // texte est toujours valide !
}
// Multiples références immutables OK
fn main() {
    let s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    println!("{} et {}", r1, r2);  // OK
}

4.3 — Les références mutables (`&mut`)

fn ajouter_monde(s: &mut String) {
    s.push_str(", world!");
}

fn main() {
    let mut texte = String::from("hello");
    ajouter_monde(&mut texte);
    println!("{}", texte);  // "hello, world!"
}
// UNE SEULE référence mutable à la fois !
fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    // let r2 = &mut s;  // ERREUR ! On ne peut pas avoir deux références mutables
    println!("{}", r1);
}
// Pas de mélange immutable + mutable
fn main() {
    let mut s = String::from("hello");
    let r1 = &s;      // OK
    let r2 = &s;      // OK
    // let r3 = &mut s;  // ERREUR !
    println!("{} et {}", r1, r2);
}

Règles du borrowing :

  1. Soit une référence mutable.
  2. Soit plusieurs références immutables.
  3. Mais jamais les deux en même temps.

5. Exercices pratiques

Exercice 1 — Variables

Compléter ce code pour qu'il compile :

fn main() {
    let x = 5;
    // Ajoute le code nécessaire pour que x devienne 10
    println!("x = {}", x);  // Doit afficher "x = 10"
}
// Solution :
fn main() {
    let mut x = 5;  // Ajouter mut
    x = 10;
    println!("x = {}", x);
}

Exercice 2 — Fonction simple

Créer une fonction qui multiplie deux nombres :

// À compléter
fn multiplier(/* paramètres */) /* -> type */ {
    // ton code
}

fn main() {
    let resultat = multiplier(6, 7);
    println!("6 x 7 = {}", resultat);  // Doit afficher 42
}
// Solution :
fn multiplier(a: i32, b: i32) -> i32 {
    a * b
}

Exercice 3 — Références

Créer une fonction qui double un nombre sans le déplacer :

fn doubler(/* référence mutable */) {
    // ton code
}

fn main() {
    let mut nombre = 5;
    doubler(/* passer référence */);
    println!("Nombre : {}", nombre);  // Doit afficher 10
}
// Solution :
fn doubler(x: &mut i32) {
    *x *= 2;
}

fn main() {
    let mut nombre = 5;
    doubler(&mut nombre);
    println!("Nombre : {}", nombre);
}

6. Aide-mémoire

Variables

SyntaxeDescriptionExemple
let x = 5;Variable immutablelet nom = "Alice";
let mut x = 5;Variable mutablelet mut compteur = 0;
const MAX: i32 = 100;Constanteconst PI: f64 = 3.14;
let x = 5; let x = 10;Shadowinglet x = x + 1;

Fonctions

SyntaxeDescriptionExemple
fn nom() { }Fonction simplefn dire_bonjour() { }
fn nom(x: i32) { }Avec paramètresfn afficher(n: i32) { }
fn nom() -> i32 { }Avec retourfn double(x: i32) -> i32 { x * 2 }
fn nom(s: &String) { }Référencefn longueur(s: &String) -> usize { }
fn nom(x: &mut i32) { }Réf. mutablefn incrementer(x: &mut i32) { }

Types courants

TypeDescriptionExemple
i32, u32, i64...Entierslet x: i32 = 42;
f32, f64Flottantslet pi: f64 = 3.14;
boolBooléenlet actif: bool = true;
charCaractèrelet lettre: char = 'A';
&strChaîne immutablelet texte = "hello";
StringChaîne mutablelet s = String::from("hi");

Conclusion

Ce cours couvre les bases de Rust — variables, types, fonctions, ownership et borrowing.

Prochaines étapes :

  • Pratiquer avec de petits programmes.
  • Apprendre les structures (struct) et les enums — voir structures-enums.md.
  • Maîtriser le pattern matching — voir structures-et-controle.md.
  • Explorer les collections (Vec, HashMap) — voir collections.md.

Continue à coder et n'aie pas peur des erreurs du compilateur : elles sont là pour aider.

Ressources recommandées :

  • The Rust Book (en français) : https://jimskapt.github.io/rust-book-fr/
  • Rustlings (exercices) : https://github.com/rust-lang/rustlings
  • Rust by Example : https://doc.rust-lang.org/rust-by-example/
  • Forum Rust : https://users.rust-lang.org/
⚡ Runique exercise Beta
Say hello to start an exercise for this course.