DanLevy.net

Quiz: Wesentliche Speicherverwaltung in Rust

Check dich selbst, bevor du dich selbst zerstörst! 🦀

Bereit, deine Rust‑Speicherverwaltungs‑Fähigkeiten zu testen? 🦀

Dieses Quiz stellt dein Verständnis des Rust‑Ownership‑Systems, der Borrowing‑Regeln, Lebensdauern und Smart‑Pointer auf die Probe.

Hinweis: Die Fragen sind auf etwa 50 Spalten Breite formatiert, um auf allen Geräten gut lesbar zu sein. (Verbesserungsvorschläge sind willkommen!)

Egal, ob du ein erfahrener Rustacean bist oder gerade erst mit Speicherverwaltung beginnst, dieses Quiz festigt dein Wissen. Los geht’s! 🦀

Was passiert, wenn du diesen Code ausführst? Versuche die Ausgabe oder den Fehler vorherzusagen:

fn main() {
let philosopher =
String::from("Zeno of Citium");
let greeting = philosopher;
println!("Hello, {}!", philosopher);
}

Dieser Code lässt sich nicht kompilieren wegen Rusts Besitzregeln. Wenn wir philosopher greeting zuweisen, wird das Eigentum des Strings nach greeting verschoben. Nach diesem Move ist philosopher nicht mehr gültig.

Hier sind drei Möglichkeiten, das zu beheben:

  1. Den String klonen (erstellt eine neue Kopie):
let greeting = philosopher.clone();
  1. Eine Referenz verwenden (leiht den Wert aus):
let greeting = &philosopher;
  1. Einen String‑Slice verwenden (leiht einen Teil des Strings aus):
let greeting = &philosopher[..];

Jede Lösung hat unterschiedliche Anwendungsfälle und Performance‑Auswirkungen. Klonen ist teurer, gibt dir aber Eigentum, während Referenzen günstiger sind, aber Lebensdauer‑Beschränkungen haben.

Was passiert, wenn du diesen Code ausführst? Denke an die Besitzübertragung:

fn take_knowledge(knowledge: String) {
println!("Knowledge: {}", knowledge);
}
fn main() {
let wisdom = String::from("know thyself");
take_knowledge(wisdom);
// What happens to our wisdom?
println!("Do you {}", wisdom);
}

Der Code lässt sich nicht kompilieren, weil das Eigentum von wisdom nach take_knowledge verschoben wurde und es danach nicht mehr verwendet werden kann.

Hier sind drei Möglichkeiten, dieses Problem zu beheben:

  1. Per Referenz übergeben (den Wert ausleihen):
fn borrow_it(text: &String) {
println!("Inside: {}", text);
}
borrow_it(&wisdom); // Now wisdom can be used after
  1. Den Wert klonen (eine neue Kopie erstellen):
take_knowledge(wisdom.clone()); // Original wisdom remains valid
  1. Das Eigentum aus der Funktion zurückgeben:
fn take_and_return(text: String) -> String {
println!("Inside: {}", text);
text // Return ownership back
}
let wisdom = take_and_return(wisdom); // Reassign returned ownership

Jeder Ansatz hat unterschiedliche Anwendungsfälle:

  • Referenzen: Am effizientesten, erfordern jedoch Lebensdauer‑Management
  • Klonen: Einfach, kann aber teuer sein
  • Eigentum zurückgeben: Nützlich zum Transformieren von Werten

Best Practice: Verwende Referenzen, es sei denn, du benötigst eine Eigentumsübertragung.

Was passiert bei mehreren mutablen Referenzen?

fn main() {
let mut wisdom = String::from("He who laughs at");
let ref1 = &mut wisdom; // First mutable borrow
let ref2 = &mut wisdom; // Second mutable borrow
ref1.push_str(" himself never runs");
ref2.push_str(" out of things to laugh at.");
}

Denke an Rusts Regeln für mutable Referenzen.

Dieser Code verletzt Rusts grundlegende Ausleihregeln:

  • Nur EINE mutable Referenz auf einen Wert zur gleichen Zeit
  • ODER beliebig viele immutable Referenzen
  • Referenzen dürfen ihren Referenten nicht überleben

So kann man den Code korrigieren:

  1. Verwende sequentielles Scoping:
let mut wisdom = String::from("He who laughs at");
{
let ref1 = &mut wisdom;
ref1.push_str(" himself never runs");
} // ref1 goes out of scope
let ref2 = &mut wisdom; // Now this is valid
ref2.push_str(" out of things to laugh at.");
  1. Oder modifiziere den String in einer einzigen Ausleihe:
let mut wisdom = String::from("He who laughs at");
let ref1 = &mut wisdom;
ref1.push_str(" himself never runs out of things to laugh at.");

Diese Regeln verhindern Datenrennen zur Compile‑Zeit und machen Rust standardmäßig thread‑sicher.

Häufiger Stolperstein: Der Versuch, mehrere mutable Referenzen zu verwenden, um Klonen zu vermeiden oder verschiedene Teile desselben Wertes gleichzeitig zu modifizieren.

Wird dieser Code kompilieren? Wenn ja, warum? Wenn nein, was ist falsch?

fn first_word(s: &str) -> &str { // No explicit lifetimes?
match s.find(' ') {
Some(pos) => &s[0..pos],
None => s,
}
}
fn main() {
let name = String::from("Seneca the Younger");
let first = first_word(&name);
println!("Hello, {}", first);
}

Dieser Code kompiliert dank Rusts Lebensdauer‑Elisionsregeln erfolgreich. Diese Regeln erlauben dem Compiler, Lebensdauern in üblichen Mustern automatisch zu inferieren.

Die drei Elisionsregeln sind:

  1. Jeder Parameter bekommt seinen eigenen Lebensdauer‑Parameter
  2. Gibt es genau einen Eingabe‑Lebensdauer‑Parameter, wird dieser allen Ausgabe‑Lebensdauer‑Parametern zugewiesen
  3. Gibt es mehrere Eingabe‑Lebensdauer‑Parameter, aber einer davon ist &self oder &mut self, wird die Lebensdauer von self allen Ausgabe‑Lebensdauer‑Parametern zugewiesen

Diese Funktion ist äquivalent zu:

fn first_word<'a>(s: &'a str) -> &'a str {
// ... same implementation
}

Übliche Muster, bei denen Elision funktioniert:

// These don't need explicit lifetimes
fn get_str(s: &str) -> &str { s }
fn get_first(s: &str) -> &str { &s[0..1] }
// These would need explicit lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

Best Practice: Lass die Elision für dich arbeiten, wenn möglich, aber verstehe, wann explizite Lebensdauern nötig sind.

Was ist falsch an dieser rekursiven Typdefinition?

#[derive(Debug)]
enum CatList {
Cons(i32, CatList), // Recursive without indirection
Nil,
}
fn main() {
let catlist = CatList::Cons(1,
CatList::Cons(2,
CatList::Cons(3,
CatList::Nil)));
}

Dieser Code schlägt fehl, weil der Compiler die Größe von CatList zur Compile‑Zeit nicht bestimmen kann. Die rekursive Natur des Typs bedeutet, dass er unendlich groß sein könnte!

So lässt sich das mit Box<T> beheben:

#[derive(Debug)]
enum CatList {
Cons(i32, Box<CatList>), // Box provides a fixed-size pointer
Nil,
}
fn main() {
let catlist = CatList::Cons(1,
Box::new(CatList::Cons(2,
Box::new(CatList::Cons(3,
Box::new(CatList::Nil))))));
}

Warum Box<T> funktioniert:

  1. Box liefert einen Zeiger fester Größe (normalerweise 8 Byte auf 64‑Bit‑Systemen)
  2. Die eigentlichen Daten liegen im Heap
  3. Der Compiler weiß jetzt genau, wie viel Speicherplatz zu reservieren ist

Häufige Anwendungsfälle für Box<T>:

  • Rekursive Datenstrukturen (verkettete Listen, Bäume)
  • Große Daten, die unbedingt im Heap liegen sollen
  • Trait‑Objekte, wenn dynamisches Dispatch nötig ist

Best Practice: Verwende Box<T>, wenn du:

  • Rekursive Typen
  • Eine Heap‑Allokation sicherstellen willst
  • Große Daten ohne Kopieren bewegen möchtest

Was wird dieser Code ausgeben? Zähle genau!

use std::rc::Rc;
fn main() {
let text = Rc::new(String::from("Meditations")); // Count: 1
let marcus = Rc::clone(&text); // What happens here?
let aurelius = Rc::clone(&text); // And here?
println!(
"Reference count: {}",
Rc::strong_count(&text)
);
}

Lass uns aufschlüsseln, wie Rc funktioniert:

  1. Anfangserstellung mit Rc::new(): count = 1
  2. Erster Klon für marcus: count = 2
  3. Zweiter Klon für aurelius: count = 3

Wichtige Rc‑Eigenschaften:

use std::rc::Rc;
fn demonstrate_rc() {
let original = Rc::new(String::from("Shared"));
println!("Count after creation: {}", Rc::strong_count(&original)); // 1
{
let copy = Rc::clone(&original);
println!("Count inside scope: {}", Rc::strong_count(&original)); // 2
} // copy is dropped here
println!("Count after scope: {}", Rc::strong_count(&original)); // 1
}

Wichtige Punkte:

  • Rc::clone() ist günstig – es erhöht nur einen Zähler
  • Rc ist nur für Single‑Thread‑Szenarien gedacht
  • Wenn die letzte Referenz fällt, wird das Datenobjekt bereinigt
  • Verwende Weak‑Referenzen, um Referenzzyklen zu vermeiden

Beste Praktiken:

  • Nutze Rc, wenn du geteiltes Eigentum brauchst
  • Ziehe Arc für thread‑sichere Szenarien in Betracht
  • Vermeide das Erzeugen von Referenzzyklen

Wird diese Strukturdefinition kompilieren? Warum oder warum nicht?

struct Philosopher {
name: &str, // Reference without lifetime
quote: &str, // Another reference without lifetime
}
fn main() {
let phil = Philosopher {
name: "Seneca",
quote: "Luck happens when preparation meets opportunity",
};
}

Der Code schlägt fehl, weil Strukturen, die Referenzen enthalten, Lebensdauern angeben müssen. So geht’s:

// Single lifetime parameter
struct Philosopher<'a> {
name: &'a str,
quote: &'a str,
}
// Or different lifetimes if needed
struct PhilosopherFlex<'n, 'q> {
name: &'n str,
quote: &'q str,
}

Übliche Muster:

// Own the data instead
struct PhilosopherOwned {
name: String,
quote: String,
}
// Mixed ownership
struct PhilosopherMixed<'a> {
name: String, // Owned
quote: &'a str, // Borrowed
}

Best practices:

  1. Verwende Typen mit eigenem Besitz (String), wenn du Daten unbegrenzt speichern musst
  2. Verwende Referenzen, wenn die Lebensdauer der Struktur eindeutig kürzer ist als die der Daten
  3. Ziehe mehrere Lebensdauer‑Parameter in Betracht, wenn Referenzen unterschiedliche Lebensdauern haben können
  4. Dokumentiere Lebensdauer‑Beziehungen in komplexen Strukturen

Was passiert bei dieser Funktion, die die längere von zwei String‑Slices zurückgibt?

fn longest(text1: &str, text2: &str) -> &str {
if text1.len() > text2.len() {
text1 // Returning a reference, but which lifetime?
} else {
text2 // Could be this reference instead
}
}
fn main() {
println!("{}", longest(
"Seneca the Younger",
"Marcus Aurelius"
));
}

Die zurückgegebene Referenz muss innerhalb einer für beide Eingaben gültigen Lebensdauer bleiben. Sie darf keinen der geliehenen Werte überleben. Annotationen beschreiben diese Beziehung, verlängern aber keine Lebensdauer.

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}

Was passiert, wenn dieser Code ausgeführt wird?

use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
let _borrow1 = data.borrow_mut(); // First mutable borrow
let _borrow2 = data.borrow_mut(); // Second mutable borrow
println!("Value: {}", _borrow2);
}

Im Fragecode ist die erste veränderbare Ausleihe _borrow1 noch aktiv, wenn _borrow2 erneut borrow_mut() aufruft. Dieser zweite Aufruf löst eine Panic aus; die Ausgabe wird nicht erreicht. Im korrigierten Beispiel unten wird jede veränderbare Ausleihe freigegeben, bevor eine weitere oder eine gemeinsame Ausleihe entsteht.

use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
let mut second = data.borrow_mut();
*second += 1;
drop(second);
let read1 = data.borrow();
let read2 = data.borrow();
println!("{} {}", *read1, *read2);
}

Was wird dieser Code ausgeben?

use std::cell::Cell;
fn main() {
let life = Cell::new(42);
let meaning = &life; // Shared reference
println!("{}", life.get()); // What prints here?
meaning.set(43); // Mutation through shared ref
println!("{}", life.get()); // And here?
}

Cell und RefCell dienen unterschiedlichen Zwecken für innere Mutabilität:

use std::cell::{Cell, RefCell};
// Cell for Copy types
struct Counter {
count: Cell<i32>,
}
impl Counter {
fn increment(&self) {
self.count.set(self.count.get() + 1);
}
}
// RefCell for non-Copy types
struct Logger {
messages: RefCell<Vec<String>>,
}
impl Logger {
fn log(&self, msg: &str) {
self.messages.borrow_mut().push(msg.to_string());
}
}

Schlüsselunterschiede:

  1. Cell:
  • Funktioniert am besten mit Copy‑Typen
  • Keine Borrowing‑API
  • Kopiert oder verschiebt immer Werte
  1. RefCell:
  • Funktioniert mit jedem Typ
  • Hat eine Borrowing‑API
  • Laufzeit‑Borrow‑Prüfung

Best Practices:

  1. Verwende Cell für einfache Copy‑Typen (Zahlen, bool, usw.)
  2. Verwende RefCell, wenn du den Inhalt ausleihen musst
  3. Halte Mutationen über Cell/RefCell minimal
  4. Dokumentiere, warum innere Mutabilität nötig ist

Wann sollte man Rc (Referenzzählung) in Rust verwenden?

Betrachte dieses Beispiel:

use std::rc::Rc;
struct SharedConfig {
name: String,
value: i32,
}
fn main() {
let config = Rc::new(SharedConfig {
name: "settings".to_string(),
value: 42,
});
let config2 = Rc::clone(&config);
// Both config and config2 share ownership
}

Rc (Referenzzählung) ist für Single‑Thread‑Szenarien gedacht, in denen du geteiltes Eigentum brauchst.

Häufige Anwendungsfälle:

use std::rc::Rc;
use std::cell::RefCell;
// Shared ownership in data structures
struct Node {
next: Option<Rc<Node>>,
value: i32,
}
// Combining with interior mutability
struct SharedState {
data: Rc<RefCell<Vec<String>>>,
}
// Multiple owners of same data
let original = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&original);
let clone2 = Rc::clone(&original);

Wichtige Punkte:

  1. Verwende Rc, wenn:
  • Mehrere Teile deines Codes Eigentum benötigen
  • Du weißt, dass das Teilen Single‑Thread‑basiert ist
  • Die Lebensdauer kann nicht statisch bestimmt werden
  1. Verwende stattdessen Arc, wenn:
  • Du thread‑sichere Teilung brauchst
  • Mehrere Threads Eigentum benötigen
  1. Einschränkungen von Rc:
  • Nicht thread‑sicher
  • Leichte Laufzeit‑Überhead
  • Kann Referenzzyklen nicht automatisch auflösen

Bewährte Vorgehensweisen:

  1. Bevorzuge eindeutiges Eigentum, wenn möglich
  2. Verwende Rc für geteiltes Eigentum in Single‑Thread‑Umgebungen
  3. Verwende Arc für Multi‑Thread‑Szenarien
  4. Kombiniere es mit Weak, um Referenzzyklen zu verhindern

Was ist der entscheidende Unterschied zwischen RefCell und RwLock in Rust?

Betrachte diese Beispiele:

use std::cell::RefCell;
use std::sync::RwLock;
// Example 1
let data = RefCell::new(vec![1, 2, 3]);
let borrowed = data.borrow_mut();
// Example 2
let shared = RwLock::new(vec![1, 2, 3]);
let locked = shared.write().unwrap();

RefCell<T> prüft Ausleihen zur Laufzeit und ist nicht Sync: Eine Referenz darf nicht zwischen Threads geteilt werden. Bei T: Send kann der Wert in einen anderen Thread verschoben werden. RwLock<T> bietet synchronisierten Zugriff entsprechend seinen Typbedingungen.

Was passiert, wenn dieser Code ausgeführt wird?

use std::sync::{Arc, Mutex};
fn main() {
let lock = Arc::new(Mutex::new(42));
let lock2 = Arc::clone(&lock);
let _guard1 = lock.lock().unwrap(); // First lock
let _guard2 = lock2.lock().unwrap(); // Second lock attempt
println!("Value: {}", _guard2);
}

Derselbe Mutex darf nicht zweimal im selben Thread gesperrt werden, ohne den ersten Guard freizugeben. Der zweite Aufruf kehrt nicht normal zurück: Die API erlaubt Deadlock oder Panic. Gib den Guard vor erneutem Sperren frei.

use std::sync::Mutex;
fn main() {
let lock = Mutex::new(42);
{ let mut guard = lock.lock().unwrap(); *guard += 1; }
let guard = lock.lock().unwrap();
println!("{}", *guard);
}

Was passiert, wenn du diesen Code mit schwachen Referenzen ausführst?

use std::rc::{Rc, Weak};
fn main() {
let data = Rc::new(String::from("Wisdom"));
let weak = Rc::downgrade(&data); // Create weak reference
drop(data); // Drop strong reference
println!("Value: {:?}", weak.upgrade());
}

Schwache Referenzen verhindern nicht die Deallokation ihrer Ziele. Hier ein ausführliches Beispiel:

use std::rc::{Rc, Weak};
use std::cell::RefCell;
// Parent-child tree structure avoiding reference cycles
struct Node {
next: Option<Rc<Node>>,
parent: RefCell<Weak<Node>>, // Weak to prevent cycles
value: i32,
}
impl Node {
fn new(value: i32) -> Rc<Node> {
Rc::new(Node {
next: None,
parent: RefCell::new(Weak::new()),
value,
})
}
fn set_parent(&self, parent: &Rc<Node>) {
*self.parent.borrow_mut() = Rc::downgrade(parent);
}
fn get_parent(&self) -> Option<Rc<Node>> {
self.parent.borrow().upgrade()
}
}

Typische Anwendungsfälle:

  1. Cache‑ähnliche Strukturen, bei denen Einträge gelöscht werden können
  2. Baumstrukturen mit Eltern‑Referenzen
  3. Beobachter‑Muster, bei denen Subjects fallen gelassen werden können
  4. Aufbrechen von Referenzzyklen in komplexen Datenstrukturen

Best Practices:

  1. Verwende schwache Referenzen für optionale Beziehungen
  2. Prüfe die Ergebnisse von upgrade() bevor du sie nutzt
  3. Dokumentiere Eigentums‑Beziehungen klar
  4. Ziehe Alternativen wie Indizes für einfachere Fälle in Betracht

Was passiert mit dem Dateihandle in diesem RAII‑Beispiel?

use std::fs::File;
struct FileWrapper {
file: File,
}
fn main() {
let file = File::create("test.txt").unwrap();
let wrapper = FileWrapper { file };
// ... use wrapper ...
// No Drop implementation
}

RAII in Rust sorgt dafür, dass Ressourcen korrekt verwaltet werden. In diesem Beispiel benötigt FileWrapper keine benutzerdefinierte Drop‑Implementierung, damit das Dateihandle geschlossen wird: sein File‑Feld wird automatisch freigegeben, wenn der Wrapper den Gültigkeitsbereich verlässt.

Man implementiert Drop nur, wenn der Wrapper selbst zusätzliches Aufräumverhalten hat, das über das Freigeben seiner Felder hinausgeht:

use std::fs::File;
use std::io::{self, Write};
struct FileWrapper {
file: File,
path: String,
}
impl FileWrapper {
fn new(path: &str) -> io::Result<FileWrapper> {
Ok(FileWrapper {
file: File::create(path)?,
path: path.to_string(),
})
}
fn write(&mut self, content: &str) -> io::Result<()> {
self.file.write_all(content.as_bytes())
}
}
impl Drop for FileWrapper {
fn drop(&mut self) {
// Ensure file is properly closed
// Could also do cleanup like deletion
println!("Closing file: {}", self.path);
}
}

RAII‑Muster:

  1. Konstruktor erwirbt Ressourcen
  2. Methoden nutzen Ressourcen sicher
  3. Felder werden automatisch freigegeben, wenn der Besitzer den Gültigkeitsbereich verlässt
  4. Benutzerdefiniertes Drop fügt bei Bedarf zusätzliche Aufräumarbeiten hinzu
  5. ? für Fehlerweiterleitung verwenden

Best practices:

  1. Auf die Drop‑Implementierungen der Standardbibliothek vertrauen, wenn sie die Ressource bereits modellieren
  2. Ressourcenverwaltung einfach und klar halten
  3. Standardbibliothek‑Typen verwenden, wann immer möglich
  4. Aufräumverhalten dokumentieren
  5. Erwägen, Guard‑Muster für scoped Operationen zu nutzen

Was passiert, wenn wir diese Philosophy‑Struktur klonen?

#[derive(Clone)]
struct Philosophy {
school: String,
founder: String,
}
fn main() {
let stoicism = Philosophy {
school: String::from("Stoicism"),
founder: String::from("Zeno of Citium")
};
let new_school = stoicism.clone();
println!("{} - {}",
stoicism.school, new_school.school);
}

Copy kopiert implizit bitweise. Clone ist explizit und kann Speicher reservieren; hier entstehen unabhängige Kopien beider String-Felder. Stack oder Heap bestimmt nicht, ob ein Typ Copy implementiert.

Auf einem typischen aktuellen 64‑Bit‑Rust‑Ziel, wie groß ist diese Struktur?

struct Metadata {
id: u32, // How many bytes?
name: String, // How many bytes?
active: bool // How many bytes + padding?
}

Lassen Sie uns das Speicherlayout der Struktur und die Optimierung aufschlüsseln:

// Typical current 64-bit Rust layout: 32 bytes
struct Metadata {
id: u32, // 4 bytes
name: String, // 24 bytes on 64-bit systems
active: bool // 1 byte + padding/alignment
}
// Reordering fields may reduce padding for repr(C) structs,
// but default Rust layout is not a stable ABI guarantee.
struct OptimizedMetadata {
name: String, // 24 bytes
id: u32, // 4 bytes
active: bool // 1 byte + 3 padding
}
// Further optimization with packing
#[repr(packed)]
struct PackedMetadata {
id: u32,
active: bool,
name: String,
}

Speicherlayout‑Überlegungen:

  1. Ausrichtungsanforderungen:
  • u32: 4‑Byte‑Ausrichtung
  • String: 8‑Byte‑Ausrichtung und 24‑Byte‑Größe auf gängigen 64‑Bit‑Zielen
  • bool: 1‑Byte‑Ausrichtung
  1. Feld‑Ordnungs‑Strategien:
  • Ähnliche Feldgrößen gruppieren
  • Größere Ausrichtungen zuerst setzen
  • Cache‑Line‑Optimierung berücksichtigen

Beste Praktiken:

  1. Für FFI oder stabile Layout‑Annahmen ein passendes repr(...) verwenden
  2. Geeignete Ganzzahlgrößen wählen
  3. Option für optionale Felder nutzen
  4. Größenkritische Strukturen mit std::mem::size_of messen
  5. #[repr(packed)] vorsichtig einsetzen – es kann die Performance beeinflussen

Was können Rusts kostenfreie Iterator-Abstraktionen bei aktivierter Optimierung in diesen Implementierungen erreichen?

// Implementation A: Iterator
fn sum_iterator(v: &[i32]) -> i32 {
v.iter().fold(0, |acc, &x| acc + x)
}
// Implementation B: Raw loop
fn sum_loop(v: &[i32]) -> i32 {
let mut sum = 0;
for i in 0..v.len() {
sum += v[i];
}
sum
}

Kostenfreie Abstraktionen ermöglichen effizienten optimierten Code, garantieren aber keine identischen Laufzeiten für jeden Compiler und jede Optimierungsstufe. Beide Funktionen berechnen dieselbe Summe. Miss die tatsächliche Last, wenn Leistung wichtig ist.

use std::ops::Range;
trait ZeroCost {
fn process(&self) -> u32;
}
impl ZeroCost for Range<u32> {
fn process(&self) -> u32 {
self.clone().fold(0, |acc, x| acc + x)
}
}

Danke, dass du am Quiz teilgenommen hast! Wenn dir das Testen deines Rust‑Wissens gefallen hat, sieh dir meine anderen Programmierungs‑Challenges! 🧠

Möchtest du deine Rust‑Fähigkeiten weiter ausbauen? Hier ein paar empfohlene Ressourcen: