Levans' workshop

Experiments and thoughts about Machine Learning, Rust, and other stuff...

Pseudo-héritage en Rust avec Deref


Un des aspects absent de Rust à l'heure actuelle (1.0) mais toutefois assez demandé est l'héritage, comme on peut le trouver dans de nombreux autres langages de programmation.

Il y a somme toute deux approches à l'héritage : celle qui utilise les méthodes virtuelles, et celle qui ne les utilise pas.

Je m'intéresse ici à ce second cas. L'utilité principale est qu'on a un ensemble de structs qui partagent toutes un certains nombres de champs, et qu'on veut réduire la duplication de code.

Par exemple, voyons une structure comme celle-ci, qui pourrait décrire des messages dans un salon de discussion:

struct PrivateMessage {
    from: String,
    content: String,
    date: u64,
    target_user: String
}

struct GroupMessage {
    from: String,
    content: String,
    date: u64,
    target_group: String
}

Ici, les trois premiers champs de chaque structure sont les mêmes, et un héritage depuis une classe BaseMessage, surtout si cette classe implémente plusieurs méthodes que l'on voudrait utiliser sur tous nos messages.

L'approche serait donc de se tourner vers quelque chose de cet esprit:

struct BaseMessage {
    from: String,
    content: String,
    date: u64
}

impl BaseMessage {
    fn is_very_old(&self) -> bool { /* ... */ }
    fn is_very_big(&self) -> bool { /* ... */ }
    // and many other methods
}

struct PrivateMessage {
    base: BaseMessage,
    target_user: String
}

struct GroupMessage {
    base: BaseMessage,
    target_group: String
}

Mais là, bon, c'est bien gentil, mais c'est pas forcément très ergonomique de devoir se taper my_message.base.is_very_big(), ou bien my_message.base.date pour récupérer la date. Avec un héritage digne de ce nom, on aurait tout simplement my_message.is_very_big() ou my_message.date.

Qu'à celà ne tienne, tout n'est pas perdu, il nous reste Deref ! Ce trait permet de définir l'opérateur * de déréférencement sur nos structures. Rust procède à un auto-déréférencement pour accéder aux attributs et aux méthodes des structures.

Par exemple, String implémente Deref<Target=str>, ce qui nous permet par exemple d'appeller .contains(..) sur une String, alors que cette méthode n'est définir que sur &str.

Vous avez deviné, il suffit de faire pareil sur nos messages:

use std::ops::Deref;

impl Deref for PrivateMessage {
    type Target = BaseMessage;
    fn deref(&self) -> &BaseMessage { &self.base }
}

impl Deref for GroupMessage {
    type Target = BaseMessage;
    fn deref(&self) -> &BaseMessage { &self.base }
}

Et voilà ! Une fois le trait implémenté, ça marche tout seul.

Si en plus vous voulez accéder à des méthodes mutables, il faut également implémenter le trait DerefMut. Ce dernier requiert que vous ayez déjà implémenté Deref et utilise le même type Target.

impl DerefMut for PrivateMessage {
    fn deref_mut(&mut self) -> &mut BaseMessage { &mut self.base }
}

impl DerefMut for GroupMessage {
    fn deref_mut(&mut self) -> &mut BaseMessage { &mut self.base }
}

Et c'est tout ! Ça marche aussi simplement que l'héritage. On peut même récupérer un pointeur &BaseMessage via &*message ou &message as &BaseMessage. Pas de méthodes virtuelles par contre, mais tant que ce n'est pas nécessaire, cette approche peut très bien convenir.