17. Les types de la bibliothèque standard▲
La bibliothèque standard fournit de nombreux types complexes qui étendent drastiquement l'utilisation des primitifs. Voici certains d'entre-eux :
- Les chaînes de caractères redimensionnables String :
"hello world"
; - Les vecteurs redimensionnables : [
1
,2
,3
] ; - Les types optionnels :
Option
<i32> ; - Les types dédiés à la gestion des erreurs :
Result
<i32, i32> ; - Les pointeurs alloués dans le tas : Box<i32>.
Voir aussi
Les primitifs et la bibliothèque standard.
17-1. Les Box, la pile et le tas▲
En Rust, toutes les valeurs sont allouées dans la pile, par défaut. Les valeurs peuvent être boxées (i.e. allouées dans le tas) en créant une instance de Box<T>. Une "box" est un pointeur intelligent sur une ressource de type T allouée dans le tas. Lorsqu'une box sort du contexte, son destructeur est appelé, l'objet à charge est détruit et la mémoire du tas libérée.
Les valeurs « boxées » peuvent être déréférencées en utilisant l'opérateur * ; Ceci supprime un niveau d'indirection.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
use
std::mem;
#[derive(Clone, Copy)]
struct
Point {
x: f64,
y: f64,
}
#[allow(dead_code)]
struct
Rectangle {
p1: Point,
p2: Point,
}
fn
origin() -> Point {
Point { x: 0
.0
, y: 0
.0
}
}
fn
boxed_origin() -> Box<Point> {
// Alloue cette instance de `Point` dans le tas et renvoie un pointeur
// sur cette dernière.
Box::new(Point { x: 0
.0
, y: 0
.0
})
}
fn
main() {
// (Ici le typage est superflu)
// Variables allouées dans la pile.
let
point: Point = origin();
let
rectangle: Rectangle = Rectangle {
p1: origin(),
p2: Point { x: 3
.0
, y: 4
.0
}
};
// Rectangle alloué dans le tas.
let
boxed_rectangle: Box<Rectangle> = Box::new(Rectangle {
p1: origin(),
p2: origin()
});
// Le résultat des fonctions peut être boxé également.
let
boxed_point: Box<Point> = Box::new(origin());
// Double indirection
let
box_in_a_box: Box<Box<Point>> = Box::new(boxed_origin());
println!
("Point occupies
{}
bytes in the stack"
,
mem::size_of_val(&
point));
println!
("Rectangle occupies
{}
bytes in the stack"
,
mem::size_of_val(&
rectangle));
// La taille du pointeur est égale à la taille de la Box.
println!
("Boxed point occupies
{}
bytes in the stack"
,
mem::size_of_val(&
boxed_point));
println!
("Boxed rectangle occupies
{}
bytes in the stack"
,
mem::size_of_val(&
boxed_rectangle));
println!
("Boxed box occupies
{}
bytes in the stack"
,
mem::size_of_val(&
box_in_a_box));
// La ressource contenue dans `boxed_point` est copiée dans
// `unboxed_point`.
let
unboxed_point: Point = *boxed_point;
println!
("Unboxed point occupies
{}
bytes in the stack"
,
mem::size_of_val(&
unboxed_point));
}
17-2. Les vecteurs▲
Les vecteurs sont des tableaux redimensionnables. Tout comme les slices, leur taille n'est pas connue à la compilation mais ils peuvent être agrandis ou tronqués au cours de l'exécution. Un vecteur est représenté par trois (3) mots : un pointeur sur la ressource, sa taille et sa capacité. La capacité indique la quantité de mémoire réservée au vecteur. La taille peut augmenter à volonté, tant qu'elle est inférieure à la capacité. Lorsqu'il est nécessaire de franchir cette limite, le vecteur est réalloué avec une capacité plus importante.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
fn
main() {
// Les éléments des itérateurs peuvent être collectés et
// ajoutés dans un vecteur.
let
collected_iterator: Vec<i32> = (0
..10
).collect();
println!
("Collected (0..10) into:
{:?}
"
, collected_iterator);
// La macro `vec!` peut être utilisée pour initialiser un vecteur.
let
mut
xs = vec!
[1
i32, 2
, 3
];
println!
("Initial vector:
{:?}
"
, xs);
// On ajoute un nouvel élément à la fin du vecteur.
println!
("Push 4 into the vector"
);
xs.push(4
);
println!
("Vector:
{:?}
"
, xs);
// Erreur! Les vecteurs immuables ne peuvent pas être
// agrandis.
// collected_iterator.push(0);
// FIXME ^ Commentez/décommentez cette ligne
// La méthode `len` renvoie la taille actuelle du vecteur.
println!
("Vector size:
{}
"
, xs.len());
// L'indexation peut être faite à l'aide des "[]" (l'indexaction débute à 0).
println!
("Second element:
{}
"
, xs[1
]);
// `pop` supprime le dernier élément du vecteur et le renvoie.
println!
("Pop last element:
{:?}
"
, xs.pop());
// Une indexaction hors des capacités du vecteur
// mène à un plantage du programme.
println!
("Fourth element:
{}
"
, xs[3
]);
}
Les méthodes rattachées à la structure Vec peuvent être trouvées au sein du module std::vec.
17-3. Les chaînes de caractères▲
Il y a deux types de chaînes de caractères en Rust : String et &
str.
Une instance de String est stockée en tant que vecteur d'octets (Vec<u8>) mais garantit de toujours fournir une séquence valide encodée en UTF-8. String est alloué dans le tas, redimensionnable et non-nul.
&
str est une slice (&
[u8]) qui pointe toujours sur une séquence UTF-8 valide et peut être utilisée comme une vue sur une String. Tout comme &
[T] est une vue sur une instance Vec<T>.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
fn
main() {
// (Le typage est optionnel)
// Une référence d'une chaîne de caractères immuable.
let
pangram: &
'static
str = "the quick brown fox jumps over the lazy dog"
;
println!
("Pangram:
{}
"
, pangram);
// On itère sur les mots dans le sens inverse, aucune nouvelle instance
// n'est créée.
println!
("Words in reverse"
);
for
word in
pangram.split_whitespace().rev() {
println!
(">
{}
"
, word);
}
// On copie les caractères dans un vecteur, les trie et supprime
// les occurrences multiples.
let
mut
chars: Vec<char> = pangram.chars().collect();
chars.sort();
chars.dedup();
// On créé une instance de `String` vide et mutable.
let
mut
string = String::new();
for
c in
chars {
// On ajoute un caractère à la fin de la chaîne.
string.push(c);
// On ajoute une nouvelle chaîne à la fin de la chaîne initiale.
string.push_str(", "
);
}
// La chaîne tronquée est une slice de la chaîne originale, il n'y a
// pas de nouvelle allocation.
let
chars_to_trim: &
[char] = &
[' '
, ','
];
let
trimmed_str: &
str = string.trim_matches(chars_to_trim);
println!
("Used characters:
{}
"
, trimmed_str);
// Chaîne allouée dans le tas.
let
alice = String::from("I like dogs"
);
// Nouvelle allocation mémoire et stockage de la chaîne modifiée
// à cet endroit.
let
bob: String = alice.replace("dog"
, "cat"
);
println!
("Alice says:
{}
"
, alice);
println!
("Bob says:
{}
"
, bob);
}
Les méthodes rattachées à str/String peuvent être trouvées dans les modules std::str et std::string.
17-4. L'énumération Option▲
Il est parfois désirable de rattraper les erreurs provenants de différentes parties du programme plutôt que d'appeler panic!
; pour ce faire, l'enum Option
prend le relais.
L'enum Option
<T> possède deux variantes :
None
, pour signaler une erreur ou l'absence d'une valeur, etSome
(value), un tuple qui enveloppe, contient une valeur de type T.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
// Une division entre deux entiers qui ne plante pas.
fn
checked_division(dividend: i32, divisor: i32) -> Option
<i32> {
if
divisor == 0
{
// L'échec est représenté par la variante `None`.
None
} else
{
// Le résultat est enveloppé dans une instance `Some`.
Some
(dividend / divisor)
}
}
// Cette fonction gère une division qui peut ne pas fonctionner.
fn
try_division(dividend: i32, divisor: i32) {
// Les valeurs d'`Option` peuvent être matchées, tout comme les autres enums.
match
checked_division(dividend, divisor) {
None
=> println!
("
{}
/
{}
failed!"
, dividend, divisor),
Some
(quotient) => {
println!
("
{}
/
{}
=
{}
"
, dividend, divisor, quotient)
},
}
}
fn
main() {
try_division(4
, 2
);
try_division(1
, 0
);
// L'assignation de `None` à une variable nécessite de typer cette dernière.
let
none: Option
<i32> = None
;
let
_equivalent_none = None
::<i32>;
let
optional_float = Some
(0
f32);
// "dé-wrapper" une instance de `Some` va extraire la valeur contenue.
println!
("
{:?}
unwraps to
{:?}
"
, optional_float, optional_float.unwrap());
// Tenter d'utiliser `unwrap` sur la variante `None` fera planter le programme.
println!
("
{:?}
unwraps to
{:?}
"
, none, none.unwrap());
}
17-5. L'énumération Result▲
Nous avons vu que l'enum Option
peut être utilisée en tant que valeur de retour depuis les fonctions pouvant échouer, où None
peut être renvoyé pour indiquer un échec. Il est parfois important d'expliquer pourquoi une opération a échoué. Pour ce faire, nous pouvons utiliser Result
.
Result
<T, E> possède deux variantes :
Ok
(valeur) qui signale que l'opération s'est correctement déroulée et enveloppe la valeur renvoyée par l'opération (valeur est de type T);Err
(pourquoi) qui signale que l'opération a échoué et enveloppe le pourquoi, qui (espérons-le) nous renseigne sur la cause de l'échec (pourquoi est de type E).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
mod
checked {
// Les "erreurs" mathématiques que nous voulons gérer.
#[derive(Debug)]
pub
enum
MathError {
DivisionByZero,
NonPositiveLogarithm,
NegativeSquareRoot,
}
pub
type
MathResult = Result
<f64, MathError>;
pub
fn
div(x: f64, y: f64) -> MathResult {
if
y == 0
.0
{
// Cette opération échouerait, nous wrappons l'erreur dans une instance
// `Err` à la place.
Err
(MathError::DivisionByZero)
} else
{
// Cette opération est valide, nous renvoyons le résultat wrappé dans `Ok`.
Ok
(x / y)
}
}
pub
fn
sqrt(x: f64) -> MathResult {
if
x < 0
.0
{
Err
(MathError::NegativeSquareRoot)
} else
{
Ok
(x.sqrt())
}
}
pub
fn
ln(x: f64) -> MathResult {
if
x <= 0
.0
{
Err
(MathError::NonPositiveLogarithm)
} else
{
Ok
(x.ln())
}
}
}
// `op(x, y)` === `sqrt(ln(x / y))`
fn
op(x: f64, y: f64) -> f64 {
// Ceci est une pyramide de match à trois niveaux !
match
checked::div(x, y) {
Err
(why) => panic!
("
{:?}
"
, why),
Ok
(ratio) => match
checked::ln(ratio) {
Err
(why) => panic!
("
{:?}
"
, why),
Ok
(ln) => match
checked::sqrt(ln) {
Err
(why) => panic!
("
{:?}
"
, why),
Ok
(sqrt) => sqrt,
},
},
}
}
fn
main() {
// Cette opération va-t-elle échouer ?
println!
("
{}
"
, op(1
.0
, 10
.0
));
}
17-5-1. La macro try!▲
Chaîner les résultats en utilisant match peut être très chaotique ; heureusement, la macro try!
peut être utilisée pour soigner l'écriture. La macro try!
étend une expression de match où la branche Err
(err) étend un retour prématuré (return
Err
(err)) et la branche Ok
(ok) étend une expression ok et fournit la ressource.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
mod
checked {
#[derive(Debug)]
enum
MathError {
DivisionByZero,
NegativeLogarithm,
NegativeSquareRoot,
}
type
MathResult = Result
<f64, MathError>;
fn
div(x: f64, y: f64) -> MathResult {
if
y == 0
.0
{
Err
(MathError::DivisionByZero)
} else
{
Ok
(x / y)
}
}
fn
sqrt(x: f64) -> MathResult {
if
x < 0
.0
{
Err
(MathError::NegativeSquareRoot)
} else
{
Ok
(x.sqrt())
}
}
fn
ln(x: f64) -> MathResult {
if
x < 0
.0
{
Err
(MathError::NegativeLogarithm)
} else
{
Ok
(x.ln())
}
}
// Fonction intermédiaire.
fn
op_(x: f64, y: f64) -> MathResult {
// Si la fonction `div` échoue, alors `DivisionByZero` sera renvoyée.
let
ratio = try!
(div(x, y));
// Si `ln` échoue, alors `NegativeLogarithm` sera renvoyée.
let
ln = try!
(ln(ratio));
sqrt(ln)
}
pub
fn
op(x: f64, y: f64) {
match
op_(x, y) {
Err
(why) => panic!
(match
why {
MathError::NegativeLogarithm
=> "logarithm of negative number"
,
MathError::DivisionByZero
=> "division by zero"
,
MathError::NegativeSquareRoot
=> "square root of negative number"
,
}),
Ok
(value) => println!
("
{}
"
, value),
}
}
}
fn
main() {
checked::op(1
.0
, 10
.0
);
}
N'hésitez pas à consulter la documentation, de nombreuses méthodes sont disponibles pour créer et gérer les Result
.
17-6. La macro panic!▲
La macro panic!
peut être utilisée pour générer un plantage et dérouler la pile. Pendant le déroulement de la pile, l'exécution prendra soin de libérer toutes les ressources possédées par le fil d'exécution en appelant le destructeur de chaque objet.
Puisque nous interagissons avec nos programmes en n'utilisant qu'un seul fil d'exécution, panic!
renverra un message d'erreur puis mettra un terme à l'exécution.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
// Ré-implémentation de la division d'entiers (/).
fn
division(dividend: i32, divisor: i32) -> i32 {
if
divisor == 0
{
// La division par zéro fait planter le thread courant.
panic!
("division by zero"
);
} else
{
dividend / divisor
}
}
fn
main() {
// Entier alloué dans le tas.
let
_x = Box::new(0
i32);
// Cette opération va déclencher la procédure d'abandon.
division(3
, 0
);
println!
("This point won't be reached!"
);
// `_x` devrait être détruit à ce niveau.
}
Vérifions que la macro panic!
ne cause aucune fuite mémoire.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
$
rustc panic.rs &&
valgrind ./panic
==
4401
==
Memcheck, a memory error detector
==
4401
==
Copyright (
C) 2002
-2013
, and GNU GPL′d, by Julian Seward et al.
==
4401
==
Using Valgrind-3
.10
.0
.SVN and LibVEX; rerun with -h for
copyright info
==
4401
==
Command: ./panic
==
4401
==
thread '<main>'
panicked at 'division by zero'
, panic.rs:5
==
4401
==
==
4401
==
HEAP SUMMARY:
==
4401
==
in
use at exit: 0
bytes in
0
blocks
==
4401
==
total heap usage: 18
allocs, 18
frees, 1
,648
bytes allocated
==
4401
==
==
4401
==
All heap blocks were freed -- no leaks are possible
==
4401
==
==
4401
==
For
counts of detected and suppressed errors, rerun with: -v
==
4401
==
ERROR SUMMARY: 0
errors from 0
contexts (
suppressed: 0
from 0
)
17-7. La structure HashMap▲
Là où les vecteurs stockent leurs valeurs en utilisant un index entier, les HashMaps stockent leurs valeurs en utilisant des clés. Les clés d'une HashMap peuvent être des booléens, des chaînes de caractères ou n'importe quel autre type qui implémente les traits Eq et Hash. Nous y reviendrons dans la section suivante.
Tout comme les vecteurs, les HashMap sont redimensionnables mais peuvent également se tronquer elles-mêmes lorsqu'elles atteignent la limite de leur capacité. Vous pouvez créer une HashMap avec une capacité donnée en utilisant HashMap::with_capacity(uint), ou utiliser HashMap::new() pour récupérer une instance avec une capacité initiale par défaut (recommandé).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
use
std::collections::HashMap;
fn
call(number: &
str) -> &
str {
match
number {
"798-1364"
=> "We're sorry, the call cannot be completed as dialed.
Please hang up and try again."
,
"645-7689"
=> "Hello, this is Mr. Awesome's Pizza. My name is Fred.
What can I get for you today?"
,
_ => "Hi! Who is this again?"
}
}
fn
main() {
let
mut
contacts = HashMap::new();
contacts.insert("Daniel"
, "798-1364"
);
contacts.insert("Ashley"
, "645-7689"
);
contacts.insert("Katie"
, "435-8291"
);
contacts.insert("Robert"
, "956-1745"
);
// Prend une référence en entrée et renvoie un conteneur `Option<&V>`.
match
contacts.get(&
"Daniel"
) {
Some
(&
number) => println!
("Calling Daniel:
{}
"
, call(number)),
_ => println!
("Don't have Daniel's number."
),
}
// La méthode `HashMap::insert()` renvoie `None`
// si la valeur insérée est nouvelle, sinon `Some(value)`.
contacts.insert("Daniel"
, "164-6743"
);
match
contacts.get(&
"Ashley"
) {
Some
(&
number) => println!
("Calling Ashley:
{}
"
, call(number)),
_ => println!
("Don't have Ashley's number."
),
}
contacts.remove(&
("Ashley"
));
// La méthode `HashMap::iter()` renvoie un itérateur qui fournit
// les paires (&'a key, &'a value) dans un ordre arbitraire.
for
(contact, &
number) in
contacts.iter() {
println!
("Calling
{}
:
{}
"
, contact, call(number));
}
}
Pour plus d"informations à propos du fonctionnement du hashage et des hash maps (parfois appelées hash tables), consultez la page wikipédia dédiée aux Hash Tables.
17-7-1. Personnaliser les types de clé▲
N'importe quel type implémentant les traits Eq et Hash peuvent être une clé dans une HashMap. Ce qui inclut :
- Le type bool (Bien que peut utile puisqu'il n'y a que deux clés possibles);
- Le type int, uint et toutes les variantes de ces derniers ;
- String et
&
str (note : vous pouvez avoir une HashMap recevant en entrée des String et appeler la méthode .get() avec une&
str).
Notez que f32 et f64 n'implémentent pas Hash, sûrement parce les erreurs de précision rendrait leur utilisation en tant que clé d'une hashmap poserait des soucis.
Toutes les classes représentant une collection implémentent Eq et Hash si le type qu'elles contiennent implémentent également ces deux traits. Par exemple, Vec<T> implémentera Hash si T l'implémente.
Vous pouvez facilement implémenter Eq et Hash pour un nouveau type avec cette seule ligne : #[derive(PartialEq, Hash)]
.
Le compilateur fera le reste. Si vous souhaitez avoir plus de contrôle sur les détails, vous pouvez implémenter Eq et/ou Hash vous-même. Ce guide ne couvre pas les implémentations spécifiques de Hash.
Pour tester l'utilisation d'une struct
dans une HashMap, créons un simple système d'identification :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
use
std::collections::HashMap;
// `Eq` nécessite de dériver `PartialEq` sur le type.
#[derive(PartialEq, Eq, Hash)]
struct
Account<'a
>{
username: &
'a
str,
password: &
'a
str,
}
struct
AccountInfo<'a
>{
name: &
'a
str,
email: &
'a
str,
}
type
Accounts<'a
> = HashMap<Account<'a
>, AccountInfo<'a
>>;
fn
try_logon<'a
>(accounts: &
Accounts<'a
>,
username: &
'a
str, password: &
'a
str){
println!
("Username:
{}
"
, username);
println!
("Password:
{}
"
, password);
println!
("Attempting logon..."
);
let
logon = Account {
username: username,
password: password,
};
match
accounts.get(&
logon) {
Some
(account_info) => {
println!
("Successful logon!"
);
println!
("Name:
{}
"
, account_info.name);
println!
("Email:
{}
"
, account_info.email);
},
_ => println!
("Login failed!"
),
}
}
fn
main(){
let
mut
accounts: Accounts = HashMap::new();
let
account = Account {
username: "j.everyman"
,
password: "password123"
,
};
let
account_info = AccountInfo {
name: "John Everyman"
,
email: "j.everyman@email.com"
,
};
accounts.insert(account, account_info);
try_logon(&
accounts, "j.everyman"
, "psasword123"
);
try_logon(&
accounts, "j.everyman"
, "password123"
);
}
17-7-2. La structure HashSet▲
Voyez une HashSet comme une HashMap où nous nous soucions uniquement des clés (HashSet<T> est, en réalité, simplement un wrapper de HashMap<T, ()>).
Vous pourriez vous demander "Quel est le but ? Je pourrais simplement stocker mes clés dans un Vec".
La fonctionnalité unique de HashSet est qu'elle garantit l'inexistance d'éléments dupliqués. C'est le contrat que n'importe quel ensemble remplit. HashSet n'est qu'une implémentation (voir aussi : BTreeSet).
Si vous ajoutez une valeur déjà présente dans l'instance HashSet, (i.e. la nouvelle valeur est égale à l'existante et ont toutes deux le même hash), alors la nouvelle valeur remplacera l'ancienne.
C'est pratique lorsque vous ne souhaitez jamais plus d'une occurrence de quelque chose ou lorsque vous voulez savoir si vous possédez déjà quelque chose. Mais les ensembles peuvent faire bien plus que cela.
Les ensembles ont quatre (4) opérations inhérentes (chacune renvoie un itérateur) :
- union : Récupère tous les éléments dans les deux ensembles ;
- difference : Récupère tous les éléments qui sont dans le premier ensemble mais pas dans le second ;
- intersection : Récupère uniquement les éléments présents dans les deux ensembles ;
- symmetric_difference : Récupère tous éléments qui sont dans le premier ou second ensemble mais pas les deux.
Essayons tout cela dans l'exemple suivant.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
use
std::collections::HashSet;
fn
main() {
let
mut
a: HashSet<i32> = vec!
(1
i32, 2
, 3
).into_iter().collect();
let
mut
b: HashSet<i32> = vec!
(2
i32, 3
, 4
).into_iter().collect();
assert!
(a.insert(4
));
assert!
(a.contains(&
4
));
// `HashSet::insert()` renvoie false si
// une valeur était déjà présente.
// assert!(b.insert(4), "Value 4 is already in set B!");
// FIXME ^ Commentez/décommentez cette ligne
b.insert(5
);
// Si le type d'un élément de la collection implémente le trait `Debug`,
// alors la collection devra, elle aussi, implémenter `Debug`.
// Elle affiche généralement ses éléments dans le format `[elem1, elem2, ...]`.
println!
("A:
{:?}
"
, a);
println!
("B:
{:?}
"
, b);
// Affiche [1, 2, 3, 4, 5] dans un ordre arbitraire.
println!
("Union:
{:?}
"
, a.union(&
b).collect::<Vec<&
i32>>());
// Ceci devrait afficher [1].
println!
("Difference:
{:?}
"
, a.difference(&
b).collect::<Vec<&
i32>>());
// Affiche [2, 3, 4] dans un ordre arbitraire.
println!
("Intersection:
{:?}
"
, a.intersection(&
b).collect::<Vec<&
i32>>());
// Affiche [1, 5].
println!
("Symmetric Difference:
{:?}
"
,
a.symmetric_difference(&
b).collect::<Vec<&
i32>>());
}
Les exemples originaux proviennent de la documentation.