Marker-Traits sind Traits ohne Methoden — sie tragen keine Verhaltens-Beschreibung, sondern kennzeichnen Eigenschaften eines Typs für den Compiler. Send etwa hat keine Methoden; ein Typ, der Send implementiert, signalisiert: "ich darf über Thread-Grenzen verschoben werden". Der Compiler nutzt diese Marker, um z.B. zu prüfen, ob ein Typ in thread::spawn übergeben werden darf. Die fünf wichtigsten Marker-Traits — Send, Sync, Sized, Copy, Unpin — sind die Sprache des Typ-Systems, mit der Rust Speicher- und Thread-Sicherheit garantiert. Wer sie versteht, versteht die Sicherheits-Architektur von Rust.

Was ein Marker-Trait ist

Ein Marker-Trait ist syntaktisch ein Trait ohne Body (keine Methoden, keine Associated Types).

Rust Eigener Marker
// Selbstdefinierter Marker
pub trait IstKonstant {}

struct Pi;
struct E;

impl IstKonstant for Pi {}
impl IstKonstant for E {}

// Funktion akzeptiert nur Konstanten
fn akzeptiere_konstante<T: IstKonstant>(_x: T) {
    println!("Eine Konstante");
}

fn main() {
    akzeptiere_konstante(Pi);
    akzeptiere_konstante(E);
    // akzeptiere_konstante(42);   // Compile-Fehler: i32 ist nicht IstKonstant
}

IstKonstant ist ein Marker — Trait ohne Methoden. Der Marker selbst tut nichts; er erlaubt nur, in Bounds zu sagen "dieser Typ ist eine Konstante". Der Compiler erzwingt die Markierung.

Wozu das gut ist: Typ-Klassen ohne Verhalten. Du gruppierst Typen nach einer Eigenschaft (hier: ist eine Konstante), ohne Methoden definieren zu müssen.

Send — Thread-Verschiebbarkeit

Send markiert: ein Wert dieses Typs darf zwischen Threads verschoben werden.

Rust Send-Verwendung
use std::thread;

fn main() {
    let daten = vec![1, 2, 3];      // Vec<i32> ist Send

    thread::spawn(move || {
        println!("{daten:?}");        // OK: Vec<i32> darf in den neuen Thread
    }).join().unwrap();
}

thread::spawn verlangt, dass die übergebene Closure Send ist. Vec ist Send (i32 ist Send, Vec von Send-Items ist Send), also funktioniert es.

Was ist nicht Send?

Rust Nicht-Send-Typen
use std::rc::Rc;

// Rc<T> ist NICHT Send — Reference-Counting ohne Atomic-Operationen
// ist nicht thread-safe
fn main() {
    let rc = Rc::new(42);
    // thread::spawn(move || {
    //     println!("{rc}");
    // });
    // Compile-Fehler: `Rc<i32>` cannot be sent between threads safely
    //                  note: the trait `Send` is not implemented for `Rc<i32>`
    let _ = rc;
}

Klassische nicht-Send-Typen:

  • Rc<T> — Reference-Counter ohne Atomics
  • *const T, *mut T — Raw-Pointers (Sicherheit beim Compiler nicht garantierbar)
  • Typen, die intern Cell / RefCell mit non-Send-Inhalt halten

Send ist ein Auto-Trait: der Compiler implementiert es automatisch für Typen, die nur aus Send-Komponenten bestehen. Du musst es nicht manuell implementieren.

Sync — Thread-Referenz-Sicherheit

Sync markiert: ein Wert dieses Typs darf per geteilter Referenz zwischen Threads benutzt werden. Etwas formaler: T ist Sync, wenn &T Send ist.

Rust Sync-Verwendung
use std::sync::Arc;
use std::thread;

fn main() {
    let daten = Arc::new(vec![1, 2, 3]);    // Arc<Vec<i32>> ist Sync

    let h1 = {
        let daten = Arc::clone(&daten);
        thread::spawn(move || {
            println!("Thread 1: {daten:?}");
        })
    };

    let h2 = {
        let daten = Arc::clone(&daten);
        thread::spawn(move || {
            println!("Thread 2: {daten:?}");
        })
    };

    h1.join().unwrap();
    h2.join().unwrap();
}

Arc<T> ist Sync, wenn T Sync ist — geteilte Referenzen auf das selbe Arc dürfen in mehrere Threads. Ohne Sync wäre das ein Datenrennen.

Wichtige nicht-Sync-Typen:

  • Cell<T>, RefCell<T> — Interior-Mutability ohne Locks
  • Rc<T> — Ref-Counter ohne Atomics

Auch Sync ist Auto-Trait: automatisch implementiert, wenn alle Komponenten Sync sind.

Sized — Größe zur Compile-Zeit bekannt

Sized markiert: die Größe des Typs in Bytes ist zur Compile-Zeit bekannt. Die meisten Typen sind Sized — Ausnahmen sind str, [T], und dyn Trait.

Rust Sized als Default-Bound
// Implizit ist <T: Sized> der Default
fn process<T>(x: T) -> T {
    x
}
// Identisch zu: fn process<T: Sized>(x: T) -> T

// Mit ?Sized erlaubst du auch unsized:
fn process_ref<T: ?Sized>(x: &T) {
    // funktioniert für str, [u8], dyn Trait etc.
}

Jeder Type-Parameter <T> hat implizit den Bound T: Sized. Wenn du explizit auch unsized erlauben willst, musst du ?Sized schreiben (das "?" bedeutet "möglicherweise nicht Sized").

Unsized-Typen können nicht direkt als Wert weitergegeben werden — nur per Referenz, Box, oder andere Container.

Rust Unsized-Typen in der Praxis
// str — die unsized Form von String
let s: &str = "hello";       // Per Referenz
// let s: str = "hello";      // FEHLER: str ist unsized

// [i32] — die unsized Form von Array
let arr: &[i32] = &[1, 2, 3];   // Per Referenz
// let arr: [i32] = [1, 2, 3];   // FEHLER: [i32] ist unsized

// dyn Trait — runtime-polymorphe Werte
// let x: dyn Display = 42;     // FEHLER
let x: Box<dyn std::fmt::Display> = Box::new(42);   // Per Box

Unsized-Werte brauchen immer eine fat-pointer-Indirektion: &str, Box<dyn Trait>, etc. Daher gehen sie nicht als Funktions-Parameter fn(x: str) — die Stack-Größe wäre unbekannt.

Copy — Bit-für-Bit-Kopierbarkeit

Copy markiert: ein Wert kann durch einfaches Kopieren der Bits dupliziert werden (kein move, kein clone-Aufruf).

Rust Copy-Effekt
// i32 ist Copy
fn main() {
    let a: i32 = 42;
    let b = a;          // Kopie, kein Move
    println!("{a}, {b}");  // Beide noch nutzbar

    // String ist NICHT Copy
    let s1 = String::from("hello");
    let s2 = s1;        // Move, nicht Copy!
    // println!("{s1}");   // FEHLER: s1 wurde gemoved
    let _ = s2;
}

Copy-Typen haben Wert-Semantik. Bei Zuweisung wird der Wert dupliziert; beide Variablen sind unabhängig nutzbar.

Welche Typen sind Copy?

  • Alle primitiven numerischen Typen: i8, i16, ..., f64
  • bool, char
  • Tuples aus Copy-Typen
  • Arrays mit Copy-Inhalten (fester Größe)
  • Shared references (&T) — aber nicht &mut T
  • Function-Pointer

Welche nicht?

  • String, Vec, Box, alle Heap-allozierten Container
  • Strukturen, die heap-Daten halten
  • Mutable references (&mut T)

Copy ist ein Supertrait von Clone: trait Copy: Clone. Wenn du Copy implementierst, musst du auch Clone implementieren.

Rust Copy für eigene Typen
#[derive(Copy, Clone)]
struct Punkt {
    x: i32,
    y: i32,
}
// Mit derive: Compiler erzeugt Copy + Clone Impls

fn main() {
    let p1 = Punkt { x: 1, y: 2 };
    let p2 = p1;            // Kopie!
    println!("p1 = ({}, {})", p1.x, p1.y);
    println!("p2 = ({}, {})", p2.x, p2.y);
}

Copy für eigene Typen via #[derive(Copy, Clone)]. Nur möglich, wenn alle Felder Copy sind.

Unpin — bewegbar trotz Pin

Unpin ist subtiler — er kommt bei async/await ins Spiel.

Pin<P> (siehe spätere Kapitel) ist ein Pointer-Wrapper, der verhindert, dass der innere Wert im Speicher bewegt wird. Das ist nötig für selbst-referenzierende Strukturen, die in async-Generated-Code vorkommen.

Aber: die meisten Typen sind eigentlich harmlos bewegbar, auch nach Pin. Diese Typen markiert Unpin — "ich darf bewegt werden, auch wenn ich gepinnt bin".

Rust Unpin in der Praxis
use std::pin::Pin;

// Praktisch alle normalen Typen sind Unpin
fn main() {
    let mut x = 42;
    let pinned: Pin<&mut i32> = Pin::new(&mut x);
    // Weil i32 Unpin ist, ist auch Pin<&mut i32> normal nutzbar
    let _ = pinned;
}

Wenn ein Typ Unpin ist, sind Pin-bezogene Operationen "ungefährlich" — du kannst die innere Mutable-Ref normal extrahieren.

Wenn ein Typ nicht Unpin ist (klassisch: async-Futures mit Selbst-Referenzen), erlaubt Pin keine ungesicherten Bewegungen — der Speicher-Ort des Wertes muss stabil bleiben.

99% der normalen Typen sind Unpin. Nur in async-Internals und unsafe-Code wirst du dem Unterschied begegnen.

Praxis: Marker-Traits im Alltag

Thread-Spawn mit Send-Anforderung

Rust Thread-API
use std::thread;
use std::sync::Arc;
use std::sync::Mutex;

fn parallel_count(items: Vec<i32>) -> usize {
    let shared = Arc::new(Mutex::new(0));

    let handles: Vec<_> = items.into_iter().map(|item| {
        let shared = Arc::clone(&shared);
        thread::spawn(move || {        // Closure muss Send sein
            let mut count = shared.lock().unwrap();
            *count += item.abs();
        })
    }).collect();

    for h in handles {
        h.join().unwrap();
    }

    let result = *shared.lock().unwrap();
    result as usize
}

thread::spawn verlangt F: Send + 'static. Die Closure muss Send sein (alle ihre Capture-Variablen Send) und 'static-Lifetime haben (keine geliehenen Refs aus dem Spawning-Scope).

Sync-Anforderung bei Shared-State

Rust Arc + Sync
use std::sync::Arc;
use std::thread;

fn parallel_print<T: std::fmt::Display + Send + Sync + 'static>(value: T) {
    let arc = Arc::new(value);

    let handles: Vec<_> = (0..3).map(|i| {
        let v = Arc::clone(&arc);
        thread::spawn(move || {
            println!("Thread {i}: {v}");
        })
    }).collect();

    for h in handles {
        h.join().unwrap();
    }
}

parallel_print verlangt T: Send + Sync + 'static. Send für die Move-Operation in den Thread, Sync für die Arc-Sharing-Semantik.

Sized vs unsized in API-Design

Rust API mit ?Sized
use std::fmt::Display;

// Funktion nimmt jede Display-Referenz — auch unsized
pub fn drucke<T: Display + ?Sized>(value: &T) {
    println!("{value}");
}

fn main() {
    drucke(&42);              // T = i32 (Sized)
    drucke("hello");          // T = str (unsized)
    drucke(&String::from("x")); // T = String (Sized)

    let trait_obj: &dyn Display = &42;
    drucke(trait_obj);        // T = dyn Display (unsized)
}

Mit ?Sized machst du eine Funktion maximal flexibel — sie akzeptiert Referenzen auf jeden Display-Typ, inkl. unsized-Typen wie str oder dyn Trait. Klassisches Stdlib-Pattern bei generischen Helpers.

Copy für Wert-Typen

Rust Position
#[derive(Copy, Clone, Debug)]
pub struct Position {
    pub x: i32,
    pub y: i32,
}

impl Position {
    pub fn neu(x: i32, y: i32) -> Self {
        Position { x, y }
    }

    pub fn verschoben(&self, dx: i32, dy: i32) -> Position {
        Position { x: self.x + dx, y: self.y + dy }
    }
}

fn main() {
    let p1 = Position::neu(1, 2);
    let p2 = p1;                  // Kopie, p1 weiterhin nutzbar
    let p3 = p1.verschoben(10, 0);
    println!("{p1:?}, {p2:?}, {p3:?}");
}

Kleine Wert-Strukturen mit primitiven Feldern: ideal für Copy. Wert-Semantik wie bei Numerics.

Custom-Marker für Type-State

Rust Type-State-Marker
// Marker für API-States
pub trait Open {}
pub trait Closed {}

pub struct File<State> {
    path: String,
    _state: std::marker::PhantomData<State>,
}

pub struct OpenState;
pub struct ClosedState;

impl Open for OpenState {}
impl Closed for ClosedState {}

impl File<ClosedState> {
    pub fn neu(path: String) -> Self {
        File { path, _state: std::marker::PhantomData }
    }

    pub fn open(self) -> File<OpenState> {
        File { path: self.path, _state: std::marker::PhantomData }
    }
}

impl File<OpenState> {
    pub fn read(&self) -> String {
        format!("(Daten aus {})", self.path)
    }

    pub fn close(self) -> File<ClosedState> {
        File { path: self.path, _state: std::marker::PhantomData }
    }
}

Marker-Traits als Type-State: nur offene Files können gelesen werden, nur geschlossene können geöffnet werden. Compiler erzwingt die Lifecycle-Reihenfolge.

Send-Auto-Implementation

Rust Auto-Send
// Send wird automatisch abgeleitet, wenn alle Felder Send sind
pub struct Wrapper {
    value: i32,
    name: String,
}

// Wrapper ist automatisch Send + Sync, weil i32 und String es sind
fn check<T: Send + Sync>(_: T) {}

fn main() {
    check(Wrapper { value: 1, name: String::new() });
}

Send und Sync sind Auto-Traits. Solange du keine "exotischen" Felder (Raw-Pointer, Rc, etc.) hast, sind deine eigenen Strukturen automatisch Send + Sync.

Bound mit mehreren Markern

Rust Multi-Bound
use std::fmt::Debug;
use std::hash::Hash;

// Eine Funktion, die viele Marker-Bounds kombiniert
pub fn process_in_thread<T>(value: T)
where
    T: Send + Sync + Debug + Hash + Eq + Clone + 'static,
{
    std::thread::spawn(move || {
        println!("Wert: {value:?}");
    }).join().unwrap();
}

Komplexe Bound-Listen sind in Concurrency-Code üblich. Jeder Marker fügt eine bestimmte Garantie hinzu.

Interessantes

Marker-Trait = Trait ohne Methoden.

Kennzeichnet eine Eigenschaft des Typs, ohne Verhalten zu definieren. Compiler nutzt Marker für Sicherheits-Garantien (Threads, Memory).

Send = darf zwischen Threads verschoben werden.

Voraussetzung für thread::spawn-Closures, tokio::spawn, alle Concurrency-APIs. Auto-Trait: automatisch implementiert, wenn alle Komponenten Send sind. Klassisch nicht-Send: Rc, Raw-Pointer.

Sync = darf per Referenz von mehreren Threads benutzt werden.

T ist Sync ⇔ &T ist Send. Auto-Trait. Klassisch nicht-Sync: Cell, RefCell, Rc — alle Interior-Mutability ohne Locks.

Sized = Größe zur Compile-Zeit bekannt — der Standard.

Jeder generic Parameter hat implizit T: Sized. Mit T: ?Sized erlaubst du auch unsized-Typen (str, [T], dyn Trait), die nur per Referenz benutzbar sind.

Copy = Bit-für-Bit-Kopie statt Move.

Wert-Semantik. Klassisch für primitive Typen (i32, bool, char). Eigene Typen via #[derive(Copy, Clone)], nur möglich wenn alle Felder Copy sind. Copy ist Supertrait von Clone.

Unpin = darf bewegt werden, auch nach Pin.

99% der Typen sind Unpin. Nur async-Futures mit Selbst-Referenzen sind !Unpin. In normalem Code irrelevant; in async/Pin-internals zentrale Frage.

Auto-Traits — automatisch implementiert.

Send, Sync, Unpin sind Auto-Traits: der Compiler implementiert sie automatisch für Typen, deren Komponenten sie haben. Du kannst opt-out mit impl !Send for MyType {} (unsafe-Code-Variante).

Marker-Traits in Bounds — die Concurrency-Sprache.

T: Send + Sync + 'static ist der typische Bound für Werte, die in Threads landen sollen. Mit Marker-Bounds drückst du Sicherheits-Garantien in der Signatur aus.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Traits

Zur Übersicht