Les variables et la mutabilité

Tel qu'abordé au chapitre 2, par défaut, les variables sont immuables. C'est un des nombreux coups de pouce de Rust pour écrire votre code de façon à garantir la sécurité et la concurrence sans problème. Cependant, vous avez quand même la possibilité de rendre vos variables mutables (modifiables). Explorons comment et pourquoi Rust vous encourage à favoriser l'immuabilité, et pourquoi parfois vous pourriez choisir d'y renoncer.

Lorsqu'une variable est immuable, cela signifie qu'une fois qu'une valeur est liée à un nom, vous ne pouvez pas changer cette valeur. À titre d'illustration, générons un nouveau projet appelé variables dans votre dossier projects en utilisant cargo new variables.

Ensuite, dans votre nouveau dossier variables, ouvrez src/main.rs et remplacez son code par le code suivant. Ce code ne se compile pas pour le moment, nous allons commencer par étudier l'erreur d'immutabilité.

Fichier : src/main.rs

fn main() {
    let x = 5;
    println!("La valeur de x est : {}", x);
    x = 6;
    println!("La valeur de x est : {}", x);
}

Sauvegardez et lancez le programme en utilisant cargo run. Vous devriez avoir un message d'erreur comme celui-ci :

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
3 |     println!("La valeur de x est : {}", x);
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` due to previous error

Cet exemple montre comment le compilateur vous aide à trouver les erreurs dans vos programmes. Les erreurs de compilation peuvent s'avérer frustrantes, mais elles signifient en réalité que, pour le moment, votre programme n'est pas en train de faire ce que vous voulez qu'il fasse en toute sécurité ; elles ne signifient pas que vous êtes un mauvais développeur ! Même les Rustacés expérimentés continuent d'avoir des erreurs de compilation.

Ce message d'erreur indique que la cause du problème est qu'il est impossible d'assigner à deux reprises la variable immuable `x` (cannot assign twice to immutable variable `x`).

Il est important que nous obtenions des erreurs au moment de la compilation lorsque nous essayons de changer une valeur qui a été déclarée comme immuable, car cette situation particulière peut donner lieu à des bogues. Si une partie de notre code part du principe qu'une valeur ne changera jamais et qu'une autre partie de notre code modifie cette valeur, il est possible que la première partie du code ne fasse pas ce pour quoi elle a été conçue. La cause de ce genre de bogue peut être difficile à localiser après coup, en particulier lorsque la seconde partie du code ne modifie que parfois cette valeur. Le compilateur Rust garantit que lorsque vous déclarez qu'une valeur ne change pas, elle ne va jamais changer, donc vous n'avez pas à vous en soucier. Votre code est ainsi plus facile à maîtriser.

Mais la mutabilité peut s'avérer très utile, et peut faciliter la rédaction du code. Les variables sont immuables par défaut ; mais comme vous l'avez fait au chapitre 2, vous pouvez les rendre mutables en ajoutant mut devant le nom de la variable. L'ajout de mut va aussi signaler l'intention aux futurs lecteurs de ce code que d'autres parties du code vont modifier la valeur de cette variable.

Par exemple, modifions src/main.rs ainsi :

Fichier : src/main.rs

fn main() {
    let mut x = 5;
    println!("La valeur de x est : {}", x);
    x = 6;
    println!("La valeur de x est : {}", x);
}

Lorsque nous exécutons le programme, nous obtenons :

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/variables`
La valeur de x est : 5
La valeur de x est : 6

En utilisant mut, nous avons permis à la valeur liée à x de passer de 5 à 6. Il y a d'autres compromis à envisager, en plus de la prévention des bogues. Par exemple, dans le cas où vous utiliseriez des grosses structures de données, muter une instance déjà existante peut être plus rapide que copier et retourner une instance nouvellement allouée. Avec des structures de données plus petites, créer de nouvelles instances avec un style de programmation fonctionnelle peut rendre le code plus facile à comprendre, donc il peut valoir le coup de sacrifier un peu de performance pour que le code gagne en clarté.

Les constantes

Comme les variables immuables, les constantes sont des valeurs qui sont liées à un nom et qui ne peuvent être modifiées, mais il y a quelques différences entre les constantes et les variables.

D'abord, vous ne pouvez pas utiliser mut avec les constantes. Les constantes ne sont pas seulement immuables par défaut − elles sont toujours immuables. On déclare les constantes en utilisant le mot-clé const à la place du mot-clé let, et le type de la valeur doit être indiqué. Nous allons aborder les types et les annotations de types dans la prochaine section, “Les types de données”, donc ne vous souciez pas des détails pour le moment. Sachez seulement que vous devez toujours indiquer le type.

Les constantes peuvent être déclarées à n'importe quel endroit du code, y compris la portée globale, ce qui les rend très utiles pour des valeurs que de nombreuses parties de votre code ont besoin de connaître.

La dernière différence est que les constantes ne peuvent être définies que par une expression constante, et non pas le résultat d'une valeur qui ne pourrait être calculée qu'à l'exécution.

Voici un exemple d'une déclaration de constante :


#![allow(unused)]
fn main() {
const TROIS_HEURES_EN_SECONDES: u32 = 60 * 60 * 3;
}

Le nom de la constante est TROIS_HEURES_EN_SECONDES et sa valeur est définie comme étant le résultat de la multiplication de 60 (le nombre de secondes dans une minute) par 60 (le nombre de minutes dans une heure) par 3 (le nombre d'heures que nous voulons calculer dans ce programme). En Rust, la convention de nommage des constantes est de les écrire tout en majuscule avec des tirets bas entre les mots. Le compilateur peut calculer un certain nombre d'opérations à la compilation, ce qui nous permet d'écrire cette valeur de façon à la comprendre plus facilement et à la vérifier, plutôt que de définir cette valeur à 10 800. Vous pouvez consulter la section de la référence Rust à propos des évaluations des constantes pour en savoir plus sur les opérations qui peuvent être utilisées pour déclarer des constantes.

Les constantes sont valables pendant toute la durée d'exécution du programme au sein de la portée dans laquelle elles sont déclarées. Cette caractéristique rends les constantes très utiles lorsque plusieurs parties du programme doivent connaître certaines valeurs, comme par exemple le nombre maximum de points qu'un joueur est autorisé à gagner ou encore la vitesse de la lumière.

Déclarer des valeurs codées en dur et utilisées tout le long de votre programme en tant que constantes est utile pour faire comprendre la signification de ces valeurs dans votre code aux futurs développeurs. Cela permet également de n'avoir qu'un seul endroit de votre code à modifier si cette valeur codée en dur doit être mise à jour à l'avenir.

Le masquage

Comme nous l'avons vu dans le Chapitre 2, on peut déclarer une nouvelle variable avec le même nom qu'une variable précédente. Les Rustacés disent que la première variable est masquée par la seconde, ce qui signifie que la valeur de la seconde variable sera ce que le programme verra lorsque nous utiliserons cette variable. Nous pouvons créer un masque d'une variable en utilisant le même nom de variable et en réutilisant le mot-clé let comme ci-dessous :

Fichier : src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("La valeur de x dans la portée interne est : {}", x);
    }

    println!("La valeur de x est : {}", x);
}

Au début, ce programme lie x à la valeur 5. Puis il crée un masque de x en répétant let x =, en récupérant la valeur d'origine et lui ajoutant 1 : la valeur de x est désormais 6. Ensuite, à l'intérieur de la portée interne, la troisième instruction let crée un autre masque de x, en récupérant la précédente valeur et en la multipliant par 2 pour donner à x la valeur finale de 12. Dès que nous sortons de cette portée, le masque prends fin, et x revient à la valeur 6. Lorsque nous exécutons ce programme, nous obtenons ceci :

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/variables`
La valeur de x dans la portée interne est : 12
La valeur de x est : 6

Créer un masque est différent que de marquer une variable comme étant mut, car à moins d'utiliser une nouvelle fois le mot-clé let, nous obtiendrons une erreur de compilation si nous essayons de réassigner cette variable par accident. Nous pouvons effectuer quelques transformations sur une valeur en utilisant let, mais faire en sorte que la variable soit immuable après que ces transformations ont été appliquées.

Comme nous créons une nouvelle variable lorsque nous utilisons le mot-clé let une nouvelle fois, l'autre différence entre le mut et la création d'un masque est que cela nous permet de changer le type de la valeur, mais en réutilisant le même nom. Par exemple, imaginons un programme qui demande à l'utilisateur le nombre d'espaces qu'il souhaite entre deux portions de texte en saisissant des espaces, et ensuite nous voulons stocker cette saisie sous forme de nombre :

fn main() {
    let espaces = "   ";
    let espaces = espaces.len();
}

La première variable espaces est du type chaîne de caractères (string) et la seconde variable espaces est du type nombre. L'utilisation du masquage nous évite ainsi d'avoir à trouver des noms différents, comme espaces_str et espaces_num ; nous pouvons plutôt simplement réutiliser le nom espaces. Cependant, si nous essayons d'utiliser mut pour faire ceci, comme ci-dessous, nous avons une erreur de compilation :

fn main() {
    let mut espaces = "   ";
    espaces = espaces.len();
}

L'erreur indique que nous ne pouvons pas muter le type d'une variable :

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
2 |     let mut espaces = "   ";
  |                       ----- expected due to this value
3 |     espaces = espaces.len();
  |               ^^^^^^^^^^^^^ expected `&str`, found `usize`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` due to previous error

Maintenant que nous avons découvert comment fonctionnent les variables, étudions les types de données qu'elles peuvent prendre.