Rust

Any donation is very welcome
Fork me on GitHub

II. Spécificités de Rust

2. 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 sûr de bien tout comprendre. Commençons donc de ce pas !

À 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 :

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

Runlet 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é au-dessus, je pense que certains d'entre vous se demande à 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 :

Runstruct 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).

Déstructuration

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

Runstruct Point {
    x: i32,
    y: i32,
}

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

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

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

Runstruct Point {
    x: i32,
    y: i32,
}

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

match origin {
    Point { 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.

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 :

Runpub struct Distance {
    // Ce champs n'est pas public donc impossible d'y accéder directement
    // en-dehors de ce fichier !
    metre: i32,
}

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

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

// autre fichier
// Si la définition de Distance est dans fichier.rs
mod fichier;

fn main() {
    let d = fichier::Distance::new();
    // ou
    let d = fichier::Distance::new_with_value(10);
}

Quel intérêt vous vous dites ? Après tout, on irait aussi vite de le faire nous-même ! 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 :

Runpub struct Distance {
    metre: i32,
}

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

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

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

// autre fichier
// Si la définition de Distance est dans fichier.rs
mod fichier;

fn main() {
    let d = fichier::Distance::new();
    // ou
    let d = fichier::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 :

Runimpl Distance {
    // les autres méthodes
    // ...

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

Tout simplement !

Syntaxe de mise à jour (ou "update syntax")

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

Runstruct 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

Destructeur

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

Runstruct Distance {
    metre: i32,
}

impl Distance {
    // fonctions membres
}

impl Drop for Distance {
    fn drop(&mut 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, mais pour plus d'explications, il va vous falloir lire le chapitre suivant !