Tutoriel pour apprendre les bases de la programmation en Rust

Image non disponible


précédentsommairesuivant

II. Spécificités de Rust

II-A. Le formatage des flux

Nous allons commencer cette deuxième partie par un chapitre relativement simple : le formatage des flux.

II-A-1. Exemple de print! et println!

Pour le moment, nous nous sommes contentés de faire de l'affichage sans y mettre de forme. Sachez toutefois qu'il est possible de modifier l'ordre dans lequel sont affichés les arguments sans pour autant changer l'ordre dans lesquels vous les passez à la macro. Démonstration :

 
Sélectionnez
println!("{} {} {}", "Bonjour", "à", "tous !");
println!("{1} {0} {2}", "à", "Bonjour", "tous !");

Le code que je vous ai montré n'a pas un grand intérêt, mais il sert à démontrer que c'est possible. Cependant, on peut faire des choses nettement plus intéressantes comme limiter le nombre de chiffres après la virgule.

 
Sélectionnez
let nombre_decimal : f64 = 0.56545874854551248754;

println!("{:.3}", nombre_decimal);

Pas mal, hein ? Hé bien sachez qu'il y a un grand nombre d'autres possibilités comme :

 
Sélectionnez
let nombre = 6i32;
let nombre2 = 16i32;

println!("{:b}", nombre); // affiche le nombre en binaire
println!("{:o}", nombre); // affiche le nombre en octal (base 8)
println!("{:x}", nombre); // affiche le nombre en "petit" hexadecimal (base 16)
println!("{:X}", nombre); // affiche le nombre en "grand" hexadecimal (base 16)
println!("{:08d}", nombre);  // affiche "00000006"
println!("{:08d}", nombre2); // affiche "00000016"

Vous pouvez aussi faire en sorte que l'affichage s'aligne sur une colonne et plein d'autres choses encore. Comme vous vous en rendrez compte par vous-même, il y a beaucoup de possibilités. Vous pourrez trouver tout ce que vous voulez à ce sujet ici (la doc officielle !).

II-A-2. format!

Comme vous vous en doutez, c'est aussi une macro. Elle fonctionne de la même façon que print! et println!, mais au lieu d'écrire sur la sortie standard (votre console la majorité du temps), elle renvoie une String. Plus d'infos ici (oui, encore la doc !).

 
Sélectionnez
let entier = 6i32;
let s_entier = format!("{}", entier);

Une façon simple et efficace de convertir un nombre en String !

II-A-3. Toujours plus loin !

Sachez que vous pouvez vous servir du formatage de la même façon pour écrire dans des fichiers ou tout autre type implémentant le trait Write (et il y en a pas mal !). Vous pouvez même faire ceci si vous le voulez :

 
Sélectionnez
use std::io::Write; // on importe le trait Write...

let mut w = Vec::new();
write!(&mut w, "test"); // ... et on l'utilise sur notre Vec !

Eh oui, encore une autre macro ! Ne vous en faites pas, c'est la dernière (pour l'instant… sourire machiavélique) ! C'était juste pour vous montrer à quel point le formatage des flux pouvait aller loin.

Je présume que vous vous dites aussi : « c'est quoi cette histoire de trait ?! ». Avant d'aborder cette partie, il faut que je vous parle des structures en Rust.

II-B. Les structures

Comme certains d'entre vous vont s'en rendre compte, elles sont à la fois très ressemblantes et très différentes de ce que vous pourriez croiser dans d'autres langages. Ce chapitre est assez lourd, donc n'hésitez surtout pas à prendre votre temps pour être bien sûr de tout comprendre. Commençons donc de ce pas !

II-B-1. À quoi ça ressemble ?

Sachez qu'il existe quatre types de structures en Rust :

  • les tuples ;
  • les structures unitaires (on dit aussi structure opaque) ;
  • les structures « classiques » (comme en C) ;
  • les structures tuples (un mélange entre les tuples et les structures « classiques »).

Exemple de déclaration pour chacune d'entre elles :

 
Sélectionnez
// Un tuple
struct Tuple(isize, usize, bool);

// Une structure "classique"
struct Classique {
    name: String,
    age: usize,
    a_un_chat: bool
}

// Une structure unitaire
struct Unitaire;

// Une structure tuple
struct StructureTuple(usize);

Maintenant, voyons comment on les instancie :

 
Sélectionnez
let t = Tuple(0, 2, false); // Le tuple

let c = Classique {
            name: "Moi".to_owned(), // on convertit une &'static str en String
            age: 18,
            a_un_chat: false
        }; // La structure "classique"

let st = StructureTuple(1); // La structure tuple

let u = Unitaire; // La structure unitaire

Vous devez savoir que, par convention, les noms des structures doivent être écrits en camel case en Rust. Par exemple, appeler une structure « ma_structure » serait « invalide ». Il faudrait l'appeler « MaStructure ». J'insiste bien sur le fait que ce n'est pas obligatoire, ce n'est qu'une convention. Cependant, il est bien de les suivre lorsqu'on le peut, ça facilite la lecture pour les autres développeurs. D'ailleurs, il est important d'ajouter :

Les noms des fonctions, par convention en Rust, doivent être écrits en snake case. Donc « MaFonction » est invalide, « ma_fonction » est correct.

Avec les exemples que je vous ai donnés au-dessus, je pense que certains d'entre vous se demandent à quoi peut bien servir la « structure tuple » ? Hé bien pas à grand-chose dans la majorité des cas, mais il y en a un où elle est très utile :

 
Sélectionnez
struct Distance(isize);

let distance = Distance(23);

let Distance(longueur) = distance;
println!("La distance est {}", longueur);

Elle permet de « masquer » un type, ce qui peut se révéler pratique dans certains cas.

« D'accord. Et la structure unitaire ? »

Celle-là par contre, vous risquez de ne pas vous en servir avant longtemps voire peut-être même jamais. Elle est utilisée en général quand on ne sait pas ce qu'elle contient.

Maintenant je présume que vous vous demandez : « Comment peut-on utiliser une structure sans savoir ce qu'elle contient ? o_O ». Quand on porte une bibliothèque depuis un autre langage par exemple. Une fonction peut vous retourner un type dont vous n'avez pas accès aux champs, mais qui est utilisé partout (les structures présentes dans la bibliothèque GTK+ en sont un très bon exemple).

II-B-2. Déstructuration

Il est possible de déstructurer une structure en utilisant le pattern matching :

 
Sélectionnez
struct Point {
    x: i32,
    y: i32,
}

let origin = Point { x: 0, y: 0 };

match origin {
    Point { x: x, y: y } => println!("({},{})", x, y),
}

Il est d'ailleurs possible de ne matcher que certains champs en utilisant « .. » :

 
Sélectionnez
struct Point {
    x: i32,
    y: i32,
}

let origin = Point { x: 0, y: 0 };

match origin {
    Point { y: y, .. } => println!("(..,{})", y),
}

Ici, il ne sera pas possible d'afficher le contenu de la variable « x », car nous l'avons volontairement ignoré lors du matching.

Maintenant que les explications sont faites, voyons voir comment ajouter des méthodes à une structure.

II-B-3. Les méthodes

Outre le fait qu'ajouter des méthodes à une structure permet de faire de l'orienté-objet, cela peut aussi permettre de forcer un développeur à appeler l'un de vos constructeurs plutôt que de le laisser initialiser tous les éléments de votre structure lui-même. Exemple :

 
Sélectionnez
pub struct Distance {
    // ce champ n'est pas public donc les utilisateurs ne pourront pas y avoir accès !
    metre: isize
}

impl Distance {
    pub fn new() -> Distance {
        Distance {
            metre: 0
        }
    }

    pub fn new_with_value(valeur: isize) -> Distance {
        Distance {
            metre: valeur
        }
    }
}

// autre fichier
use fichier::Distance;

let d = Distance::new();
// ou
let d = Distance::new_with_value(10);

Quel intérêt vous dites-vous ? Après tout, on irait aussi vite de le faire nous-mêmes ! Dans le cas présent, il n'y en a pas beaucoup, c'est vrai. Cependant, imaginez une structure contenant une vingtaine de champs, voire plus. C'est tout de suite plus agréable d'avoir une méthode nous permettant de le faire en une ligne. Maintenant, ajoutons une méthode pour convertir cette distance en kilomètre :

 
Sélectionnez
pub struct Distance {
    metre: isize
}

impl Distance {
    pub fn new() -> Distance {
        Distance {
            metre: 0
        }
    }

    pub fn new_with_value(valeur: isize) -> Distance {
        Distance {
            metre: valeur
        }
    }

    pub fn convert_in_kilometers(&self) -> isize {
        self.metre / 1000
    }
}

// autre fichier
use fichier::Distance;

let d = Distance::new();
// ou
let d = Distance::new_with_value(10);

println!("distance en kilometres : {}", d.convert_in_kilometers());

Une chose importante à noter est qu'une fonction membre ne prenant pas self en premier paramètre est une méthode statique. Les méthodes new et new_with_value sont donc des méthodes statiques tandis que convert_in_kilometers n'en est pas une.

À présent, venons-en au '&' devant le self : c'est la durée de vie de l'objet. Nous aborderons cela dans un autre chapitre.

Maintenant, si vous voulez créer une méthode pour modifier la distance, il vous faudra spécifier que self est mutable (car toutes les variables en Rust sont constantes par défaut). Exemple :

 
Sélectionnez
impl Distance {
    // les autres méthodes
    // ...

    fn set_distance(&mut self, nouvelle_distance: isize) {
        self.metre = nouvelle_distance;
    }
}

Tout simplement !

II-B-4. Syntaxe de mise à jour (ou « update syntax »)

Une structure peut inclure « .. » pour indiquer qu'elle veut copier certains champs d'une autre structure. Exemple :

 
Sélectionnez
struct Point3d {
    x: i32,
    y: i32,
    z: i32,
}

let mut point = Point3d { x: 0, y: 0, z: 0 };
let mut point2 = Point3d { y: 1, .. point }; // et ici on prend x et z de point

II-B-5. Destructeur

Maintenant, voyons comment faire un destructeur (une méthode appelée automatiquement lorsque notre objet est détruit) :

 
Sélectionnez
struct Distance {
    metre: isize
}

impl Distance {
    // fonctions membres
}

impl Drop for Distance {
    fn drop(&self) {
        println!("La structure Distance a été détruite !");
    }
}

« D'où ça sort ce impl Drop for Distance ?! »

On a implémenté le trait Drop à notre structure Distance. Quand l'objet est détruit, cette méthode est appelée. Je sais que cela ne vous dit pas ce qu'est un trait. Pour plus d'explications, il va vous falloir lire le chapitre suivant !

II-C. Les traits

Commençons par donner une rapide définition : un trait est un ensemble de méthodes que l'objet sur lequel il est appliqué doit implémenter.

Dans le chapitre précédent, il nous fallait implémenter la méthode drop pour pouvoir implémenter le trait Drop. Et au cas où vous ne vous en doutiez pas, sachez que les traits sont utilisés partout en Rust. On en retrouve même sur de simples types comme les isize ou les f64 !

On va prendre un exemple tout simple : additionner deux f64. La doc nous dit ici (regardez à « traits implementation ») que le trait Add a été implémenté sur le type f64. Ce qui nous permet de faire :

 
Sélectionnez
let valeur = 1f64;

println!("{}", valeur + 3f64);

Add était un trait implémenté « par défaut ». Si ce n'est pas le cas, vous devez importer un trait pour utiliser les fonctions qui y sont associées. Exemple :

 
Sélectionnez
use std::str::FromStr;

println!("{}", f64::from_str("3.6").unwrap());

Facile n'est-ce pas ? Les traits fournis par la bibliothèque standard sur les types standards apportent beaucoup de fonctionnalités. Si jamais vous avez besoin de quelque chose, il y a de fortes chances que ça existe déjà. À vous de chercher.

Je vous ai montré comment importer et utiliser un trait, maintenant il est temps de voir comment en créer un !

II-C-1. Créer un trait

C'est relativement similaire à la création d'une structure :

 
Sélectionnez
trait Animal {
    fn get_espece(&self) -> String;
}

Facile, n'est-ce pas ? Maintenant un petit exemple :

 
Sélectionnez
trait Animal {
    fn get_espece(&self) -> String;
    fn get_nom(&self) -> &str;
}

struct Chien {
    nom: String
}

impl Animal for Chien {
    fn get_espece(&self) -> String {
        String::from("Chien")
    }

    fn get_nom(&self) -> &str {
        &self.nom
    }
}

struct Chat {
    nom: String
}

impl Animal for Chat {
    fn get_espece(&self) -> String {
        String::from("Chat")
    }

    fn get_nom(&self) -> &str {
        &self.nom
    }
}

let chat = Chat { nom: String::from("Fifi") };
let chien = Chien { nom: String::from("Loulou") };

println!("{} est un {}", chat.get_nom(), chat.get_espece());
println!("{} est un {}", chien.get_nom(), chien.get_espece());

Je tiens à vous rappeler qu'il est tout à fait possible d'implémenter un trait disponible dans la bibliothèque standard comme je l'ai fait avec le trait Drop.
Il est aussi possible d'écrire le code de la méthode directement dans le trait. Ça permet d'éviter d'avoir à réécrire la méthode pour chaque objet sur lequel le trait est implémenté. Exemple :

 
Sélectionnez
trait Animal {
    fn get_espece(&self) -> String {
        String::from("Inconnue")
    }

    fn presentation(&self) -> String {
        format!("Je suis un {} !", self.get_espece())
    }
}

impl Animal for Chat {
    pub fn get_espece(&self) -> String {
        String::from("Chat")
    }
}

Ici, je ne redéfinis que la méthode get_espece, car presentation fait bien ce que je voulais.

Vous n'en voyez peut-être pas encore l'intérêt, mais sachez cependant que c'est vraiment très utile. Quoi de mieux qu'un autre exemple pour vous le prouver ? :D

 
Sélectionnez
fn afficher_infos<T: Animal>(animal: &T) {
    println!("{} est un {}", animal.get_nom(), animal.get_espece());
}

« C'est quoi ce <T: Animal> ?! »
Pour ceux qui ont fait du C++ ou du Java, c'est relativement proche des templates. Pour les autres, sachez juste que les templates ont été inventés pour permettre de faire du polymorphisme. Un exemple (encore un !) :

 
Sélectionnez
fn affiche_chat(chat: &Chat) {
    println!("{} est un {}", chat.get_nom(), chat.get_espece());
}

fn affiche_chien(chien: &Chien) {
    println!("{} est un {}", chien.get_nom(), chien.get_espece());
}

Dans le cas présent, ça va, cela ne représente que deux fonctions. Maintenant si on veut ajouter 40 autres espèces d'animaux, on devrait écrire une fonction pour chacune ! Pas très pratique… Utiliser la généricité est donc la meilleure solution. Et c'est ce dont il sera question dans le prochain chapitre !

II-C-2. Aller plus loin

J'en profite maintenant pour vous montrer quelques utilisations de traits comme Range (que l'on avait déjà rapidement abordé dans le chapitre des boucles). Ce dernier peut vous permettre de faire :

 
Sélectionnez
let s = "hello";

println!("{}", s);
println!("{}", &s[0..2]);
println!("{}", &s[..3]);
println!("{}", &s[3..]);

Ce qui donnera :

 
Sélectionnez
hello
he
hel
lo

Cela fonctionne aussi sur les slices :

 
Sélectionnez
let v : &[u8] = &[0; 10]; // on crée un slice contenant 10 '\0'

println!("{:?}", &v[0..2]);
println!("{:?}", &v[..3]);
println!("{:?}", &v[3..]);

Ce qui donne :

 
Sélectionnez
[0, 0]
[0, 0, 0]
[0, 0, 0, 0, 0, 0, 0]

Voilà qui devrait vous donner un petit aperçu de tout ce qu'il est possible de faire avec les traits. Il est maintenant temps de parler de la généricité.

II-D. Généricité

Reprenons donc notre précédent exemple :

 
Sélectionnez
fn affiche_chat(chat: &Chat) -> String {
    println!("{} est un {}", chat.get_nom(), chat.get_espece());
}

fn affiche_chien(chien: &Chien) -> String {
    println!("{} est un {}", chien.get_nom(), chien.get_espece());
}

Comme je vous le disais, avec deux espèces d'animaux, ça ne représente que deux fonctions, mais ça deviendra très vite long à écrire si on veut en rajouter 40. C'est donc ici qu'intervient la généricité.

II-D-1. La généricité

Commençons par la base en donnant une description de ce que c'est : « c'est une fonctionnalité qui autorise le polymorphisme paramétrique (ou juste polymorphisme pour aller plus vite) ». Pour faire simple, ça permet de manipuler des objets différents du moment qu'ils implémentent le trait demandé.

Par exemple, on pourrait manipuler un chien robot, il implémenterait le trait Machine et le trait Animal :

 
Sélectionnez
trait Machine {
    fn get_nombre_de_vis(&self) -> usize;
    fn get_numero_de_serie(&self) -> &str;
}

trait Animal {
    fn get_nom(&self) -> &str;
    fn get_nombre_de_pattes(&self) -> usize;
}

struct ChienRobot {
    nom: String,
    nombre_de_pattes: usize,
    numero_de_serie: String
}

impl Animal for ChienRobot {
    fn get_nom(&self) -> &str {
        &self.nom
    }

    fn get_nombre_de_pattes(&self) -> usize {
        self.nombre_de_pattes
    }
}

impl Machine for ChienRobot {
    fn get_nombre_de_vis(&self) -> usize {
        40123
    }

    fn get_numero_de_serie(&self) -> &str {
        &self.numero_de_serie
    }
}

Ainsi, il nous est désormais possible de faire :

 
Sélectionnez
fn presentation_animal<T: Animal>(animal: T) {
    println!("Il s'appelle {} et il a {} patte()s !", animal.get_nom(),
        animal.get_nombre_de_pattes());
}

let super_chien = ChienRobot {
                    nom: "Super chien".to_owned(),
                    nombre_de_pattes: 4,
                    numero_de_serie: "super chien DZ442".to_owned()
                };

presentation_animal(super_chien);

Mais comme c'est aussi un robot, on peut aussi faire :

 
Sélectionnez
fn description_machine<T: Robot>(machine: T) {
    println!("Le modèle {} a {} vis", machine.get_numero_serie(),
        machine.get_nombre_de_vis());
}

C'est pas trop génial ? :waw:

Revenons-en maintenant à notre problème initial : « comment faire avec 40 espèces d'animaux différentes ? » Je pense que vous commencez à voir où je veux en venir, je présume ? Non ? Très bien, dans ce cas prenons un exemple :

 
Sélectionnez
trait Animal {
    fn get_nom(&self) -> &str {
        &self.nom
    }

    fn get_nombre_de_pattes(&self) -> usize {
        self.nombre_de_pattes
    }
}

struct Chien {
    nom: String,
    nombre_de_pattes: usize
}

struct Chat {
    nom: String,
    nombre_de_pattes: usize
}

struct Oiseau {
    nom: String,
    nombre_de_pattes: usize
}

struct Araignee {
    nom: String,
    nombre_de_pattes: usize
}

impl Animal for Chien {}
impl Animal for Chat {}
impl Animal for Oiseau {}
impl Animal for Araignee {}

fn affiche_animal<T: Animal>(animal: T) {
    println!("Cet animal s'appelle {} et il a {} patte(s)", animal.get_nom(),
        animal.get_nombre_de_pattes());
}

let chat = Chat { nom : "Félix".to_owned(), nombre_de_pattes: 4};
let spider = Araignee { nom : "Yuuuurk".to_owned(), nombre_de_pattes: 8};

affiche_animal(chat);
affiche_animal(spider);

Pas mal hein ? Et pourtant… Ce code ne compile pas !

Pourquoi ?!

Tout simplement parce que les traits ne peuvent prendre en compte, dans les méthodes par défaut, le travail sur des valeurs contenues dans l'objet pris en paramètre (référencé dans cet exemple par self).

Imaginez une baleine et un chien. Tous deux sont des animaux, pas vrai ? Pourtant, ils ont très peu de points communs…
Ainsi, chacun aura des caractéristiques que l'autre pourrait ne pas avoir (la couleur des poils par exemple). Programmer une fonction par défaut sur un attribut qu'un objet implémentant le trait ne possède pas pourrait poser problème !

Voici un code fonctionnant pour ce cas :

 
Sélectionnez
struct Chien {
    nom: String,
    nombre_de_pattes: usize
}

struct Chat {
    nom: String,
    nombre_de_pattes: usize
}

trait Animal {
    fn get_nom(&self) -> &str;
    fn get_nombre_de_pattes(&self) -> usize;
    fn affiche(&self) {
        println!("Je suis un animal qui s'appelle {} et j'ai {} pattes !", self.get_nom(), self.get_nombre_de_pattes());
    }
}

// On implémente les méthodes prévues dans le trait Animal, sauf celui par défaut (sinon surcharge)
impl Animal for Chien {
    fn get_nom(&self) -> &str {
        &self.nom
    }

    fn get_nombre_de_pattes(&self) -> usize {
        self.nombre_de_pattes
    }
}

// On fait de même, mais l'on a quand même envie de surcharger la méthode par défaut...
impl Animal for Chat {
    fn get_nom(&self) -> &str {
        &self.nom
    }

    fn get_nombre_de_pattes(&self) -> usize {
        self.nombre_de_pattes
    }

    // On peut même 'surcharger' une méthode par défaut dans le trait - il suffit de la réimplémenter
    fn affiche(&self) {
        println!("Je suis un animal - un chat même qui s'appelle {} !", self.get_nom());
    }
}

fn main() {
    fn affiche_animal<T: Animal>(animal: T) {
        animal.affiche();
    }

    let chat = Chat { nom : "Félix".to_owned(), nombre_de_pattes: 4};
    let chien = Chien { nom : "Rufus".to_owned(), nombre_de_pattes: 4};

    affiche_animal(chat);
    affiche_animal(chien);
}

La seule contrainte étant que, même si l'implémentation des méthodes est la même, il faudra, à chaque structure héritant d'un trait, la réimplémenter… Cela dit, les macros pourraient grandement faciliter cette étape laborieuse, mais nous verrons cela plus tard.

II-E. Propriété

Jusqu'à présent, de temps à autre, on utilisait le caractère '&' devant des paramètres de fonctions sans que je vous explique à quoi ça servait. Exemple :

 
Sélectionnez
fn ajouter_valeur(v: &mut Vec<i32>, valeur: i32) {
    v.push(valeur);
}

struct x {
    v: i32
}

impl x {
    fn addition(&self, a: i32) -> i32 {
        self.v + a
    }
}

Il s'agit de variables passées par référence. En Rust, cela a une grande importance. Il faut savoir que chaque variable ne peut avoir qu'un seul « propriétaire » à la fois, ce qui est l'une des grandes forces de ce langage. Par exemple :

 
Sélectionnez
fn une_fonction(v: Vec<i32>) {
    // le contenu n'a pas d'importance
}

let v = vec![5, 12];

une_fonction(v);
println!("{}", v[0]); // error ! "use of moved value"

Un autre exemple encore plus simple :

 
Sélectionnez
let original = vec![1, 2, 3];
let non_original = original;

println!("original[0] is: {}", original[0]); // même erreur

« Mais c'est complètement idiot ! Comment fait-on pour modifier la variable depuis plusieurs endroits ?! »
C'est justement pour éviter ça que ce système d'ownership (propriété donc) existe. C'est ce qui vous posera sans aucun doute le plus de problèmes quand vous développerez vos premiers programmes.

Dans un chapitre précédent, je vous ai parlé des traits. Hé bien sachez que l'un d'entre eux s'appelle Copy et permet de copier (sans rire !) un type sans en devenir le propriétaire. Tous les types de « base » (i8, i16, i32, isize, f32, etc.) l'implémentent. Ce code est donc tout à fait valide :

 
Sélectionnez
let original : i32 = 8;
let copy = original;

println!("{}", original);

Il est cependant possible de « contourner » ce problème de copie de la manière suivante :

 
Sélectionnez
fn fonction(v: Vec<i32>) -> Vec<i32> {
    v // on "rend" la propriété en renvoyant l'objet
}

fn main() {
    let v = vec![5, 12];

    let v = fonction(v); // et on la re-récupère ici
    println!("{}", v[0]);
}

Bof, n'est-ce pas ? Et encore, c'est un code simple. Imaginez quelque chose comme ça :

 
Sélectionnez
fn fonction(v1: Vec<i32>, v2: Vec<i32>, v3: Vec<i32>, v4: Vec<i32>) -> (Vec<i32>, Vec<i32>, Vec<i32>, Vec<i32>) {
    (v1, v2, v3, v4)
}

let v1 = vec![5, 12, 3];
let v2 = vec![5, 12, 3];
let v3 = vec![5, 12, 3];
let v4 = vec![5, 12, 3];

let (v1, v2, v3, v4) = fonction(v1, v2, v3, v4);

Ça devient difficile de suivre, hein ? Vous l'aurez donc compris, ce n'est pas du tout une bonne idée.

« Mais alors comment fait-on ? On implémente le trait Copy sur tous les types ? »
Non, et heureusement ! La copie de certains types pourrait avoir un lourd impact sur les performances de votre programme, tandis que d'autres ne peuvent tout simplement pas être copiés ! C'est ici que les références rentrent en jeu.

Jusqu'à présent, vous vous en êtes servies sans que je vous explique à quoi elles servaient. Je pense que maintenant vous vous en doutez. Ajoutons une référence à notre premier exemple :

 
Sélectionnez
fn une_fonction(v: &Vec<i32>) {
    // le contenu n'a pas d'importance
}

let v = vec![5, 12];

une_fonction(&v);
println!("{}", v[0]); // Pas de souci !

On peut donc dire que les références permettent d'emprunter une variable sans en prendre la propriété, et c'est très important de s'en souvenir !

Tout comme les variables, les références aussi peuvent être mutables. « & » signifie référence constante et « &mut » signifie référence mutable. Il y a cependant plusieurs choses à savoir :

  • Une référence ne doit pas « vivre » plus longtemps que la variable qu'elle référence.
  • On peut avoir autant de référence constante que l'on veut sur une variable.
  • On ne peut avoir qu'une seule référence mutable sur une variable.
  • On ne peut avoir une référence mutable que sur une variable mutable.
  • On ne peut avoir une référence constante et une référence mutable en même temps sur une variable.

Pour bien comprendre cela, il faut bien avoir en tête comment la durée de vie d'une variable fonctionne :

 
Sélectionnez
fn f() {
    let mut v = 10i32; // on crée une variable

    v += 12; // on fait des opérations dessus
    v *= 2;
    // ...

    // quand on sort de la fonction, v n'existe plus
}

fn main() {
    let v : i32 = 12; // cette variable n'a rien à voir avec celle dans la fonction f
    let v2 : f32 = 0;

    f();
    // on quitte la fonction, v et v2 n'existent plus    
}

Ainsi, ce code devient invalide :

 
Sélectionnez
fn main() {
    let reference : &i32;
    let x = 5;
    reference = &x;

    println!("{}", reference);
}

Ici, le compilateur vous dira que la variable x ne vit pas assez longtemps. x ayant été déclarée après reference, elle est donc détruite en premier, rendant reference invalide ! Pour pallier à ce problème, rien de bien compliqué :

 
Sélectionnez
fn main() {
    let x = 5;
    let reference : &i32 = &x;

    println!("{}", reference);
}

Maintenant, vous savez ce qui se cache derrière les références et vous avez des notions concernant la durée de vie des variables. Il est temps de voir ce deuxième point un peu plus en détail.

II-F. Durée de vie

Il existe plusieurs types de durée de vie (ou lifetime). Jusqu'à présent, nous n'avons vu que les plus basiques, mais sachez qu'il en existe encore deux autres :

  • les durées de vie statiques ;
  • les durées de vie associées.

Les durées de vie statiques permettent aux références de référencer des variables statiques ou du contenu « constant » :

 
Sélectionnez
// avec une variable statique
static VAR: i32 = 0;
let variable_static: &'static i32 = &VAR;

// avec du contenu constant
let variable_const: &'static str = "Ceci est une str constante !";

Rien de bien difficile ici, l'autre est un peu plus complexe, mais aussi moins visible. Imaginons que vous écriviez une classe dont l'une des variables membres devait être modifiée à l'extérieur de la structure. Vous vous contentez de renvoyer une « &mut self.ma_variable », je me trompe ? Bien que ce code fonctionne, il est important de comprendre ce qu'il se passe :

 
Sélectionnez
struct MaStruct {
    variable: String
}

impl MaStruct {
    fn get_variable(&mut self) -> &mut String {
        &mut self.variable
    }
}

fn main() {
    let mut v = MaStruct { variable : String::new() };

    v.get_variable().push_str("hoho !");
    println!("{}", v.get_variable());
}

La méthode get_variable va en fait renvoyer une référence temporaire sur self.variable. Si on voulait écrire ce code de manière « complète », on l'écrirait comme ceci :

 
Sélectionnez
impl MaStruct {
    fn get_variable<'a>(&'a mut self) -> &'a mut String {
        &mut self.variable
    }
}

'a représente la durée de vie (cela aurait tout aussi bien pu être 'x ou 'z, peu importe). Ici, on retourne donc une référence avec une durée de vie 'a sur une variable.

Je tenais à vous parler de ce dernier point pour que vous compreniez bien comment tout cela fonctionne, le premier code sans l'ajout des paramètres de durée de vie était tout à fait fonctionnel.

II-G. Déréférencement

Après les gros chapitres précédents, celui-là ne devrait pas vous prendre beaucoup de temps. Il vous arrivera de croiser ce genre de code :

 
Sélectionnez
fn une_fonction(x: &mut i32) {
    *x = 2; // on déréférence
}

fn main() {
    let mut x = 0;

    println!("avant : {}", x);
    une_fonction(&mut x);
    println!("après : {}", x);
}

La valeur a donc été modifiée dans la fonction une_fonction. Pour ceux ayant fait du C/C++, c'est exactement la même chose que le déréférencement d'un pointeur. La seule différence est que cela passe par le trait Deref en Rust. Dans l'exemple précédent, le trait est implémenté sur le type &mut i32. Cependant, il est aussi possible de faire :

 
Sélectionnez
let x = String::new();
let deref_x = *x; // ce qui renvoie une erreur, car le type str n'implémente pas le trait Sized

Il est donc possible de déréférencer un objet en implémentant ce trait.

II-G-1. Implémentation

On va prendre un exemple pour que vous compreniez le tout plus facilement :

 
Sélectionnez
use std::ops::Deref; // on importe le trait

struct UneStruct {
    value: u32
}

impl Deref for UneStruct {
    type Target = u32; // pour préciser quel type on retourne !

    fn deref(&self) -> &u32 {
        &self.value
    }
}

fn main() {
    let x = UneStruct { value: 0 };
    assert_eq!(0u32, *x); // on peut maintenant déréférencer x
}

II-G-2. Autodéréférencement

Vous utilisez aussi ce trait sans le savoir quand vous faites :

 
Sélectionnez
fn affiche_la_str(s: &str) { // on obtient une &str
    println!("affichage : {}", s);
}

let x = "toto".to_owned(); // on a donc une String("toto")
affiche_la_str(&x); // on passe une &String

Normalement, vous devriez vous demander : « Pourquoi si on passe &String on obtient &str ?! ». Hé bien sachez que Rust implémente un système d'autodéréférencement. Ça permet d'écrire des codes de ce genre :

 
Sélectionnez
struct UneStruct;

impl UneStruct {
    fn foo(&self) { println!("UneStruct"); }
}

let f = UneStruct;

f.foo();
(&f).foo();
(&&f).foo();
(&&&&&&&&f).foo();

Le compilateur va déréférencer jusqu'à obtenir le type voulu (en l'occurrence, celui qui implémente la méthode foo dans le cas présent, donc UneStruct). Je pense que certains d'entre vous ont compris où je voulais en venir concernant String.

Le compilateur voit qu'on envoie &String dans une méthode qui reçoit &str comme paramètre. Il va donc déréférencer String pour obtenir str. Nous obtenons donc &str. On peut imager ce que fait le compilateur de cette façon : &(*(String.deref())). Pour ceux que ça intéresse, voici comment fait le compilateur, étape par étape :

 
Sélectionnez
&(*(&str))
&(str)
&str
  • &String -> pas &str, on déréférence String
  • &(*String) -> Le type String implémente le trait Deref, on l'appelle
  • &(*(String.deref()))
  • &(*(&str))
  • &(str)
  • &str

Et voilà, le compilateur a bien le type attendu !

II-H. Sized et String vs str

Je pense que vous vous êtes déjà posé la question : « Quelle est la différence entre String et str ? ». Ou encore : « Pourquoi deux types pour représenter la même chose ? ». Tâchons d'y répondre !

II-H-1. str

Le type str représente tout simplement en mémoire une adresse et une taille. C'est pourquoi on ne peut modifier son contenu. Mais ce n'est pas la seule chose à savoir à son sujet. Commençons par regarder le code suivant :

 
Sélectionnez
let x = "str";

x est donc une variable de type &str. Mais que se passe-t-il si nous tentons de déréférencer x pour obtenir un type str ?

 
Sélectionnez
let x = *"str";

Ce qui donnera :

 
Sélectionnez
error: the trait `core::marker::Sized` is not implemented for the type `str` [E0277]

Mais quel est donc ce trait Sized, et pourquoi ça pose un problème que str ne l'implémente pas ?

II-H-2. Le trait Sized

str n'est pas le seul type qui n'implémente pas le trait Sized. Les slices non plus ne l'implémentent pas :

 
Sélectionnez
let x : [i32] = [0, 1, 2];

Ce qui donne :

 
Sélectionnez
error: mismatched types:
expected `[i32]`,
   found `[i32; 3]`
# ...
error: the trait `core::marker::Sized` is not implemented for the type `[i32]` [E0277]

le problème est donc que si le trait Sized n'est pas implémenté sur le type, cela signifie que l'on ne peut pas connaître sa taille au moment de la compilation. Du coup, nous sommes obligés de passer par d'autres types pour les manipuler. Dans le cas des str et des slices, on peut se contenter d'utiliser des références :

 
Sélectionnez
let x : &[i32] = &[0, 1, 2];
let x = "str";

Maintenant, revenons-en aux Strings et aux str.

II-H-3. String

Les Strings permettent donc de manipuler des chaînes de caractères. En plus de ce que contient str (à savoir : une adresse mémoire et une longueur), elles contiennent aussi une capacité qui représente la quantité de mémoire réservée (mais pas nécessairement utilisée).

Pour résumer un peu le tout, String est une structure permettant de modifier le contenu d'une « vue » constante représentée par le type str. C'est d'ailleurs pour ça qu'il est très simple de passer de l'un à l'autre :

 
Sélectionnez
let x : &str = "a";
let y : String = x.to_owned(); // on aurait aussi pu utiliser String::from
let z : &str = &y;

II-H-4. Vec vs slice

C'est plus ou moins le même fonctionnement : le type Vec permet de modifier le contenu d'une vue (non constante) représentée par les slices. Exemple :

 
Sélectionnez
let x : &[i32] = &[0, 1, 2];
let y : Vec<i32> = x.to_vec();
let z : &[i32] = &y;

Ce chapitre (et notamment le trait Sized est particulièrement important pour bien comprendre les mécanismes sous-jacents de Rust. N'hésitez pas à le relire plusieurs fois pour être bien sûr d'avoir tout compris avant de passer à la suite !

II-I. Closure

Nous allons maintenant aborder un chapitre très important pour le langage Rust. Ceux ayant déjà utilisé des langages fonctionnels ne verront qu'une révision dans ce chapitre (mais ça ne fait jamais de mal après tout !).

Pour ceux qui n'ont jamais utilisé de closures, c'est une fonction anonyme qui capture son environnement.

« Une fonction “anonyme” ? Elle “capture” son environnement ? »
Ne vous inquiétez pas, vous allez très vite comprendre, prenons un exemple simple :

 
Sélectionnez
let multiplication = |nombre: i32, multiplicateur| nombre * multiplicateur;

println!("{}", multiplication(2, 2));

Pour le moment, vous vous dites sans doute qu'en fait, ce n'est qu'une fonction. Maintenant, ajoutons un élément :

 
Sélectionnez
let nombre = 2i32;
let multiplication = |multiplicateur| nombre * multiplicateur;

println!("{}", multiplication(2));

Là, je pense que vous vous demandez comment il fait pour trouver la variable nombre puisqu'elle n'est pas dans le scope de la « fonction ». Comme je vous l'ai dit, une closure capture son environnement, elle a donc accès à toutes les variables présentes dans le scope de la fonction qui l'appelle.

Mais à quoi ça peut bien servir ? Imaginons que vous avez une super interface graphique et que vous voulez effectuer une action lors d'un événement, disons lorsque le bouton est cliqué. Hé bien cela donnerait quelque chose dans ce genre :

 
Sélectionnez
let mut bouton = Bouton::new();
let mut clicked = false;

bouton.clicked(|titre| {
    clicked = true;
    println!("On a cliqué sur le bouton {} !", titre);
});

Facile, non ? Cela dit, vous pourriez aussi vous servir des closures pour trier un vecteur d'objets ou d'autres choses similaires.

Si jamais vous souhaitez écrire une fonction recevant une closure en paramètre, voici à quoi cela va ressembler :

 
Sélectionnez
fn fonction_avec_closure<F>(closure: F) -> i32
    where F : Fn(i32) -> i32 {
    closure(1)
}

Ici, la closure prend un i32 comme paramètre et renvoie un i32. Vous remarquerez que la syntaxe est proche de celle d'une fonction générique, la seule différence venant du mot-clé where qui permet de définir à quoi doit ressembler la closure.

Nous avons donc vu les bases des closures. C'est sans doute l'une des parties les plus importantes de ce tutoriel, je vous conseille donc de bien vous entraîner dessus jusqu'à être sûr de bien les maîtriser !

Après ça, il est temps d'attaquer un chapitre un peu plus « tranquille ».

II-J. Multifichier

Il est maintenant grand temps de voir comment faire en sorte que votre projet contienne plusieurs fichiers. Vous allez voir, c'est très facile. Imaginons que votre programme soit composé d'un fichier vue.rs et internet.rs. Nous allons considérer le fichier vue.rs comme le fichier « principal » : c'est à partir de lui que nous allons inclure les autres fichiers. Pour ce faire :

 
Sélectionnez
mod internet;

// le code de vue.rs

… Et c'est tout. Il n'y a rien besoin de changer dans la ligne de compilation non plus, rustc se débrouillera pour trouver les bons fichiers tout seul. Si vous voulez utiliser une classe de ce fichier, faites tout simplement :

 
Sélectionnez
internet::LaStruct {}
internet::la_fonction();

Si vous voulez éviter de devoir réécrire internet:: devant chaque struct/fonction de internet.rs, il vous suffit de faire comme ceci :

 
Sélectionnez
use internet::*; // cela veut dire que l'on inclut TOUT ce que contient ce fichier
// ou comme ceci
use internet::{LaStruct, la_fonction};

// très important, le mod internet doit venir après !
mod internet;

Et voilà, c'est à peu près tout ce qu'il y a besoin de savoir… Ou presque ! Si on veut utiliser un élément de vue.rs, on fera comme ceci :

 
Sélectionnez
// vue.rs

pub use self::LaStruct;

// internet.rs

pub use super::LaStruct;

// ou bien

::LaStruct; // :: voulant dire "dans le scope supérieur"

// ou bien encore

super::LaStruct; // super voulant aussi dire dans "le scope supérieur"

Fini ? Presque ! Imaginons maintenant que vous vouliez mettre des fichiers dans des sous-dossiers : dans ce cas-là, il vous faudra créer un fichier mod.rs dans le sous-dossier dans lequel vous devrez utiliser « pub use » sur les éléments que vous voudrez réexporter dans le scope supérieur (et n'oubliez pas d'importer les fichiers avec mod !). Maintenant, disons que vous créez un sous-dossier appelé « tests », voilà comment utiliser les éléments qui y sont :

 
Sélectionnez
// tests/mod.rs

pub use self::test1::Test1; // on réexporte Test1 directement
pub use self::test2::Test2; // idem

mod test1.rs; // pour savoir dans quel fichier on cherche
mod test2.rs; // idem
pub mod test3; // là on aura directement accès à test3

// dossier supérieur
// fichier lib.rs ou mod.rs
use tests::{Test1, Test2, test3}; // et voilà !

Voilà qui clôture ce court chapitre. Celui qui arrive est assez dur (si ce n'est le plus dur), j'espère que vous avez bien profité de la facilité de celui-ci ! Je vous conseille de bien souffler avant, car il s'agit des… macros !

II-K. Les macros

Nous voici enfin aux fameuses macros dont je vous ai déjà parlé plusieurs fois ! Pour rappel, une macro s'appelle de la façon suivante :

 
Sélectionnez
la_macro!();
// ou bien :
la_macro![];

Le point important ici est la présence du « ! » après le nom de la macro.

II-K-1. Fonctionnement

Nous rentrons maintenant dans le vif du sujet : une macro est définie au travers d'une série de règles qui sont des conditions de pattern-matching. C'est toujours bon ? Parfait !

Une déclaration de macro se fait avec le mot-clé macro_rules (suivie de l'habituel « ! »). Exemple :

 
Sélectionnez
macro_rules! dire_bonjour {
    () => {
        println!("Bonjour !");
    }
}

fn main() {
    dire_bonjour!();
}

Et on obtient :

 
Sélectionnez
Bonjour !

Merveilleux ! Bon jusque là, rien de bien difficile. Mais ne vous inquiétez pas, ça arrive !

II-K-2. Les arguments

Bien évidemment, les macros peuvent recevoir des arguments. Petit exemple :

 
Sélectionnez
macro_rules! dire_quelque_chose {
    ($x:expr) => {
        println!("Il dit : '{}'", $x);
    };
}

fn main() {
    dire_quelque_chose!("hoy !");
}

Ce qui affichera :

 
Sélectionnez
Il dit : 'hoy !'

Regardons un peu plus en détail le code. Le ($x:expr) en particulier. Ici, nous avons indiqué que notre macro prenait une expression appelée x en paramètre. Après il nous a suffi de l'afficher. Maintenant, on va ajouter la possibilité de passer un deuxième argument :

 
Sélectionnez
macro_rules! dire_quelque_chose {
    ($x:expr) => {
        println!("Il dit : '{}'", $x);
    };
    ($x:expr,$y:expr) => {
        println!("Il dit '{}' à {}", $x, $y);
    };
}

fn main() {
    dire_quelque_chose!("hoy !");
    dire_quelque_chose!("hoy !", "quelqu'un");
}

Et nous obtenons :

 
Sélectionnez
Il dit : 'hoy !'
Il dit 'hoy !' à quelqu'un

Les macros fonctionnent donc exactement de la même manière qu'un match, sauf qu'ici on « match » sur les arguments. Petit détail qui peut avoir son utilité : les arguments des macros peuvent être utilisés avec des « [] » plutôt que des « () » si l'envie vous en prend.

 
Sélectionnez
dire_quelque_chose!["hoy !"];
dire_quelque_chose!["hoy !", "quelqu'un"];

On obtiendra exactement le même affichage que précédemment.

II-K-3. Les différents types d'argument

Comme vous vous en doutez, il y a d'autres types en plus des expr. En voici la liste complète :

  • ident : une identification. Exemples : x ; foo.
  • path : un nom qualifié. Exemple : T::SpecialA.
  • expr : une expression. Exemples : 2 + 2 ; if true then { 1 } else { 2 } ; f(42).
  • ty : un type. Exemples : i32 ; Vec<(char, String)> ; &T.
  • pat : un motif (ou « pattern »). Exemples : Some(t) ; (17, 'a') ; _.
  • stmt : une instruction unique (ou « single statement »). Exemple : let x = 3.
  • block : une séquence d'instructions délimitée par des accolades. Exemple : { log(error,"hi"); return 12; }.
  • item : un item. Exemples : fn foo() { } ; struct Bar;.
  • meta : un « meta item », comme les attributs. Exemple : cfg(target_os ="windows").
  • tt : un élément un peu plus global, il peut contenir toute une expression.

II-K-4. Répétition

Les macros comme vec!, print!, write!, etc. permettent le passage d'un nombre d'arguments variable (un peu comme les va_args en C ou les templates variadiques en C++). Cela fonctionne de la façon suivante :

 
Sélectionnez
macro_rules! vector {
    (
        $($x:expr),*
    ) => {
        [ $($x),* ].to_vec()
    }
}

fn main() {
    let mut v : Vec<u32> = vector!(1, 2, 3);

    v.push(6);
    println!("{:?}", &v);
}

Ici, on dit qu'on veut une expression répétée un nombre inconnu de fois (le $(votre_variable),*). La virgule devant l'étoile indique le séparateur entre les arguments, on aurait pu mettre un ';' si on l'avait voulu. D'ailleurs, pourquoi ne pas essayer ?

 
Sélectionnez
macro_rules! vector {
    (
        $($x:expr);*
    ) => {
        [ $($x),* ].to_vec()
    }
}

fn main() {
    let mut v : Vec<u32> = vector!(1; 2; 3);

    v.push(6);
    println!("{:?}", &v);
}

Dans le cas présent, on récupère le tout dans un slice qui est ensuite transformé en Vec. On pourrait aussi afficher tous les arguments un par un :

 
Sélectionnez
macro_rules! vector {
    (
        $x:expr,$($y:expr),*
    ) => (
        println!("Nouvel argument : {}", $x);
        vector!($($y),*);
    );
    ( $x:expr ) => (
        println!("Nouvel argument : {}", $x);
    )
}

fn main() {
    vector!(1, 2, 3, 12);
}

Vous aurez noté que j'ai remplacé les parenthèses par des accolades. Il aurait aussi été possible d'utiliser « {{ }} » ou même « [ ] ». Il est davantage question de goût. Pourquoi « {{ }} » ? Tout simplement parce qu'ici nous avons besoin d'un bloc d'instructions. Si votre macro ne renvoie qu'une simple expression, vous n'en aurez pas besoin.

II-K-5. Déboguer une macro

Vous vous en serez vite rendu compte, les macros peuvent rapidement devenir très complexes et déboguer le tout ne semble pas être une mince affaire ! Sachez qu'il existe deux macros pouvant vous aider dans cette tâche :

  • log_syntax!(…) : elle affiche les arguments sur la sortie standard au moment de la compilation.
  • trace_macros!(true) : le compilateur affichera un message à chaque fois qu'une macro sera utilisée. Utilisez trace_macros!(false) pour la « désactiver ».
 
Sélectionnez
#![feature(trace_macros)] // trace_macros n'est pas encore "stable" donc on doit activer cette fonctionnalité
#![feature(log_syntax)] // log_syntax n'est pas encore "stable" donc on doit activer cette fonctionnalité

macro_rules! vector {
    (
        $x:expr,$($y:expr),*
    ) => (
        println!("Nouvel argument : {}", $x);
        log_syntax!(vector!($($y),*));
    );
    ( $x:expr ) => (
        println!("Nouvel argument : {}", $x);
    )
}

fn main() {
    trace_macros!(true);
    vector!(1, 2, 3, 12);
}

Ce qui devrait vous donner à la compilation :

 
Sélectionnez
vector! { 1 , 2 , 3 , 12 }
println! { "Nouvel argument : {}" , 1 }
print! { concat ! ( "Nouvel argument : {}" , "\n" ) , 1 }
vector ! ( 2 , 3 , 12 )

II-K-6. Scope et exportation d'une macro

Créer des macros c'est bien, pouvoir s'en servir, c'est encore mieux ! Si vos macros sont déclarées dans un fichier à part (ce qui est une bonne chose !), il vous faudra ajouter cette ligne en haut du fichier où se trouvent vos macros :

 
Sélectionnez
#![macro_use]

Vous pourrez alors les utiliser dans votre projet.

Si vous souhaitez exporter des macros (car elles font partie d'une bibliothèque par exemple), il vous faudra ajouter au-dessus de la macro :

 
Sélectionnez
#[macro_export]

Enfin, si vous souhaitez utiliser des macros d'une des dépendances de votre projet, vous pourrez les importer comme cela :

 
Sélectionnez
#[macro_use]
extern crate nom_de_la_dependance;

II-K-7. Quelques macros utiles

En bonus, je vous donne une petite liste de macros qui pourraient vous être utiles :

II-K-8. Bonus

Pour clôturer ce chapitre, je vous propose le code suivant qui permet d'améliorer celui présenté dans le chapitre sur la généricitéGénéricité grâce à une macro :

 
Sélectionnez
macro_rules! creer_animal {
    ($nom_struct:ident) => {
        struct $nom_struct {
            nom: String,
            nombre_de_pattes: usize
        }

        impl Animal for $nom_struct {
            fn get_nom(&self) -> &str {
                &self.nom
            }

            fn get_nombre_de_pattes(&self) -> usize {
                self.nombre_de_pattes
            }
        }
    }
}

trait Animal {
    fn get_nom(&self) -> &str;
    fn get_nombre_de_pattes(&self) -> usize;
    fn affiche(&self) {
        println!("Je suis un animal qui s'appelle {} et j'ai {} pattes !", self.get_nom(), self.get_nombre_de_pattes());
    }
}

creer_animal!(Chien);
creer_animal!(Chat);

fn main() {
    fn affiche_animal<T: Animal>(animal: T) {
        animal.affiche();
    }

    let chat = Chat { nom : "Félix".to_owned(), nombre_de_pattes: 4};
    let chien = Chien { nom : "Rufus".to_owned(), nombre_de_pattes: 4};

    affiche_animal(chat);
    affiche_animal(chien);
}

II-L. Box

Le type Box est « tout simplement » un pointeur sur des données stockées « sur le tas » (dans la mémoire heap donc).

On s'en sert notamment quand on veut éviter de trop surcharger la pile (la mémoire stack) en instanciant directement sur la pile (la mémoire heap). Par contre, la feature le permettant est encore instable donc le code suivant ne fonctionnera pas si vous utilisez la version stable du compilateur :

 
Sélectionnez
#![feature(box_syntax)]

let a : [u8; 100000] = [0; 100000]; // bof...
let b : Box<[u8; 100000]> = box new([0; 100000]; // mieux !

Ou pour avoir une adresse constante quand on utilise une FFI (Foreign Function Interface), comme des pointeurs sur objet/fonction. Je n'en parlerai pas dans cette partie du cours.

Pour mieux illustrer ce qu'est le type Box, je vous propose deux exemples :

II-L-1. Structure récursive

On s'en sert aussi dans le cas de figure où on ne sait pas quelle taille fera le type, comme les types récursifs par exemple :

 
Sélectionnez
#[derive(Debug)]
enum List<T> {
    Element(T, List<T>),
    Vide,
}

fn main() {
    let list: List<i32> = List::Element(1, List::Element(2, List::Vide));
    println!("{:?}", list);
}

Si vous essayez de compiler ce code, vous obtiendrez une magnifique erreur : « invalid recursive enum type ». (Le problème sera exactement le même si on utilise une structure). Ce type n'a pas de taille définie, nous obligeant à utiliser un type qui lui en a une (donc & ou bien Box) :

 
Sélectionnez
#[derive(Debug)]
enum List<T> {
    Element(T, Box<List<T>>),
    Vide,
}

fn main() {
    let list: List<i32> = List::Element(1, Box::new(List::Element(2, Box::new(List::Vide))));
    println!("{:?}", list);
}

II-L-2. Liste chaînée

Un autre cas de figure où Box est utile à utiliser est pour la création de listes chaînées :

 
Sélectionnez
use std::fmt::Display;

struct List<T> {
    a: T,
    next: Option<Box<List<T>>>, // None signifiera qu'on est à la fin de la chaîne
}

impl<T> List<T> {
    pub fn new(a: T) -> List<T> {
        List {
            a: a,
            next: None,
        }
    }

    pub fn add_next(&mut self, a: T) {
        match self.next {
            Some(ref mut n) => n.add_next(a),
            None => {
                self.next = Some(Box::new(List::new(a)));
            }
        }
    }
}

impl<T: Display> List<T> {
    pub fn display_all_list(&self) {
        println!("-> {}", self.a);
        match self.next {
            Some(ref n) => n.display_all_list(),
            None => {}
        }
    }
}

fn main() {
    let mut a = List::new(0u32);

    a.add_next(1u32);
    a.add_next(2u32);
    a.display_all_list();
}

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2016 Guillaume Gomez. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.