Kapitel 11: Schnittstellen (Traits) – Der Profi-Bereich
In diesem Abschnitt widmen wir uns den fortgeschrittenen Architekturmustern und Best Practices rund um Rusts Schnittstellensystem. Traits bilden das Fundament für Abstraktion, Code-Wiederverwendung und lose Kopplung in Rust. Für professionelle Entwickler ist es unerlässlich, nicht nur die grundlegende Syntax zu beherrschen, sondern auch die zugrundeliegenden Regeln, Mechanismen und Performanz-Implikationen zu verstehen.
Item 36: Verstehe die Orphan-Rule (Waisenregel) zur Vermeidung globaler Namenskollisionen
Die Speichersicherheit und Typsicherheit von Rust basieren auf der Eindeutigkeit von Implementierungen. Wenn der Compiler ein Trait-Verhalten für einen Typ auflösen muss, darf es zu keinem Zeitpunkt Unklarheit darüber geben, welche Implementierung verwendet werden soll. Um diese Eindeutigkeit global zu garantieren, erzwingt Rust die sogenannte Orphan-Rule (Waisenregel).
Die Theorie: Warum Kohärenz (Coherence) wichtig ist
Die Orphan-Rule besagt:
[vanilla markdown] Ein Implementierungsblock
impl\<T\> Trait for Typist nur dann zulässig, wenn sich entweder dasTraitoder derTyp(oder beide) im aktuellen Crate (der aktuellen Kompiliereinheit) befinden.
Wenn weder das Trait noch der Typ lokal in Ihrem Crate definiert sind, spricht man von einer “Waise” (Orphan). Rust verbietet solche Implementierungen rigoros.
Stellen Sie sich vor, diese Regel würde nicht existieren:
- Eine Bibliothek
lib_aimplementiert das Standard-Traitstd::fmt::Displayfür den Standard-TypVec\<T\>. - Eine andere Bibliothek
lib_bimplementiert ebenfallsstd::fmt::DisplayfürVec\<T\>, jedoch mit einer anderen Formatierungslogik. - Sie schreiben ein Anwendungsprogramm, das sowohl
lib_aals auchlib_bals Abhängigkeiten einbindet und versucht, einen Vektor überprintln!("{}", mein_vektor)auszugeben.
Der Compiler stünde vor einem unlösbaren Dilemma: Er müsste sich zwischen zwei konkurrierenden Implementierungen entscheiden. Es gäbe keine eindeutige, deterministische Lösung. Durch das Verbot von Waisen-Implementierungen stellt Rust sicher, dass es für jede Kombination aus Trait und Typ im gesamten Abhängigkeitsbaum maximal eine einzige Implementierung geben kann (Kohärenz).
Alltagsanalogie: Der Reisestecker-Adapter
Stellen Sie sich vor, Sie reisen mit einem deutschen Fön (Typ-F-Stecker) in die USA (Typ-A-Steckdosen). Weder der Fön noch die Steckdose gehören Ihnen – beide sind standardisierte Produkte von Fremdherstellern. Sie können weder das amerikanische Stromnetz umbauen (das Trait verändern) noch das Kabel des Föhns abschneiden und direkt an die Wand löten (den Typ verändern).
Die Lösung ist ein Reisestecker-Adapter: Sie stecken den Fön in ein kleines Zwischengehäuse, das Sie selbst erworben haben (Ihr lokaler Typ), und stecken dieses in die Steckdose. Der Adapter “umhüllt” das Fremdgerät und stellt die Verbindung zur Fremdsteckdose her. Dies entspricht exakt dem Newtype-Pattern in Rust.
Die Praxis: Compilerfehler und das Newtype-Pattern
Versuchen wir zunächst, die Waisenregel absichtlich zu verletzen, um die Fehlermeldung des Compilers zu verstehen. Wir möchten das Standard-Trait std::fmt::Display direkt für den Standard-Typ Vec\<String\> implementieren, um Vektoren direkt formatieren zu können:
#![allow(unused)]
fn main() {
// Dieser Code provoziert einen Compilerfehler!
use std::fmt;
impl fmt::Display for Vec<String> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}]", self.join(", "))
}
}
}
Wenn Sie versuchen, diesen Code zu kompilieren, meldet der Compiler den Fehler E0117:
error[E0117]: only traits defined in the current crate can be implemented for arbitrary types
--> src/main.rs:3:1
|
3 | impl fmt::Display for Vec<String> {
| ^^^^^^^^^^^^^^^^^^^^^^-----------
| | |
| | this type is not defined in the current crate
| impl doesn't use only types from this crate
|
= note: define and implement a trait or new type instead
Der Compiler erklärt präzise das Problem: Weder Display (aus std::fmt) noch Vec (aus std::vec) wurden in unserem aktuellen Crate definiert.
Um dieses Problem zu umgehen, nutzen wir das Newtype-Pattern. Wir definieren eine neue, lokale Struktur (ein sogenanntes Tupel-Struct), die den fremden Typ als einziges Feld umschließt:
use std::fmt;
// Wir erstellen einen lokalen Wrapper-Typ um den Vec<String>.
// Da diese Struktur in unserem Crate definiert ist, duerfen wir beliebige
// Traits dafür implementieren.
struct StringListe(Vec<String>);
// Jetzt implementieren wir das Standard-Trait Display für unseren neuen Typ.
impl fmt::Display for StringListe {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// self.0 greift auf das erste und einzige Feld des Tupel-Structs zu
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let liste = StringListe(vec![
String::from("Rust"),
String::from("C++"),
String::from("Python"),
]);
// Da StringListe das Display-Trait implementiert, koennen wir es direkt ausgeben
println!("Unterstuetzte Sprachen: {}", liste);
}
Zeilenweise Erklärung des Newtype-Patterns:
struct StringListe(Vec\<String\>);: Hier deklarieren wir eine neue StrukturStringListe. Sie ist ein Tupel-Struct mit genau einem anonymen Feld vom TypVec\<String\>. Da diese Deklaration in unserem Crate stattfindet, giltStringListeals lokaler Typ.impl fmt::Display for StringListe: Wir implementieren das TraitDisplay. Da der TypStringListelokal ist, ist diese Implementierung absolut konform mit der Waisenregel, obwohlDisplayein fremdes Trait ist.self.0: Über die Tupel-Index-Syntax greifen wir auf den zugrundeliegendenVec\<String\>zu, um dessen Elemente mit.join(", ")zu einer einzigen Zeichenkette zu verbinden.
Vor- und Nachteile des Newtype-Patterns:
- Vorteil: Vollständige Einhaltung der Typsicherheit und Umgehung der Orphan-Rule.
- Nachteil: Der Wrapper-Typ verhält sich nicht automatisch wie der innere Typ. Wenn Sie Methoden von
VecaufStringListeaufrufen möchten (z. B..push()oder.len()), müssen Sie diese entweder delegieren oder dasDeref-Trait implementieren:
#![allow(unused)]
fn main() {
use std::ops::Deref;
impl Deref for StringListe {
type Target = Vec<String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
}
Durch die Implementierung von Deref können Sie auf Instanzen von StringListe direkt Methoden des inneren Vec aufrufen (Coercion), während die Typsicherheit für die Schnittstellen-Implementierung erhalten bleibt.
Item 37: Nutze impl Trait und Trait Bounds (T: Trait) zur statischen Polymorphie
Rust bietet zwei primäre Wege, um statische Polymorphie (Kompilierzeit-Polymorphie) auszudrücken: explizite Trait Bounds (Generics mit Schnittstellenschranken) und das Schlüsselwort impl Trait. Beide Ansätze führen in den meisten Fällen zum selben optimierten Maschinencode, unterscheiden sich jedoch in ihrer Flexibilität, Lesbarkeit und Ausdrucksstärke.
Syntax-Vergleich und Monomorphisierung
Wenn wir eine Funktion schreiben, die ein Argument akzeptiert, das eine bestimmte Schnittstelle implementiert, haben wir die Wahl zwischen zwei Schreibweisen:
#![allow(unused)]
fn main() {
trait Protokollierbar {
fn nachricht(&self) -> String;
}
// Option A: Expliziter Trait Bound (Generics)
fn log_generic<T: Protokollierbar>(item: T) {
println!("Log (Generic): {}", item.nachricht());
}
// Option B: Anonymer Typparameter mittels impl Trait
fn log_impl(item: impl Protokollierbar) {
println!("Log (impl Trait): {}", item.nachricht());
}
}
Unter der Haube führt der Rust-Compiler bei beiden Optionen eine Monomorphisierung durch:
- Er analysiert das Programm und ermittelt alle konkreten Typen, mit denen
log_genericoderlog_implaufgerufen werden. - Er generiert für jeden dieser Typen eine eigene Kopie des Funktionscodes im Binärlayout.
- Zur Laufzeit gibt es keine dynamische Typauflösung (Zero-Cost Abstraction). Der Aufruf ist so schnell wie ein normaler, direkter Funktionsaufruf.
Wann Sie impl Trait bevorzugen sollten
impl Trait ist syntaktischer Zucker, der die Signatur von Funktionen drastisch vereinfacht, indem er unnnötiges Rauschen durch Typparameter entfernt. Verwenden Sie es vorzugsweise bei:
- Einfachen Parametern, die nur einmal in der Signatur vorkommen.
- Lokalen Hilfsfunktionen, bei denen die Lesbarkeit im Vordergrund steht.
Wann Sie explizite Trait Bounds (T: Trait) nutzen müssen
Es gibt architektonische Situationen, in denen impl Trait nicht ausreicht und Sie zwingend auf explizite Generics zurückgreifen müssen.
1. Erzwingen identischer Typen für mehrere Parameter
Wenn eine Funktion zwei Argumente erwartet, die denselben konkreten Typ aufweisen müssen, ist das mit impl Trait nicht formulierbar:
#![allow(unused)]
fn main() {
// Hier duerfen 'a' und 'b' unterschiedliche Typen haben, solange beide das Trait erfuellen
fn vergleiche_impl(a: impl Protokollierbar, b: impl Protokollierbar) {
// ...
}
// Hier MÜSSEN 'a' und 'b' exakt denselben konkreten Typ haben
fn vergleiche_generic<T: Protokollierbar>(a: T, b: T) {
// ...
}
}
2. Komplexe Beziehungen und die where-Klausel
Sobald eine Funktion mehrere Typparameter mit verschiedenen Schnittstellenschranken besitzt, wird die Signatur schnell unlesbar. Hier hilft die where-Klausel, die Schnittstellendefinitionen visuell vom Funktionsnamen und den Argumenten zu trennen:
#![allow(unused)]
fn main() {
use std::fmt::Debug;
// Unübersichtliche Inline-Generics:
fn verarbeite_schwer<T: Protokollierbar + Clone, U: Debug + Default>(a: T, b: U) {
// ...
}
// Saubere Strukturierung mittels where-Klausel:
fn verarbeite_sauber<T, U>(a: T, b: U)
where
T: Protokollierbar + Clone,
U: Debug + Default,
{
// ...
}
}
Rückgabetyp impl Trait (Opaque Return Types)
Ein extrem mächtiges Feature von impl Trait ist seine Verwendung als Rückgabetyp. Es erlaubt einer Funktion, einen konkreten Typ zurückzugeben, ohne dessen genauen Namen im Funktionskopf offenzulegen.
Dies ist in zwei Szenarien unverzichtbar:
- Verstecken von Implementierungsdetails: Sie möchten verhindern, dass sich der Aufrufer auf interne Typen verlässt.
- Unbenennbare Typen: Closures und komplexe Iterator-Ketten haben Typnamen, die vom Compiler generiert werden und im Quellcode nicht manuell hingeschrieben werden können.
Betrachten wir ein konkretes Beispiel mit einer Iterator-Kette:
// Ohne impl Trait waere der Rueckgabetyp dieses Iterators extrem komplex:
// FilterMap<Zip<Range<i32>, Cloned<Iter<'_, i32>>>, ...>
fn gerade_zahlen_filtern(daten: &[i32]) -> impl Iterator<Item = i32> + '_ {
daten.iter()
.cloned()
.filter(|x| x % 2 == 0)
}
fn main() {
let zahlen = vec![1, 2, 3, 4, 5, 6];
let gerade = gerade_zahlen_filtern(&zahlen);
for z in gerade {
println!("Gerade: {}", z);
}
}
Wichtige Einschränkung bei Rückgabe von impl Trait:
Eine Funktion, die impl Trait zurückgibt, must im Funktionsrumpf immer denselben konkreten Typ zurückgeben. Sie dürfen nicht abhängig von einer Bedingung unterschiedliche Typen zurückliefern:
#![allow(unused)]
fn main() {
// Dieser Code kompiliert NICHT!
fn erstelle_fehlerhaft(kondition: bool) -> impl Protokollierbar {
struct TypA;
impl Protokollierbar for TypA { fn nachricht(&self) -> String { "A".into() } }
struct TypB;
impl Protokollierbar for TypB { fn nachricht(&self) -> String { "B".into() } }
if kondition {
TypA // Fehler: Rueckgabetypen muessen identisch sein
} else {
TypB
}
}
}
Wenn Sie dynamische Rückgaben zur Laufzeit benötigen, müssen Sie auf Trait-Objekte (Box\<dyn Protokollierbar\>) ausweichen.
Item 38: Verwende Supertraits zur Strukturierung von Schnittstellen-Hierarchien
Rust unterstützt keine klassische Vererbung von Datenstrukturen (Klassenvererbung), ermöglicht jedoch das Definieren von Abhängigkeiten zwischen Schnittstellen über sogenannte Supertraits. Damit können Sie Schnittstellen modular aufbauen und Hierarchien entwerfen, bei denen ein spezialisiertes Trait die Garantien eines allgemeineren Basis-Traits voraussetzt.
Definition und Funktionsweise
Wenn Sie ein Trait definieren, können Sie ein oder mehrere andere Traits als Voraussetzung angeben:
#![allow(unused)]
fn main() {
trait BasisTrait {}
// SpezialisiertesTrait setzt voraus, dass jeder implementierende Typ
// auch BasisTrait implementiert.
trait SpezialisiertesTrait: BasisTrait {}
}
Note
Technisch gesehen handelt es sich hierbei nicht um Vererbung im klassischen OOP-Sinn. Es ist eine Schnittstellen-Einschränkung (Trait Bound) auf der Definitionsebene des Traits selbst. Der Compiler stellt sicher, dass kein Typ
SpezialisiertesTraitimplementieren kann, ohne auchBasisTraitzu implementieren.
Alltagsanalogie: Die Führerscheinklassen
Stellen Sie sich das europäische Führerscheinsystem vor. Um einen Führerschein für Lastkraftwagen (Klasse C) zu erwerben, müssen Sie zwingend den normalen Autoführerschein (Klasse B) besitzen.
Klasse B ist das Supertrait (die fundamentale Voraussetzung), während Klasse C das spezialisierte Trait ist, das zusätzliche Fertigkeiten erfordert. Ein Fahrlehrer darf beim Lkw-Führerschein darauf vertrauen, dass Sie bereits wissen, wie man ein Auto lenkt und Verkehrsregeln beachtet.
Die Praxis: Modellierung einer Fahrzeug-Hierarchie
Wir entwickeln ein System zur Steuerung von Kraftfahrzeugen. Jedes Kraftfahrzeug muss gestartet werden können (Fahrzeug). Ein Personenkraftwagen (Pkw) ist ein spezielles Fahrzeug, das zusätzlich Passagiere aufnehmen kann. Ein Elektro-Pkw (ElektroPkw) ist ein Pkw, der über eine Batterie verfügt und geladen werden muss.
// 1. Das fundamentale Supertrait
trait Fahrzeug {
fn motor_starten(&self);
}
// 2. Das mittlere Trait, das Fahrzeug voraussetzt
trait Pkw: Fahrzeug {
fn passagiere_einsteigen_lassen(&self, anzahl: usize);
}
// 3. Das hochspezialisierte Trait, das Pkw (und damit implizit auch Fahrzeug) voraussetzt
trait ElektroPkw: Pkw {
fn akku_laden(&mut self);
}
// Eine konkrete Struktur
struct ModelY {
akkustand: u8,
}
// Wir MÜSSEN die gesamte Hierarchie implementieren.
// Fehlt eine Implementierung, verweigert der Compiler den Dienst.
impl Fahrzeug for ModelY {
fn motor_starten(&self) {
println!("Model Y initialisiert Bordcomputer. Bereit.");
}
}
impl Pkw for ModelY {
fn passagiere_einsteigen_lassen(&self, anzahl: usize) {
println!("{} Passagiere steigen in das Model Y ein.", anzahl);
}
}
impl ElektroPkw for ModelY {
fn akku_laden(&mut self) {
self.akkustand = 100;
println!("Akku auf 100% geladen.");
}
}
// Eine generische Funktion, die ElektroPkw erfordert
fn fahrzeug_vorbereiten(auto: &mut impl ElektroPkw) {
// Da auto ElektroPkw implementiert, koennen wir Methoden
// ALLER Supertraits aufrufen!
auto.motor_starten(); // Aus Fahrzeug
auto.passagiere_einsteigen_lassen(4); // Aus Pkw
auto.akku_laden(); // Aus ElektroPkw
}
fn main() {
let mut mein_auto = ModelY { akkustand: 20 };
fahrzeug_vorbereiten(&mut mein_auto);
}
Zeilenweise Erklärung der Supertrait-Nutzung:
trait Pkw: Fahrzeug: Das Doppelpunkt-Symbol deklariert die Abhängigkeit. Jeder Typ, derPkwimplementieren möchte, muss auchFahrzeugimplementieren.trait ElektroPkw: Pkw: Hier erweitern wir die Kette. DaPkwvonFahrzeugabhängt, fordertElektroPkwimplizit beide Schnittstellen an.fahrzeug_vorbereiten(auto: &mut impl ElektroPkw): In dieser generischen Funktion können wir nahtlos alle Methoden der Hierarchie auf demauto-Objekt aufrufen. Der Compiler garantiert uns, dass diese Methoden zur Verfügung stehen.
Warum Supertraits nützlich sind
- Modularität: Sie können Schnittstellen in kleine, fokussierte Einheiten aufteilen (z. B.
ReadundWriteausstd::io), anstatt riesige monolithische Schnittstellen zu erstellen. - Logische Abhängigkeiten: Sie zwingen Entwickler, die Semantik Ihrer Software einzuhalten. Beispielsweise setzt das Standard-Trait
Eq(totale Äquivalenz) zwingend das TraitPartialEq(partielle Äquivalenz) voraus.
Item 39: Beherrsche das Zusammenspiel wichtiger Standard-Traits (wie Clone, Copy, Drop, Default, From & Into)
Die Rust-Standardbibliothek stellt eine Reihe von fundamentalen Schnittstellen bereit, die tief in die Sprache integriert sind. Die korrekte Implementierung dieser Traits entscheidet darüber, ob sich Ihre eigenen Typen natürlich und ergonomisch in das Ökosystem einfügen.
Clone vs. Copy: Der fundamentale Unterschied
Clonerepräsentiert die Fähigkeit zur expliziten Wertvervielfältigung. Die Methodeclonekann beliebig teuer sein (z. B. das Allokieren von neuem Heap-Speicher und Kopieren aller Elemente eines Vektors).Copyist ein Marker-Trait (es enthält keine Methoden). Es teilt dem Compiler mit, dass der Typ durch eine einfache, billige Bit-Kopie (wiememcpyim RAM) vervielfältigt werden darf. Wenn ein TypCopyimplementiert, ändert sich die Semantik der Zuweisung: Statt eines Besitzwechsels (Move) findet eine implizite Kopie statt.
Warum Copy und Drop sich gegenseitig ausschließen
Das wichtigste Gesetz im Zusammenspiel dieser Traits lautet:
Caution
Ein Typ darf niemals gleichzeitig
CopyundDropimplementieren.
Begründung über das Speicher-Layout:
Das Drop-Trait wird implementiert, um Ressourcen beim Verlassen des Gültigkeitsbereichs (Out of Scope) sauber freizugeben (z. B. Schließen eines Dateihandles, Freigabe von Heap-Speicher).
Würde ein solcher Typ Copy implementieren, würde bei jeder Zuweisung eine bitweise Kopie der Struktur im Speicher erstellt. Wir hätten dann zwei unabhängige Instanzen, die denselben Zeiger auf denselben Heap-Speicher oder dasselbe Dateihandle besitzen. Am Ende des Gültigkeitsbereichs würde für beide Instanzen der Destruktor drop aufgerufen. Dies führt unweigerlich zu einem Double-Free-Fehler (doppelte Speicherfreigabe), was zu Speicherkorruption führt und die Garantien von Rust bricht.
Alltagsanalogie für Copy vs. Drop
Stellen Sie sich ein Kinoticket vor:
- Wenn Sie das Ticket an einen Freund weitergeben, können Sie es kopieren (wenn es ein E-Ticket als PDF ist – das entspricht
Copy). Sie haben nun zwei gültige Tickets. - Wenn Sie jedoch einen physischen Schlüssel zu einem Schließfach besitzen (eine Ressource, die verwaltet wird), können Sie diesen nicht einfach fotokopieren und erwarten, dass zwei Schließfächer existieren. Der Schlüssel repräsentiert exklusiven Besitz. Wenn Sie fertig sind, müssen Sie den Schlüssel in den Rückgabekasten werfen (
Drop). Gäbe es eine magische Kopie des Schlüssels, würden zwei Personen versuchen, dasselbe Schließfach zu öffnen und zu leeren, was zu Chaos (Speicherfehlern) führt.
Die Praxis: Speicherverwaltung und Konvertierungen
Wir demonstrieren das Zusammenspiel anhand einer benutzerdefinierten Ressource, die Drop und Default implementiert, sowie der Implementierung von From zur Typkonvertierung.
// 1. Eine Ressource, die Heap-Speicher verwaltet und somit Drop benoetigt
struct DatenbankVerbindung {
verbindungs_string: String,
}
// Default-Trait fuer einen sinnvollen Standard-Startwert
impl Default for DatenbankVerbindung {
fn default() -> Self {
DatenbankVerbindung {
verbindungs_string: String::from("localhost:5432"),
}
}
}
// Drop-Trait zur Ressourcenfreigabe
impl Drop for DatenbankVerbindung {
fn drop(&mut self) {
println!("Verbindung zu {} wird geschlossen.", self.verbindungs_string);
}
}
// Der Versuch, hier Copy zu implementieren, scheitert am Compiler!
// #[derive(Clone, Copy)] // <- Fuehrt zu: "the trait `Copy` may not be implemented for this type"
// 2. Konvertierungs-Traits: From & Into
// Wir implementieren From, um eine saubere Konvertierung von &str zu ermoeglichen
impl From<&str> for DatenbankVerbindung {
fn from(adresse: &str) -> Self {
DatenbankVerbindung {
verbindungs_string: adresse.to_string(),
}
}
}
fn main() {
// Verwendung des Default-Traits
let standard_verbindung = DatenbankVerbindung::default();
println!("Standard-Datenbank: {}", standard_verbindung.verbindungs_string);
// Verwendung des From-Traits zur Konvertierung
let server_verbindung = DatenbankVerbindung::from("192.168.1.100:3306");
// Da wir From implementiert haben, koennen wir auch Into nutzen!
// Die Typ-Annotation `: DatenbankVerbindung` ist notwendig, damit Rust weiß,
// in welchen Zieltyp konvertiert werden soll.
let cloud_verbindung: DatenbankVerbindung = "cloud-db:5432".into();
println!("Cloud-Datenbank: {}", cloud_verbindung.verbindungs_string);
// Am Ende der main-Funktion verlassen alle Verbindungen den Scope.
// Der Compiler ruft automatisch fuer jede Verbindung die drop-Methode auf!
}
Zeilenweise Erklärung des Codes:
impl Default for DatenbankVerbindung: Wir definieren eine Standardverbindung zulocalhost:5432. Dies ermöglicht die Nutzung vonDatenbankVerbindung::default().impl Drop for DatenbankVerbindung: Wir überschreiben das Verhalten beim Löschen des Typs. Wenn eine Instanz ungültig wird, gibt sie eine Protokollnachricht aus. In realem Code würden hier Netzwerkverbindungen getrennt oder Sockets geschlossen werden.impl From<&str> for DatenbankVerbindung: Wir definieren die Konvertierung von einem String-Slice (&str) in unseren Verbindungstyp.let cloud_verbindung: DatenbankVerbindung = "cloud-db:5432".into();: Durch die Implementierung vonFromhat uns der Compiler automatisch das GegenstückIntogeneriert. Wir können die Konvertierung sehr ergonomisch über.into()aufrufen.
Best Practice für Konvertierungen
Implementieren Sie immer das From-Trait für Ihre Typen, wenn eine verlustfreie Konvertierung möglich ist. Dadurch erhalten Sie das Into-Trait völlig kostenfrei. Verwenden Sie in Funktionssignaturen hingegen vorzugsweise Into als Schranke, um dem Aufrufer maximale Flexibilität bei den Argumenten zu bieten:
// Diese Funktion akzeptiert alles, was sich in eine DatenbankVerbindung umwandeln laesst
fn datenbank_testen(verbindung: impl Into<DatenbankVerbindung>) {
let db = verbindung.into();
println!("Teste Verbindung zu: {}", db.verbindungs_string);
}
fn main_test() {
// Wir koennen einen &str uebergeben - die Konvertierung geschieht intern!
datenbank_testen("test-db:5432");
}