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.