Aller au contenu

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.

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 = 10
x = 5
▶ Essayez par vous-même

y 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.

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 :

10
▶ Essayez par vous-même

C’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.

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 = 49
▶ Essayez par vous-même

Deux détails à surveiller :

  • La dernière ligne price + tax + shipping n’a pas de point-virgule. Si vous en ajoutez un, le bloc renvoie () (rien) au lieu d’un nombre, et println! 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.

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 :

bonjour
▶ Essayez par vous-même

C’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.

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 = 10
x extérieur = 5
▶ Essayez par vous-même

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 = 12
▶ Essayez par vous-même

area 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.

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 1
niveau 2
niveau 3
retour au niveau 2
retour au niveau 1
▶ Essayez par vous-même

Chaque 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.

  • 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

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