Stocker des listes de valeurs avec des vecteurs
Le premier type de collection que nous allons voir est Vec<T>
, aussi appelé
vecteur. Les vecteurs vous permettent de stocker plus d'une valeur dans une
seule structure de données qui stocke les valeurs les unes à côté des autres
dans la mémoire. Les vecteurs peuvent stocker uniquement des valeurs du même
type. Ils sont utiles lorsque vous avez une liste d'éléments, tels que les
lignes de texte provenant d'un fichier ou les prix des articles d'un panier
d'achat.
Créer un nouveau vecteur
Pour créer un nouveau vecteur vide, nous appelons la fonction Vec::new
, comme
dans l'encart 8-1.
fn main() { let v: Vec<i32> = Vec::new(); }
Remarquez que nous avons ajouté ici une annotation de type. Comme nous
n'ajoutons pas de valeurs dans ce vecteur, Rust ne sait pas quel type d'éléments
nous souhaitons stocker. C'est une information importante. Les vecteurs sont
implémentés avec la généricité ; nous verrons comment utiliser la généricité sur
vos propres types au chapitre 10. Pour l'instant, sachez que le type Vec<T>
qui est fourni par la bibliothèque standard peut stocker n'importe quel type.
Lorsque nous créons un vecteur pour stocker un type précis, nous pouvons
renseigner ce type entre des chevrons. Dans l'encart 8-1, nous précisons à Rust
que le Vec<T>
dans v
va stocker des éléments de type i32
.
Le plus souvent, vous allez créer un Vec<T>
avec des valeurs initiales et
Rust va deviner le type de la valeur que vous souhaitez stocker, donc vous
n'aurez pas souvent besoin de faire cette annotation de type. Rust propose la
macro très pratique vec!
, qui va créer un nouveau vecteur qui stockera les
valeurs que vous lui donnerez. L'encart 8-2 crée un nouveau Vec<i32>
qui
stocke les valeurs 1
, 2
et 3
. Le type d'entier est i32
car c'est le
type d'entier par défaut, comme nous l'avons évoqué dans la section “Les types
de données” du chapitre 3.
fn main() { let v = vec![1, 2, 3]; }
Comme nous avons donné des valeurs initiales i32
, Rust peut en déduire que le
type de v
est Vec<i32>
, et l'annotation de type n'est plus nécessaire.
Maintenant, nous allons voir comment modifier un vecteur.
Modifier un vecteur
Pour créer un vecteur et ensuite lui ajouter des éléments, nous pouvons utiliser
la méthode push
, comme dans l'encart 8-3.
fn main() { let mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8); }
Comme pour toute variable, si nous voulons pouvoir modifier sa valeur, nous
devons la rendre mutable en utilisant le mot-clé mut
, comme nous l'avons vu
au chapitre 3. Les nombres que nous ajoutons dedans sont tous du type i32
, et
Rust le devine à partir des données, donc nous n'avons pas besoin de
l'annotation Vec<i32>
.
Libérer un vecteur libère aussi ses éléments
Comme toutes les autres structures, un vecteur est libéré quand il sort de la portée, comme précisé dans l'encart 8-4.
fn main() { { let v = vec![1, 2, 3, 4]; // on fait des choses avec v } // <- v sort de la portée et est libéré ici }
Lorsque le vecteur est libéré, tout son contenu est aussi libéré, ce qui veut dire que les nombres entiers qu'il stocke vont être effacés de la mémoire. Cela semble très simple mais cela peut devenir plus compliqué quand vous commencez à utiliser des références vers les éléments du vecteur. Voyons ceci dès à présent !
Lire les éléments des vecteurs
Il existe deux façons de désigner une valeur enregistrée dans un vecteur : via
les indices ou en utilisant la méthode get
. Dans les exemples suivants, nous
avons précisé les types des valeurs qui sont retournées par ces fonctions pour
plus de clarté.
L'encart 8-5 nous montre les deux façons d'accéder à une valeur d'un vecteur,
via la syntaxe d'indexation et avec la méthode get
.
fn main() { let v = vec![1, 2, 3, 4, 5]; let troisieme: &i32 = &v[2]; println!("Le troisième élément est {}", troisieme); match v.get(2) { Some(troisieme) => println!("Le troisième élément est {}", troisieme), None => println!("Il n'y a pas de troisième élément."), } }
Il y a deux détails à remarquer ici. Premièrement, nous avons utilisé l'indice
2
pour obtenir le troisième élément car les vecteurs sont indexés par des
nombres, qui commencent à partir de zéro. Deuxièmement, nous obtenons le
troisième élément soit en utilisant &
et []
, ce qui nous donne une
référence, soit en utilisant la méthode get
avec l'indice en argument, ce qui
nous fournit une Option<&T>
.
La raison pour laquelle Rust offre ces deux manières d'obtenir une référence vers un élement est de vous permettre de choisir le comportement du programme lorsque vous essayez d'utiliser une valeur dont l'indice est à l'extérieur de la plage des éléments existants. Par exemple, voyons dans l'encart 8-6 ce qui se passe lorsque nous avons un vecteur de cinq éléments et qu'ensuite nous essayons d'accéder à un élément à l'indice 100 avec chaque technique.
fn main() { let v = vec![1, 2, 3, 4, 5]; let existe_pas = &v[100]; let existe_pas = v.get(100); }
Lorsque nous exécutons ce code, la première méthode []
va faire paniquer le
programme car il demande un élément non existant. Cette méthode doit être
favorisée lorsque vous souhaitez que votre programme plante s'il y a une
tentative d'accéder à un élément après la fin du vecteur.
Lorsque nous passons un indice en dehors de l'intervalle du vecteur à la
méthode get
, elle retourne None
sans paniquer. Vous devriez utiliser cette
méthode s'il peut arriver occasionnellement de vouloir accéder à un élément en
dehors de l'intervalle du vecteur en temps normal. Votre code va ensuite devoir
gérer les deux valeurs Some(&element)
ou None
, comme nous l'avons vu au
chapitre 6. Par exemple, l'indice peut provenir d'une saisie utilisateur. Si
par accident il saisit un nombre qui est trop grand et que le programme obtient
une valeur None
, vous pouvez alors dire à l'utilisateur combien il y a
d'éléments dans le vecteur courant et lui donner une nouvelle chance de saisir
une valeur valide. Cela sera plus convivial que de faire planter le programme à
cause d'une faute de frappe !
Lorsque le programme obtient une référence valide, le vérificateur d'emprunt va faire appliquer les règles de possession et d'emprunt (que nous avons vues au chapitre 4) pour s'assurer que cette référence ainsi que toutes les autres références au contenu de ce vecteur restent valides. Souvenez-vous de la règle qui dit que vous ne pouvez pas avoir des références mutables et immuables dans la même portée. Cette règle s'applique à l'encart 8-7, où nous obtenons une référence immuable vers le premier élément d'un vecteur et nous essayons d'ajouter un élément à la fin. Ce programme ne fonctionnera pas si nous essayons aussi d'utiliser cet élément plus tard dans la fonction :
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let premier = &v[0];
v.push(6);
println!("Le premier élément est : {}", premier);
}
Compiler ce code va nous mener à cette erreur :
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let premier = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("Le premier élément est : {}", premier);
| ------- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` due to previous error
Le code dans l'encart 8-7 semble pourtant marcher : pourquoi une référence au premier élément devrait se soucier de ce qui se passe à la fin du vecteur ? Cette erreur s'explique par la façon dont les vecteurs fonctionnent : comme les vecteurs ajoutent les valeurs les unes à côté des autres dans la mémoire, l'ajout d'un nouvel élément à la fin du vecteur peut nécessiter d'allouer un nouvel espace mémoire et copier tous les anciens éléments dans ce nouvel espace, s'il n'y a pas assez de place pour placer tous les éléments les uns à côté des autres dans la mémoire là où est actuellement stocké le vecteur. Dans ce cas, la référence au premier élément pointerait vers de la mémoire désallouée. Les règles d'emprunt évitent aux programmes de se retrouver dans cette situation.
Remarque : pour plus de détails sur l'implémentation du type
Vec<T>
, consultez le “Rustonomicon”.
Itérer sur les valeurs d'un vecteur
Pour accéder à chaque élément d'un vecteur chacun son tour, nous devrions
itérer sur tous les éléments plutôt que d'utiliser individuellement les
indices. L'encart 8-8 nous montre comment utiliser une boucle for
pour
obtenir des références immuables pour chacun des éléments dans un vecteur de
i32
, et les afficher.
fn main() { let v = vec![100, 32, 57]; for i in &v { println!("{}", i); } }
Nous pouvons aussi itérer avec des références mutables pour chacun des éléments
d'un vecteur mutable afin de modifier tous les éléments. La boucle for
de
l'encart 8-9 va ajouter 50
à chacun des éléments.
fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } }
Afin de changer la valeur vers laquelle pointe la référence mutable, nous devons
utiliser l'opérateur de déréférencement *
pour obtenir la valeur dans i
avant que nous puissions utiliser l'opérateur +=
. Nous verrons plus en détail
l'opérateur de déréférencement dans une section du
chapitre 15.
Utiliser une énumération pour stocker différents types
Les vecteurs ne peuvent stocker que des valeurs du même type. Cela peut être un problème ; il y a forcément des cas où on a besoin de stocker une liste d'éléments de types différents. Heureusement, les variantes d'une énumération sont définies sous le même type d'énumération, donc lorsque nous avons besoin d'un type pour représenter les éléments de types différents, nous pouvons définir et utiliser une énumération !
Par exemple, imaginons que nous voulions obtenir les valeurs d'une ligne d'une feuille de calcul dans laquelle quelques colonnes sont des entiers, d'autres des nombres à virgule flottante, et quelques chaînes de caractères. Nous pouvons définir une énumération dont les variantes vont avoir les différents types, et toutes les variantes de l'énumération seront du même type : celui de l'énumération. Ensuite, nous pouvons créer un vecteur pour stocker cette énumération et ainsi, au final, qui stocke différents types. La démonstration de cette technique est dans l'encart 8-10.
fn main() { enum Cellule { Int(i32), Float(f64), Text(String), } let ligne = vec![ Cellule::Int(3), Cellule::Text(String::from("bleu")), Cellule::Float(10.12), ]; }
Rust a besoin de savoir quel type de donnée sera stocké dans le vecteur au
moment de la compilation afin de connaître la quantité de mémoire nécessaire
pour stocker chaque élément sur le tas. Nous devons être précis sur les types
autorisés dans ce vecteur. Si Rust avait permis qu'un vecteur stocke n'importe
quel type, il y aurait pu avoir un risque qu'un ou plusieurs des types
provoquent une erreur avec les manipulations effectuées sur les éléments du
vecteur. L'utilisation d'une énumération ainsi qu'une expression match
permet
à Rust de garantir au moment de la compilation que tous les cas possibles sont
traités, comme nous l'avons appris au chapitre 6.
Si vous n'avez pas une liste exhaustive des types que votre programme va stocker dans un vecteur, la technique de l'énumération ne va pas fonctionner. À la place, vous pouvez utiliser un objet trait, que nous verrons au chapitre 17.
Maintenant que nous avons vu les manières les plus courantes d'utiliser les
vecteurs, prenez le temps de consulter la documentation de
l'API pour découvrir toutes les méthodes très utiles
définies dans la bibliothèque standard pour Vec<T>
. Par exemple, en plus de
push
, nous avons une méthode pop
qui retire et retourne le dernier élément.
Intéressons-nous maintenant au prochain type de collection : la String
!