Exécuter du code lors du nettoyage avec le trait Drop

Le second trait important pour les pointeurs intelligents est Drop, qui vous permet de personnaliser ce qui se passe lorsqu'une valeur est en train de sortir d'une portée. Vous pouvez fournir une implémentation du trait Drop sur n'importe quel type, et le code que vous renseignez peut être utilisé pour libérer des ressources comme des fichiers ou des connections réseau. Nous présentons Drop dans le contexte des pointeurs intelligents car la fonctionnalité du trait Drop est quasiment systématiquement utilisée lorsque nous implémentons un pointeur intelligent. Par exemple, lorsqu'une Box<T> est libérée, elle va désallouer l'espace occupé sur le tas sur lequel la boite pointe.

Dans certains langages, le développeur doit appeler du code pour libérer la mémoire ou des ressources à chaque fois qu'il finit d'utiliser une instance ou un pointeur intelligent. S'il oublie de le faire, le système peut surcharger et planter. Avec Rust, vous pouvez renseigner du code qui sera exécuté à chaque fois qu'une valeur sort de la portée, et le compilateur va insérer automatiquement ce code. Au final, vous n'avez pas besoin de concentrer votre attention à placer du code de nettoyage à chaque fois qu'une instance d'un type particulier n'est plus utilisée — vous ne risquez pas d'avoir des fuites de ressources !

Vous renseignez le code à exécuter lorsqu'une valeur sort de la portée en implémentant le trait Drop. Le trait Drop nécessite que vous implémentiez une méthode drop qui prend en paramètre une référence mutable à self. Pour voir quand Rust appelle drop, implémentons drop avec une instruction println! à l'intérieur, pour le moment.

L'encart 15-14 montre une structure PointeurPerso dont la seule fonctionnalité personnalisée est qu'elle va écrire Nettoyage d'un PointeurPerso ! lorsque l'instance sort de la portée. Cet exemple signale quand Rust exécute la fonction drop.

Fichier : src/main.rs

struct PointeurPerso {
    donnee: String,
}

impl Drop for PointeurPerso {
    fn drop(&mut self) {
        println!("Nettoyage d'un PointeurPerso avec la donnée `{}` !", self.donnee);
    }
}

fn main() {
    let c = PointeurPerso {
        donnee: String::from("des trucs"),
    };
    let d = PointeurPerso {
        donnee: String::from("d'autres trucs"),
    };
    println!("PointeurPersos créés.");
}

Encart 15-14 : Une structure PointeurPerso qui implémente le trait Drop dans lequel nous plaçons notre code de nettoyage

Le trait Drop est importé dans l'étape préliminaire, donc nous n'avons pas besoin de l'importer dans la portée. Nous implémentons le trait Drop sur PointeurPerso et nous fournissons une implémentation de la méthode drop qui appelle println!. Le corps de la fonction drop est l'endroit où vous placez la logique que vous souhaitez exécuter lorsqu'une instance du type concerné sort de la portée. Ici nous affichons un petit texte pour voir quand Rust appelle drop.

Dans le main, nous créons deux instances de PointeurPerso et ensuite on affiche PointeurPersos créés. A la fin du main, nos instances de PointeurPerso vont sortir de la portée, et Rust va appeler le code que nous avons placé dans la méthode drop et qui va afficher notre message final. Notez que nous n'avons pas besoin d'appeler explicitement la méthode drop.

Lorsque nous exécutons ce programme, nous devrions voir la sortie suivante :

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.60s
     Running `target/debug/drop-example`
PointeurPersos créés.
Nettoyage d'un PointeurPerso avec la donnée `d'autres trucs`!
Nettoyage d'un PointeurPerso avec la donnée `des trucs`!

Rust a appelé automatiquement drop pour nous lorsque nos instances sont sorties de la portée, appelant ainsi le code que nous y avions mis. Les variables sont libérées dans l'ordre inverse de leur création, donc d a été libéré avant c. Cet exemple vous fournit une illustration de la façon dont la méthode drop fonctionne ; normalement vous devriez y mettre le code de nettoyage dont votre type a besoin d'exécuter plutôt que d'afficher simplement un message.

Libérer prématurément une valeur avec std::mem::drop

Malheureusement, il n'est pas simple de désactiver la fonctionnalité automatique drop. La désactivation de drop n'est généralement pas nécessaire ; tout l'intérêt du trait Drop est qu'il est pris en charge automatiquement. Occasionnellement, cependant, vous pourriez avoir besoin de nettoyer prématurément une valeur. Un exemple est lorsque vous utilisez des pointeurs intelligents qui gèrent un système de verrouillage : vous pourriez vouloir forcer la méthode drop qui libère le verrou afin qu'un autre code dans la même portée puisse prendre ce verrou. Rust ne vous autorise pas à appeler manuellement la méthode drop du trait Drop ; à la place vous devez appeler la fonction std::mem::drop, fournie par la bibliothèque standard, si vous souhaitez forcer une valeur à être libérée avant la fin de sa portée.

Si nous essayons d'appeler manuellement la méthode drop du trait Drop en modifiant la fonction main de l'encart 15-14, comme dans l'encart 15-15, nous aurons une erreur de compilation :

Fichier : src/main.rs

struct PointeurPerso {
    donnee: String,
}

impl Drop for PointeurPerso {
    fn drop(&mut self) {
        println!("Nettoyage d'un PointeurPerso avec la donnée `{}` !", self.donnee);
    }
}

fn main() {
    let c = PointeurPerso {
        donnee: String::from("des trucs"),
    };
    println!("PointeurPerso créé.");
    c.drop();
    println!("PointeurPerso libéré avant la fin du main.");
}

Encart 15-15 : tentative d'appel manuel de la méthode drop du trait Drop afin de nettoyer prématurément

Lorsque nous essayons de compiler ce code, nous obtenons l'erreur suivante :

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
  --> src/main.rs:16:7
   |
16 |     c.drop();
   |     --^^^^--
   |     | |
   |     | explicit destructor calls not allowed
   |     help: consider using `drop` function: `drop(c)`

For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` due to previous error

Ce message d'erreur signifie que nous ne sommes pas autorisés à appeler explicitement drop. Le message d'erreur utilise le terme de destructeur (destructor) qui est un terme général de programmation qui désigne une fonction qui nettoie une instance. Un destructeur est analogue à un constructeur, qui construit une instance. La fonction drop en Rust est un destructeur particulier.

Rust ne nous laisse pas appeler explicitement drop car Rust appellera toujours automatiquement drop sur la valeur à la fin du main. Cela serait une erreur de double libération car Rust essayerait de nettoyer la même valeur deux fois.

Nous ne pouvons pas désactiver l'ajout automatique de drop lorsqu'une valeur sort de la portée, et nous ne pouvons pas désactiver explicitement la méthode drop. Donc, si nous avons besoin de forcer une valeur à être nettoyée prématurément, nous pouvons utiliser la fonction std::mem::drop.

La fonction std::mem::drop est différente de la méthode drop du trait Drop. Nous pouvons l'appeler en lui passant en argument la valeur que nous souhaitons libérer prématurément. La fonction est présente dans l'étape préliminaire, donc nous pouvons modifier main de l'encart 15-15 pour appeler la fonction drop, comme dans l'encart 15-16 :

Fichier : src/main.rs

struct PointeurPerso {
    donnee: String,
}

impl Drop for PointeurPerso {
    fn drop(&mut self) {
        println!("Nettoyage d'un PointeurPerso avec la donnée `{}` !", self.donnee);
    }
}

fn main() {
    let c = PointeurPerso {
        donnee: String::from("des trucs"),
    };
    println!("PointeurPerso créé.");
    drop(c);
    println!("PointeurPerso libéré avant la fin du main.");
}

Encart 15-16 : appel à std::mem::drop pour libérer explicitement une valeur avant qu'elle sorte de la portée

L'exécution de code va afficher ceci :

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/drop-example`
PointeurPerso créé.
Nettoyage d'un PointeurPerso avec la donnée `des trucs` !
PointeurPerso libéré avant la fin du main.

Le texte Nettoyage d'un PointeurPerso avec la donnée `des trucs` ! est affiché entre PointeurPerso créé et PointeurPerso libéré avant la fin du main, ce qui démontre que la méthode drop a été appelée pour libérer c à cet endroit.

Vous pouvez utiliser le code renseigné dans une implémentation du trait Drop de plusieurs manières afin de rendre le nettoyage pratique et sûr : par exemple, vous pouvez l'utiliser pour créer votre propre alloueur de mémoire ! Grâce au trait Drop et le système de possession de Rust, vous n'avez pas à vous souvenir de nettoyer car Rust le fait automatiquement.

Vous n'avez pas non plus à vous soucier des problèmes résultant du nettoyage accidentel de valeurs toujours utilisées : le système de possession garantit que les références restent toujours en vigueur, et garantit également que drop n'est appelée qu'une seule fois lorsque la valeur n'est plus utilisée.

Maintenant que nous avons examiné Box<T> et certaines des caractéristiques des pointeurs intelligents, découvrons d'autres pointeurs intelligents définis dans la bibliothèque standard.