La portée en Rust
La portée (scope) est la zone du code où une variable est valide. C’est une idée simple, mais une fois comprise, beaucoup d’autres règles de Rust (en particulier l’ownership, ou possession) deviennent évidentes.
Les accolades définissent la portée
Section intitulée « Les accolades définissent la portée »Une variable existe à partir du moment où elle est déclarée, jusqu’à l’accolade fermante } du bloc qui la contient :
fn main() { let x = 5; // x entre dans la portée ici
{ let y = 10; // y entre dans la portée ici println!("x = {x}, y = {y}"); // les deux sont valides } // y sort de la portée ici
println!("x = {x}"); // println!("{y}"); // ❌ erreur : y n'est plus dans la portée}Sortie :
x = 5, y = 10x = 5y n’existe qu’à l’intérieur du bloc { } intérieur. Dès que ce bloc se termine, y disparaît : essayer de l’utiliser ensuite provoque une erreur de compilation, pas une mauvaise surprise à l’exécution.
La portée ne fonctionne que dans un sens : un bloc intérieur voit les variables des blocs qui l’entourent (c’est pour ça que le bloc intérieur peut afficher x), mais un bloc extérieur ne voit pas ce qu’il y a dans un bloc intérieur.
Le corps d’une fonction est aussi une portée
Section intitulée « Le corps d’une fonction est aussi une portée »Les paramètres et toutes les variables créées dans une fonction n’existent que pendant l’appel de cette fonction :
fn calculate() -> i32 { let result = 5 * 2; // result n'existe qu'à l'intérieur de calculate result}
fn main() { let value = calculate(); println!("{value}"); // println!("{result}"); // ❌ erreur : result n'existe pas ici}Sortie :
10C’est vrai dans l’autre sens aussi : une fonction ne voit pas les variables locales de main. Si elle a besoin d’une valeur, vous la lui passez en paramètre, comme vous l’avez vu à la page Fonctions.
Les blocs sont aussi des expressions
Section intitulée « Les blocs sont aussi des expressions »Un bloc { } ne fait pas que limiter la durée de vie des variables. Il peut aussi produire une valeur : sa dernière expression, écrite sans point-virgule. C’est la même règle que celle qui fait qu’une fonction renvoie sa dernière ligne.
C’est pratique quand vous avez besoin de quelques variables intermédiaires pour faire un calcul, mais que vous ne voulez pas qu’elles traînent ensuite :
fn main() { let price = 40;
let total = { let tax = price / 10; // tax ne vit qu'à l'intérieur de ce bloc let shipping = 5; price + tax + shipping // pas de point-virgule : c'est la valeur du bloc };
println!("total = {total}"); // println!("{tax}"); // ❌ erreur : tax est hors de portée}Sortie :
total = 49Deux détails à surveiller :
- La dernière ligne
price + tax + shippingn’a pas de point-virgule. Si vous en ajoutez un, le bloc renvoie()(rien) au lieu d’un nombre, etprintln!ne peut pas l’afficher. - L’ensemble est une instruction
let, donc il se termine par};après l’accolade fermante.
Une fois le bloc terminé, tax et shipping disparaissent. Il ne reste que le résultat, total. Garder les variables intermédiaires dans un petit bloc comme celui-ci montre clairement qu’elles ne servent qu’à un seul calcul.
Pourquoi la portée compte pour l’ownership
Section intitulée « Pourquoi la portée compte pour l’ownership »Tout cela est directement lié à l’ownership, que vous découvrirez un peu plus loin : une valeur est automatiquement libérée au moment où la portée de son propriétaire se termine.
fn main() { { let text = String::from("bonjour"); println!("{text}"); } // la portée de text se termine ici : Rust la libère automatiquement}Sortie :
bonjourC’est tout le mécanisme qui permet à Rust de se passer de ramasse-miettes (garbage collector) : il sait déjà, à la compilation, exactement où se termine chaque portée.
Le masquage et la portée
Section intitulée « Le masquage et la portée »Vous vous souvenez du masquage (shadowing), vu à la page Variables ? Chaque let avec le même nom dans une portée crée une variable distincte : il n’efface pas la variable extérieure, il la remplace simplement jusqu’à la fin de cette portée.
fn main() { let x = 5; { let x = x * 2; // un nouveau x, seulement dans ce bloc println!("x intérieur = {x}"); // 10 } println!("x extérieur = {x}"); // 5 : inchangé}Sortie :
x intérieur = 10x extérieur = 5Exercices
Section intitulée « Exercices »1. Corriger l’erreur de portée
Section intitulée « 1. Corriger l’erreur de portée »Ce programme ne compile pas, car area est utilisée après la fin de son bloc :
fn main() { let width = 3; { let height = 4; let area = width * height; } println!("aire = {area}");}Modifiez-le pour qu’il affiche aire = 12, tout en gardant height uniquement à l’intérieur du bloc. Indice : faites en sorte que le bloc produise l’aire, et stockez le résultat dans une variable déclarée en dehors.
Voir la solution
fn main() { let width = 3; let area = { let height = 4; width * height }; println!("aire = {area}");}Sortie :
aire = 12area vit maintenant dans la portée de main, donc println! peut la voir. height ne sert toujours qu’à l’intérieur du bloc, et elle est libérée quand le bloc se termine.
2. Des niveaux d’imbrication
Section intitulée « 2. Des niveaux d’imbrication »Créez une variable level qui vaut 1. Ouvrez un bloc qui la masque avec 2, et à l’intérieur, un autre bloc qui la masque avec 3. Affichez level dans chaque bloc, puis à nouveau juste après la fermeture de chaque bloc. Indice : vous devriez voir les nombres passer par 1, 2, 3, puis redescendre à 2 et 1.
Voir la solution
fn main() { let level = 1; println!("niveau {level}"); { let level = 2; println!("niveau {level}"); { let level = 3; println!("niveau {level}"); } println!("retour au niveau {level}"); } println!("retour au niveau {level}");}Sortie :
niveau 1niveau 2niveau 3retour au niveau 2retour au niveau 1Chaque level intérieur ne cache celui de l’extérieur que jusqu’à la fin de son bloc. Les variables extérieures n’ont jamais été modifiées : elles sont toujours là, à attendre.
Récapitulatif
Section intitulée « Récapitulatif »- La portée est la zone (délimitée par
{ }) où une variable est valide. - Une variable cesse d’exister dès qu’on atteint l’accolade fermante
}de sa portée. - Le corps d’une fonction est aussi une portée : les variables locales disparaissent quand la fonction se termine.
- Un bloc est une expression : sa dernière ligne (sans point-virgule) est sa valeur, et ses variables intermédiaires disparaissent à la fin du bloc.
- C’est la portée qui indique à Rust exactement quand libérer une valeur, et c’est pour ça qu’il n’a pas besoin de ramasse-miettes.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. D'après cette page, pourquoi Rust n'a-t-il pas besoin de ramasse-miettes ?
2. Que se passe-t-il quand vous masquez une variable avec let x = x * 2; dans un bloc intérieur ?
3. Quelle est la valeur de total après let total = { let a = 2; a * 10 }; ?
Score : 0 / 3
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Variable Scope — The Rust Book (en anglais) : le fonctionnement de la portée, présenté comme la première étape vers l’ownership.
- Scope and Shadowing — Rust by Example (en anglais) : un court exemple exécutable de blocs et de masquage.
- RAII — Rust by Example (en anglais) : ce qui arrive aux valeurs quand leur portée se termine, pour quand vous serez prêt à passer à l’étape suivante.
- Block expressions — The Rust Reference (en anglais) : les règles exactes des blocs et des valeurs qu’ils produisent.