Deep Dive into Rust’s Smart Pointers
Olivia Novak
Dev Intern · Leapcell

Was sind Smart Pointers in Rust?
Smart Pointers sind eine Art Datenstruktur, die nicht nur Daten besitzt, sondern auch zusätzliche Funktionalitäten bietet. Sie sind eine fortgeschrittene Form von Pointern.
Ein Pointer ist ein allgemeines Konzept für eine Variable, die eine Speicheradresse enthält. Diese Adresse "zeigt auf" oder referenziert andere Daten. Referenzen in Rust werden durch das Symbol &
gekennzeichnet und leihen sich die Werte, auf die sie zeigen. Referenzen erlauben nur den Datenzugriff, ohne zusätzliche Funktionen bereitzustellen. Sie verursachen auch keinen zusätzlichen Overhead, weshalb sie in Rust weit verbreitet sind.
Smart Pointers in Rust sind eine spezielle Art von Datenstruktur. Im Gegensatz zu regulären Pointern, die Daten lediglich ausleihen, besitzen Smart Pointers die Daten typischerweise. Sie bieten auch zusätzliche Funktionen.
Wofür werden Smart Pointers in Rust verwendet und welche Probleme lösen sie?
Smart Pointers bieten leistungsstarke Abstraktionen, um Programmierern zu helfen, Speicher und Nebenläufigkeit sicherer und effizienter zu verwalten. Einige dieser Abstraktionen umfassen Smart Pointers und Typen, die innere Veränderbarkeit bieten. Zum Beispiel:
Box<T>
wird verwendet, um Werte auf dem Heap zu allozieren.Rc<T>
ist ein referenzgezählter Typ, der mehrfachen Besitz von Daten ermöglicht.RefCell<T>
bietet innere Veränderbarkeit und ermöglicht mehrere mutable Referenzen auf dieselben Daten.
Diese Typen sind in der Standardbibliothek definiert und bieten flexibles Speichermanagement. Ein Hauptmerkmal von Smart Pointern ist, dass sie die Traits Drop
und Deref
implementieren:
- Der
Drop
Trait bietet diedrop
Methode, die aufgerufen wird, wenn der Smart Pointer den Gültigkeitsbereich verlässt. - Der
Deref
Trait ermöglicht automatisches Dereferenzieren, was bedeutet, dass Sie Smart Pointers in den meisten Situationen nicht manuell dereferenzieren müssen.
Gängige Smart Pointers in Rust
Box<T>
Box<T>
ist der einfachste Smart Pointer. Er ermöglicht es Ihnen, Werte auf dem Heap zu allozieren und den Speicher automatisch freizugeben, wenn er den Gültigkeitsbereich verlässt.
Zu den häufigen Anwendungsfällen für Box<T>
gehören:
- Speicherallokation für Typen mit unbekannter Größe zur Kompilierzeit, wie z. B. rekursive Typen.
- Verwaltung großer Datenstrukturen, die Sie nicht auf dem Stack speichern möchten, um Stack-Overflow zu vermeiden.
- Besitz eines Wertes, bei dem Sie sich nur für seinen Typ interessieren, nicht für den Speicher, den er belegt. Zum Beispiel beim Übergeben eines Closures an eine Funktion.
Hier ist ein einfaches Beispiel:
fn main() { let b = Box::new(5); println!("b = {}", b); }
In diesem Beispiel hält die Variable b
eine Box
, die auf den Wert 5
auf dem Heap zeigt. Das Programm gibt b = 5
aus. Auf die Daten innerhalb der Box kann zugegriffen werden, als ob sie auf dem Stack gespeichert wären. Wenn b
den Gültigkeitsbereich verlässt, gibt Rust automatisch sowohl die auf dem Stack allozierte Box als auch die auf dem Heap alloziierten Daten frei.
Allerdings kann Box<T>
nicht von mehreren Eigentümern gleichzeitig referenziert werden. Zum Beispiel:
enum List { Cons(i32, Box<List>), Nil, } use List::{Cons, Nil}; fn main() { let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); let b = Cons(3, Box::new(a)); let c = Cons(4, Box::new(a)); }
Dieser Code würde zu dem Fehler führen: error[E0382]: use of moved value: a
, da das Eigentum an a
bereits übertragen wurde. Um mehrfachen Besitz zu ermöglichen, ist Rc<T>
erforderlich.
Rc<T> - Referenzgezählt
Rc<T>
ist ein referenzgezählter Smart Pointer, der mehrfachen Besitz von Daten ermöglicht. Wenn der letzte Besitzer den Gültigkeitsbereich verlässt, werden die Daten automatisch freigegeben. Allerdings ist Rc<T>
nicht threadsicher und kann nicht in Multithread-Umgebungen verwendet werden.
Zu den häufigen Anwendungsfällen für Rc<T>
gehören:
- Gemeinsame Nutzung von Daten über mehrere Teile eines Programms hinweg, wodurch die mit
Box<T>
aufgetretenen Besitzprobleme gelöst werden. - Erstellen zyklischer Referenzen zusammen mit
Weak<T>
, um Speicherlecks zu vermeiden.
Hier ist ein Beispiel, das demonstriert, wie Rc<T>
für die gemeinsame Datennutzung verwendet wird:
use std::rc::Rc; fn main() { let data = Rc::new(vec![1, 2, 3]); let data1 = data.clone(); let data2 = data.clone(); println!("data: {:?}", data); println!("data1: {:?}", data1); println!("data2: {:?}", data2); }
In diesem Beispiel:
Rc::new
wird verwendet, um eine neue Instanz vonRc<T>
zu erstellen.- Die
clone
Methode wird verwendet, um den Referenzzähler zu erhöhen und neue Pointer auf denselben Wert zu erstellen. - Wenn der letzte
Rc
Pointer den Gültigkeitsbereich verlässt, wird der Wert automatisch freigegeben.
Allerdings ist Rc<T>
nicht sicher für die gleichzeitige Verwendung über mehrere Threads hinweg. Um dies zu beheben, bietet Rust Arc<T>
.
Arc<T> - Atomarisch Referenzgezählt
Arc<T>
ist die threadsichere Variante von Rc<T>
. Sie ermöglicht es mehreren Threads, den Besitz derselben Daten gemeinsam zu nutzen. Wenn die letzte Referenz den Gültigkeitsbereich verlässt, werden die Daten freigegeben.
Zu den häufigen Anwendungsfällen für Arc<T>
gehören:
- Sichere gemeinsame Nutzung von Daten über mehrere Threads hinweg.
- Übertragen von Daten zwischen Threads.
Hier ist ein Beispiel, das demonstriert, wie Arc<T>
für die gemeinsame Datennutzung über Threads hinweg verwendet wird:
use std::sync::Arc; use std::thread; fn main() { let data = Arc::new(vec![1, 2, 3]); let data1 = Arc::clone(&data); let data2 = Arc::clone(&data); let handle1 = thread::spawn(move || { println!("data1: {:?}", data1); }); let handle2 = thread::spawn(move || { println!("data2: {:?}", data2); }); handle1.join().unwrap(); handle2.join().unwrap(); }
In diesem Beispiel:
Arc::new
erstellt einen threadsicheren, referenzgezählten Pointer.Arc::clone
wird verwendet, um den Referenzzähler sicher für mehrere Threads zu erhöhen.- Jeder Thread erhält seinen eigenen Klon des
Arc
, und wenn alle Referenzen den Gültigkeitsbereich verlassen, werden die Daten freigegeben.
Weak<T> - Schwacher Referenztyp
Weak<T>
ist ein schwacher Referenztyp, der mit Rc<T>
oder Arc<T>
verwendet werden kann, um zyklische Referenzen zu erstellen. Im Gegensatz zu Rc<T>
und Arc<T>
erhöht Weak<T>
den Referenzzähler nicht, was bedeutet, dass sie nicht verhindert, dass Daten freigegeben werden.
Zu den häufigen Anwendungsfällen für Weak<T>
gehören:
- Beobachten eines Wertes, ohne dessen Lebenszyklus zu beeinflussen.
- Aufbrechen starker Referenzzyklen, um Speicherlecks zu vermeiden.
Hier ist ein Beispiel, das demonstriert, wie Rc<T>
und Weak<T>
verwendet werden, um zyklische Referenzen zu erstellen:
use std::rc::{Rc, Weak}; struct Node { value: i32, next: Option<Rc<Node>>, prev: Option<Weak<Node>>, } fn main() { let first = Rc::new(Node { value: 1, next: None, prev: None }); let second = Rc::new(Node { value: 2, next: None, prev: Some(Rc::downgrade(&first)) }); first.next = Some(second.clone()); }
In diesem Beispiel:
Rc::downgrade
wird verwendet, um eineWeak
Referenz zu erstellen.- Das Feld
prev
enthält eineWeak
Referenz, die sicherstellt, dass sie nicht zum Referenzzähler beiträgt und somit ein Speicherleck verhindert. - Beim Zugriff auf eine
Weak
Referenz können Sie.upgrade()
aufrufen, um zu versuchen, sie wieder in einRc
zu konvertieren. Wenn der Wert freigegeben wurde, gibtupgrade
None
zurück.
UnsafeCell<T>
UnsafeCell<T>
ist ein Low-Level-Typ, der es Ihnen ermöglicht, Daten über eine unveränderliche Referenz zu modifizieren. Im Gegensatz zu Cell<T>
und RefCell<T>
führt UnsafeCell<T>
keine Laufzeitprüfungen durch, was es zu einer Grundlage für den Aufbau anderer Typen mit innerer Veränderlichkeit macht.
Wichtige Punkte zu UnsafeCell<T>
:
- Es kann zu undefiniertem Verhalten führen, wenn es falsch verwendet wird.
- Es wird typischerweise in Low-Level-, performancekritischem Code oder bei der Implementierung benutzerdefinierter Typen verwendet, die innere Veränderlichkeit erfordern.
Hier ist ein Beispiel für die Verwendung von UnsafeCell<T>
:
use std::cell::UnsafeCell; fn main() { let x = UnsafeCell::new(1); let y = &x; let z = &x; unsafe { *x.get() = 2; *y.get() = 3; *z.get() = 4; } println!("x: {}", unsafe { *x.get() }); }
In diesem Beispiel:
UnsafeCell::new
erstellt eine neueUnsafeCell
.- Die Methode
.get()
stellt einen Raw Pointer bereit, der die Modifikation der Daten im Inneren ermöglicht. - Modifikationen werden innerhalb eines
unsafe
Blocks durchgeführt, da Rust die Speichersicherheit nicht garantieren kann.
Hinweis: Da UnsafeCell<T>
Rusts Sicherheitsgarantien umgeht, sollte es mit Vorsicht verwendet werden. In den meisten Fällen ist Cell<T>
oder RefCell<T>
für sichere innere Veränderlichkeit vorzuziehen.
Cell<T>
Cell<T>
ist ein Typ, der innere Veränderlichkeit in Rust ermöglicht. Er erlaubt es Ihnen, Daten zu modifizieren, selbst wenn Sie eine unveränderliche Referenz haben. Allerdings funktioniert Cell<T>
nur mit Typen, die das Copy
Trait implementieren, da es innere Veränderlichkeit durch das Kopieren von Werten hinein und heraus erreicht.
Häufige Anwendungsfälle für Cell<T>
:
- Wenn Sie Daten über eine unveränderliche Referenz mutieren müssen.
- Wenn Sie eine Struktur haben, die ein veränderliches Feld benötigt, die Struktur selbst aber nicht veränderlich ist.
Beispiel für die Verwendung von Cell<T>
:
use std::cell::Cell; fn main() { let x = Cell::new(1); let y = &x; let z = &x; x.set(2); y.set(3); z.set(4); println!("x: {}", x.get()); }
In diesem Beispiel:
Cell::new
erstellt eine neueCell<T>
Instanz, die den Wert1
enthält.set
wird verwendet, um den internen Wert zu ändern, obwohl die Referenzeny
undz
unveränderlich sind.get
wird verwendet, um den Wert abzurufen.
Da Cell<T>
Kopiersemantik verwendet, funktioniert es nur mit Typen, die das Copy
Trait implementieren. Wenn Sie innere Veränderlichkeit für Nicht-Copy
-Typen (wie Vec
oder benutzerdefinierte Strukturen) benötigen, sollten Sie RefCell<T>
verwenden.
RefCell<T>
RefCell<T>
ist ein weiterer Typ, der innere Veränderlichkeit ermöglicht, aber er funktioniert für Nicht-Copy
-Typen. Im Gegensatz zu Cell<T>
erzwingt RefCell<T>
Rusts Borrowing-Regeln zur Laufzeit anstelle der Kompilierzeit.
- Es erlaubt mehrere unveränderliche Barrows oder einen veränderlichen Borrow.
- Wenn die Borrowing-Regeln verletzt werden, wird
RefCell<T>
zur Laufzeit paniken.
Häufige Anwendungsfälle für RefCell<T>
:
- Wenn Sie Nicht-
Copy
-Typen über unveränderliche Referenzen modifizieren müssen. - Wenn Sie veränderliche Felder innerhalb einer Struktur benötigen, die ansonsten unveränderlich sein sollte.
Beispiel für die Verwendung von RefCell<T>
:
use std::cell::RefCell; fn main() { let x = RefCell::new(vec![1, 2, 3]); let y = &x; let z = &x; x.borrow_mut().push(4); y.borrow_mut().push(5); z.borrow_mut().push(6); println!("x: {:?}", x.borrow()); }
In diesem Beispiel:
RefCell::new
erstellt eine neueRefCell<T>
, die einen Vektor enthält.borrow_mut()
wird verwendet, um eine veränderliche Referenz auf die Daten zu erhalten, die eine Mutation auch über eine unveränderliche Referenz ermöglicht.borrow()
wird verwendet, um eine unveränderliche Referenz zum Lesen zu erhalten.
Wichtige Hinweise:
-
Laufzeit-Borrow-Prüfung: Rusts übliche Borrowing-Regeln werden zur Kompilierzeit erzwungen, aber
RefCell<T>
verschiebt diese Prüfungen zur Laufzeit. Wenn Sie versuchen, veränderlich zu borrowen, während ein unveränderlicher Borrow noch aktiv ist, wird das Programm paniken. -
Vermeidung von Borrowing-Konflikten: Zum Beispiel wird der folgende Code zur Laufzeit paniken:
let x = RefCell::new(5); let y = x.borrow(); let z = x.borrow_mut(); // Dies wird paniken, da `y` immer noch ein aktiver unveränderlicher Borrow ist.
Daher müssen Sie, obwohl RefCell<T>
flexibel ist, darauf achten, Borrowing-Konflikte zu vermeiden.
Zusammenfassung der wichtigsten Smart-Pointer-Typen
Smart Pointer | Thread-Safe | Erlaubt mehrere Besitzer | Innere Veränderlichkeit | Laufzeit-Borrow-Prüfung |
---|---|---|---|---|
Box<T> | ❌ | ❌ | ❌ | ❌ |
Rc<T> | ❌ | ✅ | ❌ | ❌ |
Arc<T> | ✅ | ✅ | ❌ | ❌ |
Weak<T> | ✅ | ✅ (schwacher Besitz) | ❌ | ❌ |
Cell<T> | ❌ | ❌ | ✅ (Nur Copy-Typen) | ❌ |
RefCell<T> | ❌ | ❌ | ✅ | ✅ |
UnsafeCell<T> | ✅ | ❌ | ✅ | ❌ |
Auswahl des richtigen Smart Pointers
- Verwenden Sie
Box<T>
, wenn Sie Heap-Allokation mit einfachem Besitz benötigen. - Verwenden Sie
Rc<T>
, wenn Sie mehrfachen Besitz in einem Single-Threaded-Kontext benötigen. - Verwenden Sie
Arc<T>
, wenn Sie mehrfachen Besitz über mehrere Threads hinweg benötigen. - Verwenden Sie
Weak<T>
, um Referenzzyklen mitRc<T>
oderArc<T>
zu verhindern. - Verwenden Sie
Cell<T>
fürCopy
-Typen, bei denen innere Veränderlichkeit benötigt wird. - Verwenden Sie
RefCell<T>
für Nicht-Copy
-Typen, bei denen innere Veränderlichkeit erforderlich ist. - Verwenden Sie
UnsafeCell<T>
nur in Low-Level-, performancekritischen Szenarien, in denen manuelle Sicherheitsprüfungen akzeptabel sind.
Wir sind Leapcell, Ihre erste Wahl für das Hosting 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.
Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung - keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 $ unterstützen 6,94 Millionen Anfragen bei 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
- Auto-Skalierung zur einfachen Bewältigung hoher Parallelität.
- Null Betriebsaufwand - konzentrieren Sie sich einfach auf das Bauen.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ