Thanks!
X
Paypal
Github sponsorship
Patreon

Rust

Fork me on GitHub

II. Spécificités de Rust

8. Les unions

Les unions ressemblent beaucoup aux structures tout en étant très différentes : tous les champs d'une union partagent le même espace mémoire. Si la valeur d'un champ d'une union est changé, cela peut écrire par-dessus un autre champ. Autre information importante : la taille d'une union est la taille de son champ avec la plus grande taille.

Bien évidemment, vous vous doutez bien qu'avec toutes ces restrictions, les types des champs d'une union doivent suivre certaines règles : ils doivent implémenter le trait Copy ou bien être wrappés dans le type ManuallyDrop.

Chaque accès à un champ d'une union est considéré comme unsafe et vous ne pourrez pas faire des emprunts mutable sur plus d'un champ à la fois car ils sont considérés comme faisant tous parties du même espace mémoire.

La plupart des derive traits ne peuvent pas être utilisés non plus (par exemple #[derive(Debug)]. Cela ne veut pas dire qu'une union ne peut pas implémenter ces traits, juste qu'il vous faudra les implémenter vous-même.

Enfin, dernier point : quand on instancie une union, on ne doit spécifier qu'un seul champ.

Mise en pratique

Prenons un exemple :

Rununion Foo {
    a: u16,
    b: u8,
}

let f = Foo { a: 1 };
unsafe { // Nécessaire pour pouvoir accéder aux champs.
    println!("a: {} b: {}", f.a, f.b);
}

Ce qui affichera :

a: 1 b: 1

Et oui, souvenez-vous : les champs partagent le même espace mémoire. Par-contre que se passe-t-il pour le champ b si on assigne au champ a une valeur plus grande que ce que peut contenir un u8 ?

Runlet f = Foo { a: u16::MAX };
unsafe {
    println!("a: {} b: {}", f.a, f.b);
}

Ce qui affichera :

a: 65535 b: 255

Donc b représente la partie "basse" de a. Ce qui illustre parfaitement l'espace mémoire partagé.

Que se passe-t-il si on change l'ordre des types et que l'on commence par le u8 à la place du u16 ?

Rununion Foo {
    a: u8,
    b: u16,
}

// Ce sera maintenant le champ `b` qu'on va initialiser.
let f = Foo { b: u16::MAX };
unsafe {
    println!("a: {} b: {}", f.a, f.b);
}

Ce qui affichera :

a: 255 b: 65535

Donc rien n'a changé, le u8 représente toujours la partie "basse" du u16. Et que se passe-t-il si on ajoute un autre champ de type u8 ?

Rununion Foo {
    a: u16,
    b: u8,
    c: u8,
}

let f = Foo { a: 10 };
unsafe {
    println!("a: {} b: {} c: {}", f.a, f.b, f.c);
}

Ce qui affichera :

a: 10 b: 10 c: 10

Donc un type plus petit représentera toujours la partie basse d'un type plus grand, même s'il y en a plusieurs.

Pattern macthing

Maintenant regardons rapidement comment le pattern matching fonctionne avec une union. Tout comme lorsque l'on initialise une union, il ne faut spécifier qu'un seul champ. Et bien évidemment, un block unsafe est nécessaire pour pouvoir accéder au champ. Exemple :

Runlet f = Foo { a: 10 };
unsafe {
    match f {
        Foo { a: 10 } => println!("ok"),
        _ => println!("not ok"),
    }
}

Voilà qui conclut ce chapitre sur les unions.