Stocker des listes de valeurs avec les vecteurs
Le premier type de collection que nous allons examiner est Vec<T>, également appelé vector. Les vectors vous permettent de stocker plus d’une valeur dans une seule structure de données qui place toutes les valeurs les unes à côté des autres en mémoire. Les vectors ne peuvent stocker que des valeurs du même type. Ils sont utiles lorsque vous avez une liste d’éléments, comme les lignes de texte dans un fichier ou les prix des articles dans un panier.
Créer un nouveau vector
Pour créer un nouveau vector vide, nous appelons la fonction Vec::new, comme montré dans l’encart 8-1.
fn main() {
let v: Vec<i32> = Vec::new();
}
i32Notez que nous avons ajouté une annotation de type ici. Comme nous n’insérons aucune valeur dans ce vector, Rust ne sait pas quel type d’éléments nous avons l’intention de stocker. C’est un point important. Les vectors sont implémentés à l’aide de la généricité ; nous verrons comment utiliser la généricité avec vos propres types au chapitre 10. Pour l’instant, sachez que le type Vec<T> fourni par la bibliothèque standard peut contenir n’importe quel type. Quand nous créons un vector pour contenir un type spécifique, nous pouvons préciser le type entre chevrons. Dans l’encart 8-1, nous avons indiqué à Rust que le Vec<T> dans v contiendra des éléments de type i32.
Le plus souvent, vous créerez un Vec<T> avec des valeurs initiales, et Rust déduira le type de valeur que vous souhaitez stocker, de sorte que vous avez rarement besoin de faire cette annotation de type. Rust fournit la macro vec! pour plus de commodité, qui crée un nouveau vector contenant les valeurs que vous lui donnez. L’encart 8-2 crée un nouveau Vec<i32> qui contient les valeurs 1, 2 et 3. Le type entier est i32 car c’est le type entier par défaut, comme nous l’avons vu dans la section [“Types de données”][data-types] du chapitre 3.
fn main() {
let v = vec![1, 2, 3];
}
Comme nous avons donné des valeurs i32 initiales, Rust peut déduire que le type de v est Vec<i32>, et l’annotation de type n’est pas nécessaire. Voyons maintenant comment modifier un vector.
Mettre à jour un vector
Pour créer un vector puis y ajouter des éléments, nous pouvons utiliser la méthode push, comme montré dans l’encart 8-3.
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
}
push method to add values to a vectorComme 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 plaçons à l’intérieur sont tous de type i32, et Rust le déduit à partir des données, donc nous n’avons pas besoin de l’annotation Vec<i32>.
Lire les éléments d’un vector
Il existe deux façons de référencer une valeur stockée dans un vector : par l’indexation ou en utilisant la méthode get. Dans les exemples suivants, nous avons annoté les types des valeurs renvoyées par ces fonctions pour plus de clarté.
L’encart 8-4 montre les deux méthodes d’accès à une valeur dans un vector, avec la syntaxe d’indexation et la méthode get.
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
}
get method to access an item in a vectorNotez quelques détails ici. Nous utilisons l’indice 2 pour obtenir le troisième élément car les vectors sont indexés par numéro, en commençant à zéro. L’utilisation de & et [] nous donne une référence à l’élément situé à cet indice. Lorsque nous utilisons la méthode get avec l’indice passé en argument, nous obtenons un Option<&T> que nous pouvons utiliser avec match.
Rust fournit ces deux façons de référencer un élément afin que vous puissiez choisir comment le programme se comporte lorsque vous essayez d’utiliser un indice en dehors de la plage des éléments existants. Par exemple, voyons ce qui se passe lorsque nous avons un vector de cinq éléments et que nous essayons d’accéder à un élément à l’indice 100 avec chaque technique, comme montré dans l’encart 8-5.
fn main() {
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
}
Lorsque nous exécutons ce code, la première méthode avec [] provoquera un panic du programme car elle référence un élément inexistant. Cette méthode est à privilégier lorsque vous souhaitez que votre programme plante s’il y à une tentative d’accès à un élément au-delà de la fin du vector.
Lorsque la méthode get reçoit un indice en dehors du vector, elle renvoie None sans paniquer. Vous utiliseriez cette méthode si l’accès à un élément au-delà de la plage du vector peut arriver occasionnellement dans des circonstances normales. Votre code aura alors la logique pour gérer soit Some(&element) soit None, comme nous l’avons vu au chapitre 6. Par exemple, l’indice pourrait provenir d’une personne qui saisit un nombre. Si elle entre accidentellement un nombre trop grand et que le programme obtient une valeur None, vous pourriez indiquer à l’utilisateur combien d’éléments se trouvent dans le vector actuel et lui donner une autre chance de saisir une valeur valide. Ce serait plus convivial que de faire planter le programme à cause d’une faute de frappe !
Lorsque le programme détient une référence valide, le vérificateur d’emprunt applique les règles de possession et d’emprunt (couvertes au chapitre 4) pour garantir que cette référence et toute autre référence au contenu du vector restent valides. Rappelez-vous la règle qui stipule que vous ne pouvez pas avoir des références mutables et immuables dans la même portée. Cette règle s’applique dans l’encart 8-6, où nous détenons une référence immuable au premier élément d’un vector et essayons d’ajouter un élément à la fin. Ce programme ne fonctionnera pas si nous essayons aussi de faire référence à cet élément plus tard dans la fonction.
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
}
La compilation de ce code produira cette erreur : console {{#include ../listings/ch08-common-collections/listing-08-06/output.txt}}
$ 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 first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("The first element is: {first}");
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error
Le code de l’encart 8-6 pourrait sembler devoir fonctionner : pourquoi une référence au premier élément se soucierait-elle de changements à la fin du vector ? Cette erreur est due au fonctionnement des vectors : comme les vectors placent les valeurs les unes à côté des autres en mémoire, l’ajout d’un nouvel élément à la fin du vector pourrait nécessiter l’allocation de nouvelle mémoire et la copie des anciens éléments vers le nouvel espace, s’il n’y a pas assez de place pour mettre tous les éléments les uns à côté des autres là où le vector est actuellement stocké. 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 empêchent les programmes de se retrouver dans cette situation.
Remarque : Pour plus de détails sur l’implémentation du type
Vec<T>, consultez [“The Rustonomicon”][nomicon].
Itérer sur les valeurs d’un vector
Pour accéder à chaque élément d’un vector à tour de rôle, nous itérerions sur tous les éléments plutôt que d’utiliser des indices pour y accéder un par un. L’encart 8-7 montre comment utiliser une boucle for pour obtenir des références immuables à chaque élément d’un vector de valeurs i32 et les afficher.
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
}
for loopNous pouvons également itérer sur des références mutables à chaque élément d’un vector mutable afin d’apporter des modifications à tous les éléments. La boucle for de l’encart 8-8 ajoutera 50 à chaque élément.
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
}
Pour modifier la valeur à laquelle la référence mutable fait référence, nous devons utiliser l’opérateur de déréférencement * pour accéder à la valeur dans i avant de pouvoir utiliser l’opérateur +=. Nous parlerons davantage de l’opérateur de déréférencement dans la section [“Suivre la référence jusqu’à la valeur”][deref] du chapitre 15.
Itérer sur un vector, que ce soit de manière immuable ou mutable, est sûr grâce aux règles du vérificateur d’emprunt. Si nous tentions d’insérer ou de supprimer des éléments dans le corps des boucles for des encarts 8-7 et 8-8, nous obtiendrions une erreur de compilation similaire à celle que nous avons obtenue avec le code de l’encart 8-6. La référence au vector que la boucle for détient empêche la modification simultanée de l’ensemble du vector.
Utiliser une enum pour stocker plusieurs types
Les vectors ne peuvent stocker que des valeurs du même type. Cela peut être gênant ; il existe assurément des cas d’utilisation où l’on a besoin de stocker une liste d’éléments de différents types. Heureusement, les variantes d’une enum sont définies sous le même type d’enum, donc lorsque nous avons besoin d’un seul type pour représenter des éléments de différents types, nous pouvons définir et utiliser une enum !
Par exemple, supposons que nous voulions obtenir des valeurs d’une ligne dans un tableur dans lequel certaines colonnes de la ligne contiennent des entiers, d’autres des nombres à virgule flottante, et d’autres des chaînes de caractères. Nous pouvons définir une enum dont les variantes contiendront les différents types de valeurs, et toutes les variantes de l’enum seront considérées comme le même type : celui de l’enum. Ensuite, nous pouvons créer un vector pour contenir cette enum et ainsi, au final, contenir des types différents. Nous l’avons démontré dans l’encart 8-9.
fn main() {
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}
Rust a besoin de savoir quels types seront dans le vector à la compilation afin de savoir exactement combien de mémoire sur le tas sera nécessaire pour stocker chaque élément. Nous devons également être explicites sur les types autorisés dans ce vector. Si Rust permettait à un vector de contenir n’importe quel type, il y aurait un risque qu’un où plusieurs de ces types causent des erreurs avec les opérations effectuées sur les éléments du vector. L’utilisation d’une enum combinée à une expression match signifie que Rust s’assurera à la compilation que chaque cas possible est traité, comme nous l’avons vu au chapitre 6.
Si vous ne connaissez pas l’ensemble exhaustif des types qu’un programme recevra à l’exécution pour les stocker dans un vector, la technique de l’enum ne fonctionnera pas. À la place, vous pouvez utiliser un objet trait, que nous couvrirons au chapitre 18.
Maintenant que nous avons discuté de certaines des façons les plus courantes d’utiliser les vectors, n’oubliez pas de consulter [la documentation de l’API][vec-api] pour toutes les nombreuses méthodes utiles définies sur Vec<T> par la bibliothèque standard. Par exemple, en plus de push, une méthode pop supprime et renvoie le dernier élément.
Libérer un vector libère ses éléments
Comme toute autre struct, un vector est libéré lorsqu’il sort de la portée, comme annoté dans l’encart 8-10.
fn main() {
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
}
Lorsque le vector est libéré, tout son contenu est également libéré, ce qui signifie que les entiers qu’il contient seront nettoyés. Le vérificateur d’emprunt garantit que toute référence au contenu d’un vector n’est utilisée que tant que le vector lui-même est valide.
Passons au type de collection suivant : String !