Das Eigentumssystem von Rust beherrschen: Der vollständige Leitfaden zur Speichersicherheit ohne Garbage Collection
1. Warum Eigentum wichtig ist: Das 2-Billionen-Dollar-Problem
Speichersicherheitsfehler kosten die Softwareindustrie jährlich schätzungsweise 2 Billionen US-Dollar. Microsoft berichtet, dass 70 % ihrer Sicherheitslücken Probleme mit der Speichersicherheit sind. Das Chrome-Team von Google hat ähnliche Zahlen ermittelt. Zu diesen Fehlern gehören:
- Use-after-free: Zugriff auf Speicher, dessen Zuordnung aufgehoben wurde
- Doppelt frei: Speicher wird zweimal freigegeben, was zu Beschädigungen führt
- Pufferüberlauf: Schreiben über den zugewiesenen Speicher hinaus
- Datenrennen: Gleichzeitiger Zugriff führt zu unvorhersehbarem Verhalten
- Speicherlecks: Vergessen, zugewiesenen Speicher freizugeben
Der traditionelle Kompromiss
Programmiersprachen haben in der Vergangenheit einen von zwei Ansätzen gewählt:
Ansatz 1: Manuelle Speicherverwaltung (C/C++) „c // C-Code – fehleranfällig char* create_message() { char* msg = malloc(100); strcpy(msg, "Hallo"); Rückgabenachricht; // Anrufer muss daran denken, freizugeben! }
void-Prozess() { char* m = create_message(); printf("%s", m); // Free(m) vergessen - Speicherverlust! } „ Probleme: Erfordert absolute Disziplin, leicht Fehler zu machen, Sicherheitslücken.
Ansatz 2: Garbage Collection (Java/Go/JavaScript) „Java // Java-Code – sicher, aber mit Laufzeit-Overhead String createMessage() { „Hallo“ zurückgeben; // GC wird irgendwann aufräumen } // Sicher, aber GC-Pausen beeinträchtigen die Leistung „ Probleme: Unvorhersehbare Pausen, Speicheraufwand, weniger Kontrolle über die Leistung.
Rusts revolutionäre Lösung
Rust bietet einen dritten Weg: Speichersicherheit ohne Garbage Collection durch Überprüfung der Besitzverhältnisse zur Kompilierungszeit. Sie erhalten:
- ✅ Speichersicherheit zur Kompilierungszeit garantiert
- ✅ Kein Laufzeit-Overhead (kostenlose Abstraktionen)
- ✅ Keine Garbage Collector-Pausen
- ✅ Furchtlose Parallelität (Datenrennen unmöglich)
- ✅ Vorhersehbare Leistung
Der Haken? Sie müssen das Eigentumssystem erlernen. Dieser Leitfaden wird es kristallklar machen.
2. Die drei goldenen Regeln des Eigentums
Jedes Rust-Programm befolgt diese drei Regeln, die zur Kompilierungszeit durchgesetzt werden:
Regel 1: Jeder Wert hat einen einzigen Besitzer
„Rost.“ fn main() { let s = String::from("hello"); // s besitzt den String // Diese Daten können jeweils nur einer Variablen gehören } // s verlässt den Gültigkeitsbereich, Speicher wird automatisch freigegeben „
Regel 2: Wenn der Eigentümer den Gültigkeitsbereich verlässt, wird der Wert gelöscht
„Rost.“ fn main() { { let s = String::from("hello"); // s ist ab hier gültig // Sachen mit s machen } // s verlässt den Gültigkeitsbereich und wird gelöscht, wodurch Speicher freigegeben wird
// println!("{}", s); // FEHLER: s existiert nicht mehr
} „
Regel 3: Sie können entweder eine veränderliche Referenz ODER mehrere unveränderliche Referenzen haben
„Rost.“ fn main() { let mut s = String::from("hello");
// Mehrere unveränderliche Referenzen - OK
sei r1 = &s;
sei r2 = &s;
println!("{} und {}", r1, r2);
// Eine veränderbare Referenz - OK (nach r1 werden r2 nicht mehr verwendet)
sei r3 = &mut s;
r3.push_str(" world");
println!("{}", r3);
} „
Warum diese Regeln?
- Regel 1 und 2: Verhindert Speicherlecks und Double-Free-Fehler
- Regel 3: Verhindert Datenrennen zur Kompilierungszeit
3. Bewegungssemantik: Eigentumsübertragung verstehen
Das Problem: Naives Kopieren ist teuer
„Rost.“ fn main() { let s1 = String::from("hello"); sei s2 = s1; // Was passiert hier?
// println!("{}", s1); // FEHLER: Wert wurde nach s2 verschoben
} „
Was tatsächlich passiert:
„ Stapel: Heap: s1 -> [ptr, len, cap] -> „Hallo“-Daten | | (bewegen) v s2 -> [ptr, len, cap] -> (gleiche Heap-Daten) „
Rust verschiebt den Besitz, anstatt Heap-Daten zu kopieren. Nach „let s2 = s1“ ist nur „s2“ gültig. Dies verhindert:
- Standardmäßig teure tiefe Kopien
- Double-Free-Fehler (nur s2 gibt den Speicher frei)
Wann kopiert Rust statt zu verschieben?
Typen, die das Merkmal „Kopieren“ implementieren, werden kopiert statt verschoben:
„Rost.“ fn main() { // Ganzzahlen, Floats, Bools, Zeichen implementieren Copy sei x = 5; sei y = x; // x wird nach y kopiert println!("x = {}, y = {}", x, y); // Beides gültig!
// Tupel von Copy-Typen sind auch Copy
let point = (3, 4);
sei Punkt2 = Punkt; // kopiert
println!("{:?} und {:?}", point, point2); // Beides gültig!
} „ Faustregel: Wenn ein Typ Daten auf dem Heap speichert oder Ressourcen besitzt, implementiert er „Kopieren“ nicht.
Explizites Klonen, wenn Sie es brauchen
„Rost.“ fn main() { let s1 = String::from("hello"); sei s2 = s1.clone(); // Explizite tiefe Kopie
println!("s1 = {}, s2 = {}", s1, s2); // Beide gültig
} „
Klonen verwenden, wenn:
- Sie benötigen unabhängige Kopien
- Leistungskosten sind akzeptabel
- Macht die Absicht deutlich
Klonen vermeiden, wenn:
- Sie können eine Umstrukturierung durchführen, um stattdessen Kredite zu nutzen
- Leistung ist entscheidend
- Arbeiten in Hot Loops
4. Ausleihen: Referenzen, die man nicht besitzt
Durch Ausleihen können Sie auf Daten verweisen, ohne Eigentümer zu werden.
Unveränderliche Referenzen (&T)
„Rost.“ fn main() { let s = String::from("hello");
let len = berechne_länge(&s); // s ausleihen
println!("Länge von '{}' ist {}", s, len); // ist noch gültig!
}
fn berechne_länge(s: &String) -> usize { s.len() } // s verlässt den Gültigkeitsbereich, besitzt aber nicht die Daten, sodass nichts passiert „
Wichtige Punkte:
- „&s“ erstellt einen Verweis auf s
- Referenzen sind standardmäßig unveränderlich
- Mehrere unveränderliche Referenzen erlaubt
- Der ursprüngliche Besitzer kann die Daten weiterhin lesen
Veränderbare Referenzen (&mut T)
„Rost.“ fn main() { let mut s = String::from("hello");
change(&mut s); // s veränderlich ausleihen
println!("{}", s); // Gibt „Hallo, Welt“ aus
}
fn change(s: &mut String) { s.push_str(", world"); } „
Kritische Einschränkung: Nur eine veränderbare Referenz gleichzeitig!
„Rost.“ fn main() { let mut s = String::from("hello");
sei r1 = &mut s;
sei r2 = &mut s; // FEHLER: Kann nicht mehr als einmal als veränderlich ausgeliehen werden
println!("{}, {}", r1, r2);
} „
Warum? Verhindert Datenrennen zur Kompilierungszeit: „Rost.“ // Das ist in Rust unmöglich (würde in C++ kompiliert) let mut data = vec![1, 2, 3]; let ref1 = &mut data; let ref2 = &mut data; ref1.push(4); // Könnte neu zugewiesen werden ref2.push(5); // Könnte zu Use-After-Free führen! „
Das Geheimnis des Borrow Checkers: Non-Lexical Lifetimes (NLL)
Modern Rust (Ausgabe 2018+) ist intelligenter, wenn Referenzen enden:
„Rost.“ fn main() { let mut s = String::from("hello");
sei r1 = &s;
sei r2 = &s;
println!("{} und {}", r1, r2);
// r1 und r2 werden ab diesem Zeitpunkt nicht mehr verwendet
sei r3 = &mut s; // OK! r1 und r2 sind „tot“
r3.push_str(" world");
println!("{}", r3);
} „
Vor NLL (Ausgabe 2015) konnte dies nicht kompiliert werden. Jetzt verfolgt der Compiler, wo Referenzen tatsächlich verwendet werden, nicht nur ihren lexikalischen Umfang.
Gemeinsames Ausleihmuster: Mehrere Lesevorgänge, einzelner Schreibvorgang
„Rost.“ fn main() { let mut data = vec![1, 2, 3, 4, 5];
// Lesephase: mehrere unveränderliche Ausleihen
let first = &data[0];
let last = &data[data.len() - 1];
println!("first: {}, last: {}", first, last);
// Schreibphase: exklusives veränderliches Ausleihen
data.push(6);
println!("Aktualisiert: {:?}", data);
} „
5. Lebenszeiten: Dem Compiler etwas über Referenzen beibringen
Das Problem, das Lebenszeiten lösen
„Rost.“ fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x // Welche Lebensdauer soll die Rendite haben? } sonst { y // x's Lebensdauer oder y's Lebensdauer? } } „
Der Compiler kann nicht feststellen, ob die zurückgegebene Referenz gültig ist:
„Rost.“ fn main() { let string1 = String::from("long string"); resultieren lassen;
{
let string2 = String::from("short");
result = longest(&string1, &string2);
} // string2 hier abgelegt
// Ist das Ergebnis gültig? Hängt davon ab, welche Eingabe zurückgegeben wurde!
} „
Lebenslange Anmerkungen: Explizite Verträge
„Rost.“ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } sonst { j } } „
Lesen Sie dies: „Die zurückgegebene Referenz ist gültig, solange sowohl x als auch y gültig sind.“
„Rost.“ fn main() { let string1 = String::from("long string"); resultieren lassen;
{
let string2 = String::from("short");
result = longest(&string1, &string2);
println!("{}", Ergebnis); // OK: beides noch gültig
} // string2 gelöscht
// println!("{}", result); // FEHLER: string2 wurde möglicherweise zurückgegeben
} „
Lifetime Elision: Wenn Sie keine Anmerkungen benötigen
Der Compiler kann Lebensdauern in gängigen Mustern ableiten:
„Rost.“ // Keine Anmerkung erforderlich – Lebensdauer einer einzelnen Eingabe fn first_word(s: &str) -> &str { s.split_whitespace().next().unwrap_or("") }
// Der Compiler sieht dies als: fn first_word<'a>(s: &'a str) -> &'a str { s.split_whitespace().next().unwrap_or("") } „
Elision-Regeln:
- Jede Eingabereferenz erhält ihre eigene Lebensdauer
- Bei genau einer Eingabelebensdauer erhält die Ausgabe diese Lebensdauer
- Bei einer Methode mit „&self“ erhält die Ausgabe die Lebensdauer von „self“.
Lebensdauern in Strukturen
Wenn Strukturen Referenzen enthalten, müssen Sie Lebensdauern mit Anmerkungen versehen:
„Rost.“ struct ImportantExcerpt<'a> { Teil: &'a str, }
fn main() { let novel = String::from("Nennen Sie mich Ishmael. Vor einigen Jahren..."); let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
Teil: erster_Satz,
};
println!("{}", excerpt.part);
} // Auszug und Roman gelöscht, alles gut „
Bedeutung: Ein „ImportantExcerpt“ kann die Daten, auf die er verweist, nicht überleben.
„Rost.“ fn main() { auszug lassen;
{
let novel = String::from("Nenn mich Ishmael.");
Auszug = ImportantExcerpt {
Teil: &Roman,
};
} // FEHLER: Roman wurde gelöscht, Auszugsteil blieb hängen
// println!("{}", excerpt.part);
} „
Die 'statische Lebensdauer
„Statisch“ bedeutet „lebt für die gesamte Programmdauer“:
„Rost.“ // String-Literale haben eine 'statische Lebensdauer' let s: &'static str = "Ich bin in der Binärdatei gespeichert";
// Statische Variablen static GLOBAL: &str = "Auch 'statisch"; „
Häufiger Fehler: Verwenden Sie „statisch“ nicht nur, um Fehler zu beseitigen!
„Rost.“ // SCHLECHT: 'static zum Kompilieren erzwingen fn bad_function() -> &'static str { let s = String::from("hello"); // &s // Kann nicht zurückkehren – lebt nicht lange genug // Speicherverlust, um „Statik ist falsch!“ zu erhalten. }
// GUT: Stattdessen eigene Daten zurückgeben fn good_function() -> String { String::from("Hallo") } „
6. Häufige Fallstricke und wie man sie behebt
Fallstrick 1: Kann nicht als veränderlich ausgeliehen werden, da es bereits ausgeliehen wurde
„Rost.“ // FEHLER fn main() { let mut vec = vec![1, 2, 3];
let first = &vec[0]; // Unveränderlicher Kredit
vec.push(4); // FEHLER: veränderlicher Kredit
println!("{}", first);
} „
Warum es fehlschlägt: „push“ wird möglicherweise neu zugewiesen, wodurch „first“ ungültig wird.
Lösung 1: Umstrukturierung zur Trennung der Kredite „Rost.“ fn main() { let mut vec = vec![1, 2, 3];
let first_value = vec[0]; // Kopiere den Wert
vec.push(4); // OK: keine ausstehenden Kredite
println!("{}", erster_Wert);
} „
Lösung 2: Klonen Sie die Daten vor der Mutation „Rost.“ fn main() { let mut vec = vec![1, 2, 3];
let first = vec.get(0).cloned(); // Option<i32>, kein Ausleihen
vec.push(4);
if let Some(val) = first {
println!("{}", val);
}
} „
Fallstrick 2: Referenz auf lokale Variable kann nicht zurückgegeben werden
„Rost.“ // FEHLER fn create_string() -> &String { let s = String::from("hello"); &s // FEHLER: Gibt einen Verweis auf Daten zurück, die der Funktion gehören } // s wird hier abgelegt, würde einen baumelnden Zeiger zurückgeben! „
Lösung: Eigene Daten zurückgeben „Rost.“ fn create_string() -> String { String::from("hello") // Eigentum an den Aufrufer übertragen } „
Fallstrick 3: Ausgeliehene Inhalte können nicht entfernt werden
„Rost.“ // FEHLER fn main() { let vec = vec![String::from("a"), String::from("b")]; let first = &vec;
let take = vec[0]; // FEHLER: Der indizierte Inhalt kann nicht verlassen werden
} „
Lösung 1: Klonen Sie den Wert „Rost.“ fn main() { let vec = vec![String::from("a"), String::from("b")]; let take = vec[0].clone(); println!("{}", genommen); } „
Lösung 2: Verwenden Sie Methoden, die das Eigentum übertragen „Rost.“ fn main() { let mut vec = vec![String::from("a"), String::from("b")]; let take = vec.swap_remove(0); // Übernimmt den Besitz println!("{}", genommen); } „
Fallstrick 4: Gleichzeitige veränderliche und unveränderliche Kredite
„Rost.“ // FEHLER fn main() { let mut map = HashMap::new(); map.insert("key", "value");
let value = map.get("key");
map.insert("key2", "value2"); // FEHLER: Kann im geliehenen Zustand nicht mutieren
println!("{:?}", value);
} „
Lösung: Verwenden Sie die Eingabe-API „Rost.“ fn main() { let mut map = HashMap::new(); map.insert("key", "value");
map.entry("key2").or_insert("value2"); // Keine widersprüchlichen Kredite
if let Some(value) = map.get("key") {
println!("{}", value);
}
} „
Fallstrick 5: Lebenszeitinkongruenz in Strukturen
„Rost.“ // FEHLER struct Container { Daten: &str, // FEHLER: Lebenszeitanmerkung fehlt } „
Lösung: Lebensdauerparameter hinzufügen „Rost.“ struct Container<'a> { Daten: &'a str, }
impl<'a> Container<'a> { fn new(text: &'a str) -> Self { Container { Daten: Text } }
fn get_data(&self) -> &str {
Selbstdaten
}
} „
Fallstrick 6: Iterator-Invalidierung
„Rost.“ // FEHLER fn main() { let mut vec = vec![1, 2, 3, 4, 5];
für i in &vec {
if *i % 2 == 0 {
vec.push(*i * 2); // FEHLER: Kann während der Iteration nicht geändert werden
}
}
} „
Lösung: Zuerst Indizes sammeln „Rost.“ fn main() { let mut vec = vec![1, 2, 3, 4, 5];
let to_add: Vec<i32> = vec.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.collect();
vec.extend(to_add);
println!("{:?}", vec);
} „
7. Fortgeschrittene Muster: Über das grundlegende Eigentum hinaus
Muster 1: Innere Veränderlichkeit mit RefCell
Manchmal müssen Sie Daten nur mit einer unveränderlichen Referenz ändern:
„Rost.“ benutze std::cell::RefCell;
struct Logger { logs: RefCell<Vec<String>>, // Kann durch &self mutieren }
impl Logger { fn new() -> Self { Logger { Protokolle: RefCell::new(Vec::new()), } }
fn log(&self, message: &str) { // Nimmt &self, nicht &mut self
self.logs.borrow_mut().push(message.to_string());
}
fn print_logs(&self) {
zum Einloggen self.logs.borrow().iter() {
println!("{}", log);
}
}
}
fn main() { let logger = Logger::new(); logger.log("Erste Nachricht"); logger.log("Zweite Nachricht"); logger.print_logs(); } „
Wann zu verwenden:
- Implementierung von Caches oder Loggern
- Graphen- oder Baumstrukturen mit innerer Veränderlichkeit
- Scheinobjekte in Tests
Vorsicht: Runtime-Kreditprüfung – Panik bei Regelverstoß!
„Rost.“ let cell = RefCell::new(5); let Borrowed1 = cell.borrow(); let Borrowed2 = cell.borrow_mut(); // PANIK: schon ausgeliehen! „
Muster 2: Referenzzählung mit Rc
Teilen Sie das Eigentum an Daten mit mehreren Eigentümern:
„Rost.“ benutze std::rc::Rc;
Strukturknoten { Wert: i32, Kinder: Vec<Rc<Node>>, }
fn main() { let leaf = Rc::new(Node { Wert: 3, Kinder: vec![], });
let branch1 = Rc::new(Node {
Wert: 1,
Kinder: vec![Rc::clone(&leaf)], // Geteilter Besitz
});
let branch2 = Rc::new(Node {
Wert: 2,
Kinder: vec![Rc::clone(&leaf)], // Beide Zweige besitzen ein Blatt
});
println!("Blattreferenzanzahl: {}", Rc::strong_count(&leaf)); // 3
} „
Wichtige Punkte:
- Nicht Thread-sicher (verwenden Sie „Arc“ für Threads)
- Die Referenzzählung verursacht einen Overhead
- Erstellt Zyklen, wenn man nicht aufpasst (verwende „Schwach“, um Zyklen zu unterbrechen)
Muster 3: Kombination von Rc und RefCell
Mehrere Besitzer mit Mutation:
„Rost.“ benutze std::rc::Rc; benutze std::cell::RefCell;
#[ableiten(Debug)] struct SharedCounter { count: Rc<RefCell>, }
impl SharedCounter { fn new() -> Self { SharedCounter { count: Rc::new(RefCell::new(0)), } }
fn inkrementieren(&self) {
*self.count.borrow_mut() += 1;
}
fn get(&self) -> i32 {
*self.count.borrow()
}
}
fn main() { let counter1 = SharedCounter::new(); let counter2 = SharedCounter { count: Rc::clone(&counter1.count), };
counter1.increment();
counter2.increment();
println!("Count: {}", counter1.get()); // 2
} „
Muster 4: Builder-Muster mit Eigentum
„Rost.“ struct Server { Host: String, Hafen: u16, Zeitüberschreitung: u64, }
struct ServerBuilder { host: Option<String>, Port: Option, Zeitüberschreitung: Option, }
impl ServerBuilder { fn new() -> Self { ServerBuilder { Gastgeber: Keine, Port: Keiner, Zeitüberschreitung: Keine, } }
fn host(mut self, host: impl Into<String>) -> Self {
self.host = Some(host.into());
self // Besitz zurück verschieben
}
fn port(mut self, port: u16) -> Self {
self.port = Some(port);
selbst
}
fn timeout(mut self, timeout: u64) -> Self {
self.timeout = Some(timeout);
selbst
}
fn build(self) -> Result<Server, &'static str> {
Ok(Server {
host: self.host.ok_or("Host ist erforderlich")?,
Port: self.port.unwrap_or(8080),
Zeitüberschreitung: self.timeout.unwrap_or(30),
})
}
}
fn main() { let server = ServerBuilder::new() .host("localhost") .port(3000) .timeout(60) .build() .unwrap();
println!("Server: {}:{}", server.host, server.port);
} „
Muster 5: RAII (Ressourcenbeschaffung ist Initialisierung)
Der Besitz ermöglicht die automatische Ressourcenbereinigung:
„Rost.“ benutze std::fs::File; use std::io::{self, Write};
struct LogFile { Datei: Datei, }
impl LogFile { fn new(path: &str) -> io::Result<Self> { Ok(LogFile { Datei: Datei::erstellen(Pfad)?, }) }
fn write_log(&mut self, message: &str) -> io::Result<()> {
writeln!(self.file, „{}“, Nachricht)
}
}
impl Drop für LogFile { fn drop(&mut self) { println!("Protokolldatei schließen"); // Datei wird beim Löschen automatisch geschlossen } }
fn main() -> io::Result<()> { { let mut log = LogFile::new("app.log"?; log.write_log("Anwendung gestartet"?; log.write_log("Daten werden verarbeitet"?; } // Datei wird hier automatisch per Drop geschlossen
println!("Protokolldatei automatisch geschlossen");
Ok(())
} „
8. Reale Refactoring-Strategien
Szenario 1: Übergabe von Daten an Funktionen
Vorher (Kampf gegen den Kreditprüfer): „Rost.“ struct Benutzer { Name: Zeichenfolge, E-Mail: String, }
fn process_user(Benutzer: Benutzer) { println!("Processing {}", user.name); }
fn main() { let user = User { Name: String::from("Alice"), E-Mail: String::from("[email protected]"), };
Process_user(Benutzer);
// println!("{}", user.name); // FEHLER: Benutzer verschoben
} „
Nachher (ausleihen statt verschieben): „Rost.“ fn process_user(user: &User) { // Stattdessen ausleihen println!("Processing {}", user.name); }
fn main() { let user = User { Name: String::from("Alice"), E-Mail: String::from("[email protected]"), };
Process_user(&user);
println!("{}", user.name); // OK: Benutzer ist immer noch im Besitz
} „
Szenario 2: Arbeiten mit Sammlungen
Vorher: „Rost.“ fn get_first_name(users: Vec<User>) -> Option<String> { Users.first().map(|u| u.name.clone()) // Unnötiger Klon }
fn main() { letuser = vec![/* ... */]; let name = get_first_name(users); // Benutzer können nicht mehr verwendet werden – verschoben } „
Nachher: „Rost.“ fn get_first_name(users: &[User]) -> Option<&str> { users.first().map(|u| u.name.as_str()) }
fn main() { letuser = vec![/* ... */]; let name = get_first_name(&users); // Benutzer weiterhin verwendbar } „
Szenario 3: Struktur mit mehreren String-Feldern
Vorher (viel Klonen): „Rost.“ fn build_full_name(first: String, last: String) -> String { format!("{} {}", erster, letzter) }
fn main() { let first = String::from("John"); let last = String::from("Doe");
let full = build_full_name(first.clone(), last.clone());
println!("First: {}, Last: {}", first, last);
} „
Nachher (String-Slices verwenden): „Rost.“ fn build_full_name(first: &str, last: &str) -> String { format!("{} {}", erster, letzter) }
fn main() { let first = String::from("John"); let last = String::from("Doe");
let full = build_full_name(&first, &last);
println!("First: {}, Last: {}", first, last);
} „
Szenario 4: Ergebnisse zwischenspeichern
Problem: Veränderbarer Cache mit unveränderlichen Methoden erforderlich
Lösung: Innere Veränderlichkeit „Rost.“ benutze std::cell::RefCell; verwenden Sie std::collections::HashMap;
struct ExpensiveCalculator { Cache: RefCell<HashMap<i32, i32>>, }
impl ExpensiveCalculator { fn new() -> Self { Teuerrechner { Cache: RefCell::new(HashMap::new()), } }
fn berechnen(&self, input: i32) -> i32 { // &self, nicht &mut self
// Cache prüfen
if let Some(&cached) = self.cache.borrow().get(&input) {
zwischengespeichert zurückgeben;
}
// Teure Berechnung
let result = input * input;
// Im Cache speichern
self.cache.borrow_mut().insert(input, result);
Ergebnis
}
}
fn main() { let calc = ExpensiveCalculator::new(); println!("{}", calc.calculate(5)); // Berechnet println!("{}", calc.calculate(5)); // Aus dem Cache } „
Szenario 5: Baumstrukturen
Herausforderung: Eltern-Kind-Beziehungen führen zu Kreditkonflikten
Lösung: Indizes oder Rc/Weak verwenden „Rost.“ use std::rc::{Rc, Weak}; benutze std::cell::RefCell;
Strukturknoten { Wert: i32, übergeordnetes Element: RefCell<Weak<Node>>, Kinder: RefCell<Vec<Rc<Node>>>, }
impl-Knoten { fn new(value: i32) -> Rc<Self> { Rc::new(Knoten { Wert, übergeordnetes Element: RefCell::new(Weak::new()), Kinder: RefCell::new(vec![]), }) } fn add_child(parent: &Rc<Node>, child: Rc<Node>) { *child.parent.borrow_mut() = Rc::downgrade(parent); parent.children.borrow_mut().push(child); } }
fn main() { let root = Node::new(1); let child1 = Node::new(2); let child2 = Node::new(3);
Node::add_child(&root, child1);
Node::add_child(&root, child2);
println!("Root hat {} Kinder", root.children.borrow().len());
} „
9. Auswirkungen auf die Leistung: Kostenfreie Abstraktionen
Das Eigentum ist kostenlos
Das Eigentümersystem hat keinen Laufzeit-Overhead:
„Rost.“ // Dieser Rust-Code: fn-Prozess(Daten: Vec) -> i32 { data.iter().sum() }
// Kompiliert zur gleichen Assembly wie: // int Process(int* data, size_t len) { // int sum = 0; // for (size_t i = 0; i < len; i++) { // sum += data[i]; // } // Summe zurückgeben; // } „
Beweis: Überprüfen Sie die Baugruppe mit „cargo build --release“ und Tools wie „cargo-asm“.
Wenn das Klonen Kosten verursacht
„Rost.“ // Teuer: Tiefe Kopie sei vec1 = vec![1, 2, 3, 4, 5]; let vec2 = vec1.clone(); // Weist neuen Heap-Speicher zu und kopiert alle Elemente
// Kostenlos: Referenz sei vec1 = vec![1, 2, 3, 4, 5]; sei vec2 = &vec1; // Keine Zuordnung, nur ein Zeiger „
Benchmark: Klonen vs. Ausleihen
„Rost.“ benutze std::time::Instant;
fn process_by_value(data: Vec) -> i32 { data.iter().sum() }
fn process_by_reference(data: &[i32]) -> i32 { data.iter().sum() }
fn main() { let data: Vec = (0..1_000_000).collect();
// Klonversion
let start = Instant::now();
für _ in 0..1000 {
let result =process_by_value(data.clone()); // Jedes Mal klonen
}
println!("Klonen: {:?}", start.elapsed());
// Version ausleihen
let start = Instant::now();
für _ in 0..1000 {
let result = process_by_reference(&data); // Kein Klon
}
println!("Borrow: {:?}", start.elapsed());
}
// Typische Ergebnisse: // Klonen: 850 ms // Ausleihen: 120 ms „
Smart Pointer-Overhead
„Rost.“ // Rc hat einen geringen Overhead benutze std::rc::Rc; benutze std::time::Instant;
fn with_rc(data: Rc<Vec>) { let _ = data.len(); }
fn with_ref(data: &Vec) { let _ = data.len(); }
// Rc besteht aus 2 Wörtern (Zeiger + Referenzanzahl) // Referenz ist 1 Wort (nur Zeiger) // Aber Rc ermöglicht gemeinsames Eigentum, wo Referenzen dies nicht können „
Wenn es auf den Overhead ankommt:
- Heiße Schleifen mit Millionen von Iterationen
- Echtzeitsysteme
- Eingebettete Systeme mit begrenzten Ressourcen
Wenn der Overhead keine Rolle spielt:
- Der meiste Anwendungscode
- Wenn es ein besseres Design ermöglicht
- Wenn das Klonen teurer wäre
10. Migrationsleitfaden: Aus anderen Sprachen
Kommt aus C++
C++-Denkweise: „cpp std::string* createString() { return new std::string("hello"); // Anrufer muss löschen }
void-Prozess() { std::string* s = createString(); std::cout << *s; s löschen; // Manuelle Bereinigung } „
Rostäquivalent: „Rost.“ fn create_string() -> String { String::from("hello") // Eigentum übertragen }
fn-Prozess() { let s = create_string(); println!("{}", s); } // Automatisch gelöscht „
Hauptunterschiede:
- Kein manuelles „Neu“/„Löschen“. – Keine Rohzeiger im sicheren Code
- Referenzen unterliegen einer lebenslangen Überprüfung
- Semantik standardmäßig verschieben
Kommt von Go
Go-Denkweise: „Geh func process(data []int) { data[0] = 100 // Mutiert das Original }
func main() { Zahlen := []int{1, 2, 3} Prozess(Zahlen) fmt.Println(nums) // [100, 2, 3] } „
Rust erfordert explizite Veränderbarkeit: „Rost.“ fn process(data: &mut [i32]) { // Explizit &mut Daten[0] = 100; }
fn main() { let mut nums = vec![1, 2, 3]; // Schlüsselwort mut erforderlich Prozess(&mut nums); // Explizit &mut println!("{:?}", nums); // [100, 2, 3] } „
Hauptunterschiede:
- Veränderlichkeit muss explizit sein
- Keine versteckten Datenrennen
- Referenzen sind explizit („&“ vs. Wert)
Kommt von Python
Python-Denkweise: „Python def changes_list(items): items.append(4) # Mutiert das Original
Zahlen = [1, 2, 3] modifizieren_liste(nums) print(nums) # [1, 2, 3, 4] „
Rostäquivalent: „Rost.“ fn changes_list(items: &mut Vec) { items.push(4); }
fn main() { let mut nums = vec![1, 2, 3]; modifizieren_list(&mut nums); println!("{:?}", nums); // [1, 2, 3, 4] } „
Hauptunterschiede:
- Alles in Python ist eine Referenz; Rust unterscheidet Werte und Referenzen
- Python GC übernimmt die Bereinigung; Rust nutzt Eigentum
- Python erlaubt freie Mutation; Rust erfordert „mut“.
Kommt aus JavaScript
JavaScript-Denkweise: „Javascript Funktion createUser() { return { Name: „Alice“, E-Mail: „[email protected]“ }; }
let user = createUser(); let user2 = user; // Flache Kopie user2.name = „Bob“; console.log(Benutzername); // „Bob“ – beide beziehen sich auf dasselbe Objekt „
Rostverhalten: „Rost.“ #[ableiten(Klonen)] struct Benutzer { Name: Zeichenfolge, E-Mail: String, }
fn create_user() -> Benutzer { Benutzer { Name: String::from("Alice"), E-Mail: String::from("[email protected]"), } }
fn main() { let user = create_user(); let user2 = user; // Verschoben, nicht kopiert // println!("{}", user.name); // FEHLER: Wert verschoben
// Wenn Sie Kopierverhalten wünschen:
let user = create_user();
let user2 = user.clone(); // Expliziter Klon
println!("{}", user.name); // OK
} „
Hauptunterschiede:
- JS-Objekte werden referenzgezählt; Rust bewegt sich standardmäßig
- JS hat GC; Rust ist Eigentümer
- Die JS-Mutation ist uneingeschränkt; Rust erzwingt Ausleihregeln
Fazit: Nutzen Sie den Borrow Checker
Das Eigentumssystem fühlt sich zunächst restriktiv an, ist aber tatsächlich befreiend:
✅ Keine Speicherlecks: Beim Kompilieren wird der Speicher korrekt verwaltet ✅ Keine Datenrennen: Gleichzeitige Fehler werden zur Kompilierungszeit erkannt ✅ Keine Verwendung nach dem Freigeben: Es ist nicht möglich, auf den freigegebenen Speicher zuzugreifen ✅ Vorhersehbare Leistung: Keine GC-Pausen, keine versteckten Zuweisungen ✅ Furchtloses Refactoring: Der Compiler erkennt wichtige Änderungen
Professionelle Dienstleistungen erhalten →
Die Lernkurve
Woche 1-2: Frustration. Der Kreditprüfer lehnt alles ab. Woche 3-4: Verstehen. Sie fangen an, in Eigenverantwortung zu denken. Monat 2: Fließende Sprachkenntnisse. Sie entwerfen APIs, die mit Eigentum funktionieren. Monat 3+: Meisterschaft. Sie schreiben Rust schneller als Ihre alte Sprache.
Praktische Tipps zum Lernen
- Lesen Sie Compilerfehler sorgfältig durch – sie sind ausgezeichnete Lehrer
- Beginnen Sie mit kleinen Programmen – beherrschen Sie die Grundlagen, bevor Sie große Systeme erstellen
- Verwenden Sie „clone()“ zunächst großzügig – optimieren Sie es später
- Kämpfen Sie nicht gegen den Kreditprüfer – gestalten Sie das Design neu, wenn Sie Schwierigkeiten haben
- Studieren Sie den Code der Standardbibliothek – sehen Sie, wie Experten es machen
Nächste Schritte
- Übung: Lösen Sie Probleme zu Exercism, LeetCode oder Advent of Code in Rust
- Lesen: Das Rust-Buch, Rust by example, Rustonomicon (unsicheres Rust)
- Build: Echte Projekte zwingen Sie dazu, auf echte Probleme zu stoßen und diese zu lösen
- Mitwirken: Open-Source-Rust-Projekte heißen Neulinge willkommen
Ressourcen
- Die Programmiersprache Rust (Das Buch)
- Rust durch Beispiel
- Rustlings – Kleine Übungen
- Rust-Benutzerforum - Stellen Sie Fragen
- Diese Woche in Rust - Bleiben Sie auf dem Laufenden