Partager un comportement avec des traits

RustBeginner
Pratiquer maintenant

Introduction

Des types de données sans lien direct peuvent parfois devoir garantir le même comportement. Un capteur de serre et une expédition possèdent des champs différents, mais chacun peut décrire son état actuel. Les traits Rust nomment cette capacité commune sans obliger les types à partager la même structure de données.

Dans cet atelier, vous allez définir un petit trait, l’implémenter pour deux structures préparées, puis transmettre les deux valeurs à une seule fonction générique de reporting. Les structures plus longues et les exemples de données sont déjà créés afin que chaque modification reste ciblée.

Définir un contrat d’état partagé

Dans cette étape, vous allez définir un trait qui nomme un comportement commun à des types qui ne sont pas liés autrement.

Accédez au paquet préparé et examinez l’ossature complète. La commande sed -n affiche les lignes demandées sans les modifier :

cd /home/labex/project/status-reporter
sed -n '1,160p' src/main.rs

Sensor et Shipment possèdent des champs différents. Leurs champs texte préparés utilisent &'static str, c’est-à-dire des littéraux de chaîne empruntés qui restent valides pendant toute la durée du programme. L’annotation de durée de vie 'static ne fait pas partie de cette leçon sur les traits et est volontairement reportée ; vous n’avez pas besoin de la modifier ni de l’analyser ici.

Sous les espaces réservés, show_status est déjà générique. Lisez T: Status en trois parties : T représente un type concret, les deux-points signifient « doit satisfaire », et Status est le trait requis. En termes simples : « pour tout type T, à condition que T implémente Status ». Cette garantie permet à la fonction d’appeler item.status() en toute sécurité.

Le trait doit exister avant que Rust puisse comprendre la contrainte. Ouvrez le fichier source :

nano src/main.rs

Remplacez // STATUS_TRAIT par :

trait Status {
    fn status(&self) -> String;
}

La ligne de méthode se termine par un point-virgule, car le trait déclare une méthode obligatoire sans choisir d’implémentation. &self permet à la méthode d’inspecter une valeur sans en prendre possession, et String permet à chaque type de construire son propre texte d’état. Chaque implémentation ultérieure devra reprendre exactement le même nom de méthode, le même récepteur et le même type de retour, puis fournir son propre corps.

Enregistrez avec Ctrl+O, appuyez sur Entrée, puis quittez avec Ctrl+X. Vérifiez ensuite le paquet :

cargo check

Vous pouvez voir des avertissements indiquant que le trait et les éléments préparés ne sont pas encore utilisés. La ligne finale Finished est l’élément important : elle confirme que le contrat partagé et la contrainte générique sont valides. Les deux prochaines étapes ajouteront les implémentations et les appels.

Implémenter Status pour Sensor

Dans cette étape, vous allez respecter le contrat du trait pour Sensor et transmettre un capteur au rapporteur partagé.

Un bloc impl Trait for Type relie un trait existant à un type concret. Sa signature de méthode doit correspondre à celle du trait, tandis que son corps peut utiliser les propres champs de ce type.

Ouvrez le fichier source :

nano src/main.rs

Remplacez // SENSOR_IMPLEMENTATION par cette courte implémentation :

impl Status for Sensor {
    fn status(&self) -> String {
        format!("{}: {}°C", self.location, self.celsius)
    }
}

format! construit et renvoie une String ; il n’affiche rien directement. L’implémentation peut utiliser location et celsius parce que self est un Sensor emprunté.

Ensuite, dans main, remplacez // SENSOR_REPORT par :

    show_status("Sensor", &sensor);

L’argument &sensor correspond au paramètre emprunté &T du rapporteur ; sensor reste donc la propriété de main. Enregistrez et quittez Nano, puis exécutez le programme :

cargo run --quiet
Sensor: Greenhouse: 24°C

Cette ligne prouve que Sensor respecte Status et peut être transmis au rapporteur générique contraint. L’expédition n’est pas encore reliée.

Implémenter Status pour Shipment

Dans cette étape, vous allez implémenter le même trait pour Shipment et vérifier qu’un seul rapporteur accepte les deux types concrets.

Ouvrez à nouveau le fichier source :

nano src/main.rs

Remplacez // SHIPMENT_IMPLEMENTATION par :

impl Status for Shipment {
    fn status(&self) -> String {
        format!("#{} {}", self.id, self.stage)
    }
}

Cette méthode suit le même contrat de trait, mais utilise les propres champs id et stage de l’expédition. Ensuite, remplacez // SHIPMENT_REPORT par :

    show_status("Shipment", &shipment);

Enregistrez et quittez Nano. Exécutez le paquet terminé :

cargo run --quiet
Sensor: Greenhouse: 24°C
Shipment: #204 in transit

Les deux lignes proviennent de show_status<T: Status>. La contrainte décrit le comportement dont la fonction a besoin, tandis que chaque implémentation contrôle la manière dont sa propre valeur fournit ce comportement.

Résumé

Vous avez défini un comportement partagé avec un trait, l’avez implémenté indépendamment pour deux structures et utilisé une contrainte de trait élémentaire afin qu’un seul rapporteur générique puisse accepter les deux types.