Traits und Trait-Grenzen in Rust: Eine umfassende Anleitung
Olivia Novak
Dev Intern · Leapcell

Ein Trait in Rust ist ähnlich dem, was in anderen Programmiersprachen oft als „Interface“ bezeichnet wird, obwohl es einige Unterschiede gibt. Ein Trait teilt dem Rust-Compiler mit, dass ein bestimmter Typ Funktionalität besitzt, die möglicherweise mit anderen Typen geteilt wird. Traits ermöglichen es uns, gemeinsames Verhalten auf abstrakte Weise zu definieren. Wir können Trait-Grenzen verwenden, um festzulegen, dass ein generischer Typ bestimmte Verhaltensweisen implementieren muss.
Einfach ausgedrückt ist ein Trait wie ein Interface in Rust, das das Verhalten definiert, das ein Typ bereitstellen muss, wenn er diesen Trait implementiert. Traits können das Verhalten einschränken, das zwischen mehreren Typen geteilt wird, und wenn sie in der generischen Programmierung verwendet werden, können sie Generics auf Typen beschränken, die dem durch den Trait angegebenen Verhalten entsprechen.
Einen Trait definieren
Wenn verschiedene Typen das gleiche Verhalten zeigen, können wir einen Trait definieren und ihn dann für diese Typen implementieren. Einen Trait zu definieren bedeutet, eine Reihe von Methoden zu gruppieren, mit dem Ziel, ein Verhalten und eine Reihe von Anforderungen zu beschreiben, die notwendig sind, um einen bestimmten Zweck zu erreichen.
Ein Trait ist ein Interface, das eine Reihe von Methoden definiert:
pub trait Summary { // Methoden innerhalb von Traits müssen nur deklariert werden fn summarize_author(&self) -> String; // Diese Methode hat eine Standardimplementierung; andere Typen müssen sie nicht selbst implementieren fn summarize(&self) -> String { format!("(Weiterlesen von {}...)", self.summarize_author()) } }
- Dies definiert einen Trait namens
Summary
, der zwei Methoden bereitstellt:summarize_author
undsummarize
. - Methoden in Traits benötigen nur Deklarationen; ihre Implementierung bleibt den jeweiligen Typen überlassen. Methoden können jedoch auch Standardimplementierungen haben. In diesem Fall hat die Methode
summarize
eine Standardimplementierung, die internsummarize_author
aufruft, die keine Standardimplementierung hat. - Beide Methoden des
Summary
-Traits nehmenself
als Parameter, genau wie Methoden für Strukturen. Hier istself
das erste Argument für die Trait-Methoden.
Hinweis: Tatsächlich ist
self
eine Kurzform fürself: Self
,&self
ist eine Kurzform fürself: &Self
und&mut self
ist eine Kurzform fürself: &mut Self
.Self
bezieht sich auf den Typ, der den Trait implementiert. Wenn beispielsweise ein TypFoo
den TraitSummary
implementiert, bezieht sichSelf
innerhalb der Implementierung aufFoo
.
pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!("@{}\ gepostet einen Tweet...", self.summarize_author()) } } pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } pub struct Post { pub title: String, pub author: String, pub content: String, } impl Summary for Post { fn summarize_author(&self) -> String { format!("{} hat einen Artikel gepostet", self.author) } fn summarize(&self) -> String { format!("{} gepostet: {}", self.author, self.content) } } impl Summary for Tweet { fn summarize_author(&self) -> String { format!("{} getwittert", self.username) } fn summarize(&self) -> String { format!("@{}: {}", self.username, self.content) } } fn main() { let tweet = Tweet { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, }; println!("{}", tweet.summarize()) }
Es gibt ein wichtiges Prinzip bezüglich des Ortes, an dem Traits und ihre Implementierungen definiert werden können, das als Orphan-Regel bezeichnet wird: Wenn Sie den Trait T
für den Typ A
implementieren möchten, muss entweder T
oder A
im aktuellen Crate definiert sein.
Diese Regel stellt sicher, dass von anderen geschriebener Code Ihren Code nicht zerstört und dass Ihr Code nicht unbeabsichtigt den Code anderer beschädigt.
Traits als Funktionsparameter verwenden
Traits können als Funktionsparameter verwendet werden. Hier ist ein Beispiel für die Definition einer Funktion, die einen Trait als Parameter verwendet:
pub fn notify(item: &impl Summary) { // Trait-Parameter println!("Breaking News! {}", item.summarize()); }
Der Parameter item
bedeutet „ein Wert, der den Trait Summary
implementiert“. Sie können jeden Typ, der den Trait Summary
implementiert, als Argument für diese Funktion verwenden. Innerhalb des Funktionskörpers können auch Methoden, die im Trait definiert sind, für den Parameter aufgerufen werden.
Trait-Grenzen
Die Verwendung von impl Trait
oben ist eigentlich syntaktischer Zucker. Die vollständige Syntax ist wie folgt: T: Summary
, was als Trait-Grenze bezeichnet wird.
pub fn notify<T: Summary>(item: &T) { // Trait-Grenze println!("Breaking News! {}", item.summarize()); }
Für komplexere Anwendungsfälle bieten Trait-Grenzen mehr Flexibilität und Ausdruckskraft. Zum Beispiel eine Funktion, die zwei Parameter akzeptiert, die beide den Trait Summary
implementieren:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {} // Trait-Parameter pub fn notify<T: Summary>(item1: &T, item2: &T) {} // Generische T-Grenze: erfordert, dass item1 und item2 vom selben Typ sind und dass T den Summary-Trait implementiert
Mehrere Trait-Grenzen mit + angeben
Neben einzelnen Einschränkungen können Sie mehrere Einschränkungen angeben, z. B. dass ein Parameter mehrere Traits implementieren muss:
pub fn notify(item: &(impl Summary + Display)) {} // Zucker-Syntax pub fn notify<T: Summary + Display>(item: &T) {} // Vollständige Trait-Grenzen-Syntax
Trait-Grenzen mit where
vereinfachen
Wenn es viele Trait-Einschränkungen gibt, können Funktionssignaturen schwer lesbar werden. In solchen Fällen können Sie die where
-Klausel verwenden, um die Syntax zu bereinigen:
// Wenn mehrere generische Typen viele Trait-Grenzen haben, kann die Signatur schwer zu lesen sein fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { ... } // Verwendung von `where` zur Vereinfachung, wodurch Funktionsname, Parameter und Rückgabetyp näher zusammenrücken fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug { ... }
Bedingtes Implementieren von Methoden oder Traits mithilfe von Trait-Grenzen
Die Verwendung von Trait-Grenzen als Parameter ermöglicht es uns, Methoden bedingt basierend auf bestimmten Typen und Traits zu implementieren, sodass Funktionen Argumente verschiedener Typen akzeptieren können. Zum Beispiel:
fn notify(summary: impl Summary) { println!("notify: {}", summary.summarize()) } fn notify_all(summaries: Vec<impl Summary>) { for summary in summaries { println!("notify: {}", summary.summarize()) } } fn main() { let tweet = Weibo { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, }; let tweets = vec![tweet]; notify_all(tweets); }
Der Parameter summary
in der Funktion verwendet impl Summary
anstelle eines konkreten Typs. Dies bedeutet, dass die Funktion jeden Typ akzeptieren kann, der den Trait Summary
implementiert.
Wenn Sie einen Wert besitzen möchten und sich nur darum kümmern, dass er einen bestimmten Trait implementiert – nicht um seinen konkreten Typ – können Sie die Trait-Objektform verwenden, die einen Smartpointer wie Box
mit dem Schlüsselwort dyn
kombiniert.
fn notify(summary: Box<dyn Summary>) { println!("notify: {}", summary.summarize()) } fn notify_all(summaries: Vec<Box<dyn Summary>>) { for summary in summaries { println!("notify: {}", summary.summarize()) } } fn main() { let tweet = Tweet { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, }; let tweets: Vec<Box<dyn Summary>> = vec![Box::new(tweet)]; notify_all(tweets); }
Traits in Generics verwenden
Schauen wir uns an, wie Traits verwendet werden, um generische Typen in der generischen Programmierung einzuschränken.
Im früheren Beispiel, in dem wir die Funktion notify
als fn notify(summary: impl Summary)
definiert haben, haben wir angegeben, dass der Typ des Parameters summary
den Trait Summary
implementieren soll, anstatt einen konkreten Typ anzugeben. Tatsächlich ist impl Summary
syntaktischer Zucker für eine Trait-Grenze in der generischen Programmierung. Der Code mit impl Trait
kann umgeschrieben werden als:
fn notify<T: Summary>(summary: T) { println!("notify: {}", summary.summarize()) } fn notify_all<T: Summary>(summaries: Vec<T>) { for summary in summaries { println!("notify: {}", summary.summarize()) } } fn main() { let tweet = Tweet { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, }; let tweets = vec![tweet]; notify_all(tweets); }
impl Trait
aus Funktionen zurückgeben
Sie können impl Trait
verwenden, um anzugeben, dass eine Funktion einen Typ zurückgibt, der einen bestimmten Trait implementiert:
fn returns_summarizable() -> impl Summary { Tweet { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, } }
Diese Art von Rückgabetyp mit impl Trait
muss zu einem einzigen konkreten Typ aufgelöst werden. Wenn die Funktion möglicherweise verschiedene Typen zurückgibt, die denselben Trait implementieren, führt dies zu einem Kompilierungsfehler. Zum Beispiel:
fn returns_summarizable(switch: bool) -> impl Summary { if switch { Tweet { ... } // Kann hier nicht zwei verschiedene Typen zurückgeben } else { Post { ... } // Kann hier nicht zwei verschiedene Typen zurückgeben } }
Der obige Code würde einen Fehler verursachen, da er zwei verschiedene Typen zurückgibt – Tweet
und Post
–, obwohl beide denselben Trait implementieren. Wenn Sie verschiedene Typen zurückgeben möchten, müssen Sie ein Trait-Objekt verwenden:
fn returns_summarizable(switch: bool) -> Box<dyn Summary> { if switch { Box::new(Tweet { ... }) // Trait-Objekt } else { Box::new(Post { ... }) // Trait-Objekt } }
Zusammenfassung
Eines der Hauptziele von Rusts Design ist Abstraktionen ohne Laufzeitkosten – High-Level-Sprachfunktionen ohne Einbußen bei der Laufzeitleistung zu ermöglichen. Das Fundament dieser Abstraktion ohne Laufzeitkosten sind Generics und Traits. Sie ermöglichen es, High-Level-Syntax während der Kompilierung in effizienten Low-Level-Code zu kompilieren, wodurch die Laufzeiteffizienz ermöglicht wird.
Traits definieren gemeinsames Verhalten auf abstrakte Weise, während Trait-Grenzen Einschränkungen für Funktionsparameter oder Rückgabetypen definieren – wie z. B. impl SuperTrait
oder T: SuperTrait
. Traits und Trait-Grenzen ermöglichen es uns, Wiederholungen zu reduzieren, indem wir generische Typparameter verwenden, während wir dem Compiler dennoch eine klare Anleitung geben, welche Verhaltensweisen diese generischen Typen implementieren müssen. Da wir dem Compiler Trait-Grenzen-Informationen zur Verfügung stellen, kann er überprüfen, ob die in unserem Code verwendeten tatsächlichen Typen das richtige Verhalten bieten.
Zusammenfassend lässt sich sagen, dass Traits in Rust zwei Hauptzwecken dienen:
- Verhaltensabstraktion: Ähnlich wie Interfaces abstrahieren sie über das gemeinsame Verhalten von Typen, indem sie gemeinsame Funktionalität definieren.
- Typeinschränkungen: Sie schränken das Typverhalten ein und verringern den Umfang dessen, was ein Typ sein kann, basierend darauf, welche Traits er implementiert.
Wir sind Leapcell, Ihre erste Wahl für das Hosten von Rust-Projekten.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Sprachen-Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Unbegrenzt viele Projekte kostenlos bereitstellen
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen mit einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und Protokollierung für umsetzbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein betrieblicher Aufwand – konzentrieren Sie sich einfach auf das Bauen.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ