Einleitung: Die Falle, in die alle Rust-Anfänger tappen
\n\nSie lernen Rust. Sie haben das Konzept von Ownership und Borrowing bereits verstanden. Sie schreiben Ihre erste ernsthafte Funktion und denken: „Aha, um den String nicht zu verschieben, übergebe ich eine Referenz!“. Und Sie schreiben:
\n\nfn print_string(s: &String) {\n println!("{}", s);\n}\n\nDer Compiler ist glücklich. Der Code funktioniert. Sie sind glücklich. Aber das Problem: Ein erfahrener Rust-Entwickler wird bei Ihrem Code das Gesicht verziehen. Warum? Es funktioniert doch alles!
\n\nDiese Frage ist eine der beliebtesten auf StackOverflow. Tausende Entwickler stolpern über dieses scheinbar unbedeutende Detail. Heute werden wir verstehen, warum &String, &Vec und &Box in der Funktionssignatur eine rote Flagge sind und wie man es richtig macht.
Problem Nr. 1: Implizite Typeinschränkung (Type Coercion)
\n\nRust hat ein leistungsstarkes System zur automatischen Typumwandlung, aber nur in eine Richtung. Dies nennt man Deref Coercion (Umwandlung durch Dereferenzierung).
\n\nDie Idee ist einfach: Wenn Sie eine Referenz auf String haben, kann Rust diese automatisch in eine Referenz auf &str umwandeln. Gleiches gilt für &Vec<T> → &[T] und &Box<T> → &T.
Sehen Sie den Unterschied:
\n\n// Schlecht: akzeptiert nur &String\nfn bad(s: &String) {\n println!("{}", s);\n}\n\n// Gut: akzeptiert &str\nfn good(s: &str) {\n println!("{}", s);\n}\n\nfn main() {\n let owned = String::from("Hallo, Welt!");\n let slice = "Hallo, Slice!";\n \n bad(&owned); // Funktioniert\n // bad(slice); // FEHLER! slice ist &str, nicht &String\n \n good(&owned); // Funktioniert (automatische Umwandlung)\n good(slice); // Funktioniert (es ist bereits &str)\n}\n\nSehen Sie? Die Funktion good akzeptiert jeden String: sowohl den Besitzer String als auch das String-Literal &str und sogar einen Teil eines Strings. Die Funktion bad akzeptiert nur String. Das ist eine unnötige Einschränkung.
Praktisches Beispiel: Substring-Suche
\n\n// Eingeschränkte Suche\nfn find_in_string(haystack: &String, needle: &str) -> bool {\n haystack.contains(needle)\n}\n\n// Universelle Suche\nfn find_in_str(haystack: &str, needle: &str) -> bool {\n haystack.contains(needle)\n}\n\nfn main() {\n let my_string = String::from("Rust ist großartig");\n let my_str = "Rust ist großartig";\n \n // Funktioniert nur mit String\n find_in_string(&my_string, "Rust");\n // find_in_str(my_str, "Rust"); // Compiler-Fehler!\n \n // Funktioniert mit beidem\n find_in_str(&my_string, "Rust");\n find_in_str(my_str, "Rust");\n}\n\nProblem Nr. 2: Performance und unnötige Indirektionen
\n\nSchauen wir uns die „Motorhaube“ an. Was ist ein String im Speicher? Es ist eine Struktur mit drei Feldern:
- \n
- Zeiger auf die Daten im Heap \n
- Länge des Strings \n
- Kapazität (capacity) \n
Wenn Sie &String übergeben, übergeben Sie eine Referenz auf diese Struktur. Um zu den Daten zu gelangen, muss der Compiler:
- \n
&Stringdereferenzieren → dieString-Struktur erhalten \n - Aus der Struktur den Zeiger auf die Daten im Heap nehmen \n
- Diesen Zeiger dereferenzieren → die String-Bytes erhalten \n
Wenn Sie &str übergeben, übergeben Sie einen Fat Pointer, der nur aus zwei Feldern besteht:
- \n
- Zeiger auf die Daten \n
- Länge \n
Der Compiler muss nur einen Schritt machen – den Zeiger dereferenzieren. Weniger Indirektion bedeutet schnellerer Code. Der Unterschied ist mikroskopisch, aber in heißen Schleifen oder bei der Übergabe an