IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Rust par l'exemple


précédentsommairesuivant

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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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![1i32, 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>.

 
Sélectionnez
1.
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 :

  1. None, pour signaler une erreur ou l'absence d'une valeur, et
  2. Some(value), un tuple qui enveloppe, contient une valeur de type T.
 
Sélectionnez
1.
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(0f32);

    // "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 :

  1. 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);
  2. 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).
 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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(0i32);

    // 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.

 
Sélectionnez
1.
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é).

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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) :

  1. union : Récupère tous les éléments dans les deux ensembles ;
  2. difference : Récupère tous les éléments qui sont dans le premier ensemble mais pas dans le second ;
  3. intersection : Récupère uniquement les éléments présents dans les deux ensembles ;
  4. 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.

 
Sélectionnez
1.
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!(1i32, 2, 3).into_iter().collect();
    let mut b: HashSet<i32> = vec!(2i32, 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.


précédentsommairesuivant

Licence Creative Commons
Le contenu de cet article est rédigé par Rust Core Team et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.