Menu
Coddy logo textTech

Syntaxe des contraintes de trait

Fait partie de la section Object Oriented Programming du Journey Rust de Coddy — leçon 40 sur 61.

Jusqu'à présent, vous avez appris à définir des traits et à les implémenter pour des structures. Vous avez également travaillé avec des génériques pour écrire du code flexible. Il est maintenant temps de combiner ces concepts — en utilisant des traits pour contraindre les types qu'une fonction générique peut accepter.

Considérez une fonction générique qui doit appeler une méthode spécifique sur son paramètre. Sans aucune contrainte, Rust n'a aucune garantie que le type T possède cette méthode :

fn print_info<T>(item: T) {
    println!("{}", item.summarize());  // Erreur ! T pourrait ne pas avoir summarize()
}

La solution est une borne de trait (trait bound). En ajoutant : TraitName après le paramètre générique, vous indiquez à Rust que T doit implémenter ce trait :

trait Summary {
    fn summarize(&self) -> String;
}

fn print_info<T: Summary>(item: T) {
    println!("{}", item.summarize());  // Maintenant Rust sait que cette méthode existe
}

La syntaxe <T: Summary> se lit comme « T est n'importe quel type qui implémente Summary ». Désormais, la fonction n'accepte que les types qui possèdent le comportement requis. Si vous essayez de passer un type qui n'implémente pas Summary, le compilateur le rejettera avec une erreur claire.

Ce modèle est puissant car il vous permet d'écrire du code générique qui a tout de même accès à des fonctionnalités spécifiques. Votre fonction reste flexible — elle fonctionne avec n'importe quel type — mais seulement avec les types qui fournissent le comportement dont vous avez besoin.

challenge icon

Défi

Facile

Construisons un système de notification qui utilise des bornes de trait (trait bounds) pour garantir que seuls les messages correctement formatés peuvent être envoyés ! Vous allez créer une fonction générique qui accepte n'importe quel type implémentant un trait spécifique, démontrant ainsi comment les bornes de trait contraignent les paramètres génériques.

Vous organiserez votre code sur deux fichiers :

  • notification.rs : Définissez un trait public Notify avec une méthode appelée message qui prend &self et retourne un String. Créez ensuite deux structures publiques qui implémentent ce trait :
    • Email — avec un champ public subject (String). Sa méthode message doit retourner "Email: {subject}"
    • SMS — avec un champ public content (String). Sa méthode message doit retourner "SMS: {content}"
    Enfin, créez une fonction générique publique appelée send_notification qui accepte n'importe quel type T implémentant le trait Notify. Cette fonction doit imprimer le résultat de l'appel à message() sur l'élément.
  • main.rs : Importez votre module de notification et utilisez les entrées fournies pour créer une instance de Email et une instance de SMS. Appelez send_notification avec chacune d'elles pour démontrer que votre fonction générique fonctionne avec n'importe quel type qui implémente Notify.

L'idée clé ici est que send_notification ne connaît pas le type concret qu'elle reçoit — elle sait seulement que le type peut produire un message. La borne de trait T: Notify garantit cette capacité.

Votre sortie devrait montrer l'envoi des deux notifications :

Email: {subject}
SMS: {content}

Par exemple, avec les entrées Meeting Tomorrow et On my way! :

Email: Meeting Tomorrow
SMS: On my way!

Vous recevrez deux entrées : le sujet de l'e-mail et le contenu du SMS.

Aide-mémoire

Une borne de trait (trait bound) contraint un paramètre de type générique à n'accepter que les types qui implémentent un trait spécifique.

La syntaxe pour les bornes de trait utilise : TraitName après le paramètre générique :

fn function_name<T: TraitName>(item: T) {
    // Peut maintenant appeler des méthodes de TraitName sur item
}

Exemple avec une borne de trait :

trait Summary {
    fn summarize(&self) -> String;
}

fn print_info<T: Summary>(item: T) {
    println!("{}", item.summarize());
}

La notation <T: Summary> signifie « T est n'importe quel type qui implémente Summary ». Cela permet à la fonction d'appeler des méthodes de trait tout en restant générique.

Essayez vous-même

mod notification;

use notification::{Email, SMS, send_notification};

fn main() {
    // Lire les entrées
    let mut subject = String::new();
    std::io::stdin().read_line(&mut subject).expect("Failed to read line");
    let subject = subject.trim().to_string();
    
    let mut content = String::new();
    std::io::stdin().read_line(&mut content).expect("Failed to read line");
    let content = content.trim().to_string();
    
    // TODO: Créer une instance Email avec le sujet
    
    // TODO: Créer une instance SMS avec le contenu
    
    // TODO: Appeler send_notification avec l'email
    
    // TODO: Appeler send_notification avec le sms
}
quiz iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Object Oriented Programming