Funktionen in Rust sind First-Class-Werte. Eine Funktion hat einen Typ — geschrieben fn(T) -> U — der wie jeder andere Typ in Variablen gespeichert, als Parameter übergeben oder als Rückgabe zurückgegeben werden kann. Das ist die Grundlage für Higher-Order-Patterns: Callbacks, Strategy-Pattern, Funktions-Tables. Verwandt, aber nicht identisch sind Closures, die zusätzlich Variablen aus ihrem Scope einfangen können. Dieser Artikel zeigt fn-Pointer in Reinform, ordnet sie gegen Closures ab und erklärt die Coercion-Regeln zwischen beiden.

fn-Pointer — Funktionen als Werte

Rust Funktion in Variable
fn doppelt(x: i32) -> i32 { x * 2 }
fn quadrat(x: i32) -> i32 { x * x }

fn main() {
    let mut f: fn(i32) -> i32 = doppelt;
    println!("{}", f(5));        // 10
    f = quadrat;
    println!("{}", f(5));        // 25
}

Der Typ fn(i32) -> i32 ist der Funktions-Pointer-Typ: eine Variable dieses Typs hält die Adresse einer Funktion mit passender Signatur. Eine Funktions-Pointer-Variable kann mit jeder passenden Funktion neu beschrieben werden.

Größe und Eigenschaften

Rust Größe
use std::mem::size_of;

println!("{}", size_of::<fn(i32) -> i32>());       // 8 (auf 64-bit)
println!("{}", size_of::<Option<fn(i32) -> i32>>()); // 8 — Niche-Optimization

Ein fn-Pointer ist 8 Bytes (ein Pointer auf 64-bit-Maschinen). Option<fn(...)> ist genauso groß — der Null-Pointer dient als None-Marker.

fn-Pointer als Parameter

Rust Funktion nimmt Funktion
fn anwenden(werte: &[i32], op: fn(i32) -> i32) -> Vec<i32> {
    werte.iter().map(|&x| op(x)).collect()
}

fn doppelt(x: i32) -> i32 { x * 2 }
fn negativ(x: i32) -> i32 { -x }

fn main() {
    let v = vec![1, 2, 3];
    assert_eq!(anwenden(&v, doppelt), vec![2, 4, 6]);
    assert_eq!(anwenden(&v, negativ), vec![-1, -2, -3]);
}

op: fn(i32) -> i32 ist ein klar definierter Parameter-Typ. Der Aufrufer übergibt eine konkrete Funktion (kein &-Operator nötig — Funktionen sind direkt als Wert übergebbar).

Stable Sort mit fn-Pointer

Rust Sort-Comparator
use std::cmp::Ordering;

fn nach_laenge(a: &&str, b: &&str) -> Ordering {
    a.len().cmp(&b.len())
}

fn main() {
    let mut woerter = vec!["Welt", "Hi", "Schöne"];
    woerter.sort_by(nach_laenge);
    assert_eq!(woerter, vec!["Hi", "Welt", "Schöne"]);
}

sort_by nimmt einen FnMut-Comparator. fn-Pointer implementieren alle drei Fn-Traits (siehe nächster Abschnitt), also passen sie problemlos.

fn-Pointer vs. Closure

Closures sind verwandt, aber nicht identisch:

Rust Closure vs. fn
fn als_fn_pointer(x: i32) -> i32 { x * 2 }

fn main() {
    // fn-Pointer
    let fp: fn(i32) -> i32 = als_fn_pointer;

    // Closure ohne Capture — kann zu fn-Pointer gecoerced werden
    let c1: fn(i32) -> i32 = |x| x * 2;     // ok

    // Closure mit Capture — KANN NICHT fn-Pointer sein
    let faktor = 3;
    // let c2: fn(i32) -> i32 = |x| x * faktor;  // Fehler — captures faktor
    let c2 = |x: i32| x * faktor;                // ok — anderer Typ
}

Wichtige Regel:

  • Closure ohne Capture — kann zu fn(...) gecoerced werden.
  • Closure mit Capture — hat einen anonymen Typ, der nur die Fn-Traits implementiert, nicht fn(...).

Das ist die Stelle, an der die Fn-Traits ins Spiel kommen.

Die Fn-Trait-Familie

Drei Traits beschreiben „aufrufbare Werte":

TraitBedeutet
Fn(Args) -> OutputMehrfach aufrufbar, captures werden nur gelesen
FnMut(Args) -> OutputMehrfach aufrufbar, captures dürfen mutiert werden
FnOnce(Args) -> OutputNur einmal aufrufbar, kann captures verbrauchen

Hierarchie: FnFnMutFnOnce. Wer Fn ist, ist automatisch auch FnMut und FnOnce. Wer FnMut ist, ist auch FnOnce.

Rust Generischer Callback
fn anwenden_generisch<F: Fn(i32) -> i32>(werte: &[i32], op: F) -> Vec<i32> {
    werte.iter().map(|&x| op(x)).collect()
}

fn main() {
    // Funktioniert mit fn-Pointer
    fn doppelt(x: i32) -> i32 { x * 2 }
    let v = anwenden_generisch(&[1, 2, 3], doppelt);

    // Funktioniert mit Closure ohne Capture
    let v2 = anwenden_generisch(&[1, 2, 3], |x| x + 10);

    // Funktioniert auch mit Closure mit Capture
    let faktor = 5;
    let v3 = anwenden_generisch(&[1, 2, 3], |x| x * faktor);
}

F: Fn(i32) -> i32 ist der allgemeinste Parameter-Typ — akzeptiert alle drei oben. Faustregel: Schreibe generisch über die Fn-Traits, wenn du Closures akzeptieren willst. Nimm fn(...)-Pointer nur, wenn du explizit nur Funktionen ohne Capture willst (z. B. für C-FFI).

fn-Pointer-Coercion

Es gibt mehrere Coercion-Regeln:

Closure ohne Capture → fn-Pointer

Rust Coercion
let f: fn(i32) -> i32 = |x| x + 1;    // Closure ohne Capture

Funktioniert nur bei Closures ohne Captures.

Methode → fn-Pointer

Rust Method-Reference
struct Zaehler(u32);

impl Zaehler {
    fn doppelt(self) -> u32 { self.0 * 2 }
}

fn main() {
    let f: fn(Zaehler) -> u32 = Zaehler::doppelt;
    assert_eq!(f(Zaehler(5)), 10);
}

Methoden ohne Capture können als fn-Pointer verwendet werden — der self-Receiver wird zum ersten Argument.

extern und FFI

fn-Pointer sind besonders wichtig für C-FFI. Wenn eine C-Library einen Callback-Parameter erwartet, brauchst du einen fn-Pointer mit C-ABI:

Rust extern fn
extern "C" fn rust_callback(x: i32) -> i32 {
    x.wrapping_mul(2)
}

extern "C" {
    fn c_register_callback(cb: extern "C" fn(i32) -> i32);
}

fn main() {
    unsafe {
        c_register_callback(rust_callback);
    }
}

extern "C" fn(...) ist ein fn-Pointer mit C-ABI. Closures haben keine C-ABI — daher sind sie für FFI-Callbacks ungeeignet (außer in seltenen Fällen über Trampoline-Funktionen).

Praxis: Funktions-Pointer im echten Code

Dispatcher-Tabelle für CLI-Kommandos

Rust Command-Dispatch
fn cmd_status(args: &[&str]) -> i32 {
    println!("Status: alle Systeme grün ({} args)", args.len());
    0
}

fn cmd_restart(_args: &[&str]) -> i32 {
    println!("Restart läuft...");
    0
}

fn cmd_help(_args: &[&str]) -> i32 {
    println!("Verfügbare Befehle: status, restart, help");
    0
}

type CmdFn = fn(&[&str]) -> i32;

const KOMMANDOS: &[(&str, CmdFn)] = &[
    ("status", cmd_status),
    ("restart", cmd_restart),
    ("help", cmd_help),
];

fn dispatch(name: &str, args: &[&str]) -> Option<i32> {
    KOMMANDOS.iter()
        .find(|(n, _)| *n == name)
        .map(|(_, f)| f(args))
}

fn main() {
    let code = dispatch("status", &["--verbose"]).unwrap_or(127);
    std::process::exit(code);
}

Klassisches Dispatch-Pattern: eine Tabelle von (Name → Funktion), Lookup über find. Mit fn-Pointer (nicht Closure), weil die Tabelle als const initialisiert wird.

Event-Handler-Registry

Rust Event-Hooks
pub type Handler = fn(&str);

pub struct EventBus {
    handlers: Vec<(String, Handler)>,
}

impl EventBus {
    pub fn neu() -> Self {
        EventBus { handlers: Vec::new() }
    }

    pub fn registrieren(&mut self, typ: &str, h: Handler) {
        self.handlers.push((typ.to_string(), h));
    }

    pub fn emit(&self, typ: &str, payload: &str) {
        for (t, h) in &self.handlers {
            if t == typ { h(payload); }
        }
    }
}

fn auf_login(p: &str) { println!("Login-Event: {p}"); }
fn auf_logout(p: &str) { println!("Logout-Event: {p}"); }

fn main() {
    let mut bus = EventBus::neu();
    bus.registrieren("login", auf_login);
    bus.registrieren("logout", auf_logout);
    bus.emit("login", "user=42");
}

fn-Pointer als Handler-Typ. Wer mehr Flexibilität braucht (Captures, Mutation), wechselt zu Box<dyn Fn(&str)> — siehe Closures-Kapitel.

Strategy-Pattern für Sortierung

Rust Sort-Strategy
use std::cmp::Ordering;

type Comparator<T> = fn(&T, &T) -> Ordering;

struct Liste<T> { items: Vec<T> }

impl<T> Liste<T> {
    fn sortieren(&mut self, cmp: Comparator<T>) {
        self.items.sort_by(cmp);
    }
}

fn nach_länge(a: &String, b: &String) -> Ordering {
    a.len().cmp(&b.len())
}

fn alphabetisch(a: &String, b: &String) -> Ordering {
    a.cmp(b)
}

fn main() {
    let mut l = Liste { items: vec!["banana".to_string(), "apple".into(), "fig".into()] };
    l.sortieren(nach_länge);
    assert_eq!(l.items, vec!["fig", "apple", "banana"]);
    l.sortieren(alphabetisch);
    assert_eq!(l.items, vec!["apple", "banana", "fig"]);
}

Verschiedene Sortier-Strategien per fn-Pointer auswählbar — klares Strategy-Pattern, ohne Trait-Hierarchie.

Plugin-Hooks mit fester ABI

Rust Plugin-Style
pub type LifecycleHook = extern "C" fn() -> i32;

pub struct Plugin {
    pub name: &'static str,
    pub on_start: Option<LifecycleHook>,
    pub on_stop: Option<LifecycleHook>,
}

extern "C" fn meines_start() -> i32 {
    println!("Plugin gestartet");
    0
}

pub const MEIN_PLUGIN: Plugin = Plugin {
    name: "mein-plugin",
    on_start: Some(meines_start),
    on_stop: None,
};

extern "C" fn ermöglicht stabile ABI für dynamisch geladene Plugins. Sehr typisch in DLL-basierten Architekturen.

Higher-Order-Funktion ohne Closure-Overhead

Rust Inline-freundlich
pub fn apply_to_all(daten: &mut [i32], op: fn(i32) -> i32) {
    for x in daten.iter_mut() {
        *x = op(*x);
    }
}

fn inc(x: i32) -> i32 { x + 1 }

fn main() {
    let mut v = vec![1, 2, 3];
    apply_to_all(&mut v, inc);
    assert_eq!(v, vec![2, 3, 4]);
}

fn-Pointer hat fixe Größe (8 Bytes). Wenn die Funktion nicht generisch sein muss, ist fn(...) als Parameter-Typ kompakter im Maschinencode als ein impl Fn(...)-Trait-Bound, der Monomorphisierung auslöst.

Zustandsmaschine mit fn-Tabelle

Rust State-Machine
type StateFn = fn(input: char) -> State;

#[derive(Debug, Clone, Copy)]
enum State { Start, Mitte, Ende, Fehler }

fn von_start(c: char) -> State {
    match c {
        'a' => State::Mitte,
        _ => State::Fehler,
    }
}

fn von_mitte(c: char) -> State {
    match c {
        'b' => State::Ende,
        _ => State::Fehler,
    }
}

fn dispatch(state: State, c: char) -> State {
    let f: StateFn = match state {
        State::Start => von_start,
        State::Mitte => von_mitte,
        _ => return state,
    };
    f(c)
}

fn main() {
    let mut s = State::Start;
    for c in "ab".chars() {
        s = dispatch(s, c);
    }
    assert!(matches!(s, State::Ende));
}

Eine Zustandsmaschine, in der jeder Zustand seine eigene Transition-Funktion hat. fn-Pointer als Wert macht den Code lesbar.

Häufige Stolperfallen

fn(...)-Pointer und Closures sind verschiedene Typen.

Eine Closure mit Capture ist kein fn-Pointer. Sie implementiert nur Fn/FnMut/FnOnce. Wer fn-Pointer als Parameter-Typ verlangt, schließt damit jede capturing Closure aus — was manchmal genau gewollt ist (FFI), oft aber zu unnötiger Einschränkung führt.

Closure ohne Capture coerciert automatisch zu fn(...).

let f: fn(i32) -> i32 = |x| x + 1; funktioniert. Sobald die Closure aber eine Variable einfängt, klappt es nicht mehr — der Compiler sagt „expected fn pointer, found closure".

Generische Funktion mit F: Fn(...) ist meist flexibler.

fn foo<F: Fn(i32) -> i32>(f: F) akzeptiert fn-Pointer, Closures mit Capture, Methoden — alles, was den Trait implementiert. Nur in Spezialfällen (FFI, fester Typ für Storage in Struct) ist fn(...) direkt der bessere Parameter-Typ.

fn(...) implementiert alle drei Fn-Traits.

Ein fn-Pointer ist gleichzeitig Fn, FnMut und FnOnce. Das macht ihn kompatibel mit den meisten Higher-Order-APIs. Closures mit Mutation sind nur FnMut/FnOnce.

fn-Pointer haben Größe eines normalen Pointers.

8 Bytes auf 64-bit. Klein, kopierbar, kein Heap. Verglichen mit Box<dyn Fn(...)> (16 Bytes, plus Heap-Allokat) ist fn(...) sehr leichtgewichtig — aber nicht so flexibel.

extern "C" fn ist ein anderer Typ als fn.

Die ABI ist Teil des Typs. fn(i32) -> i32 und extern "C" fn(i32) -> i32 sind nicht zuweisungs-kompatibel. Pflicht bei FFI: ABI-bewusst deklarieren.

Eine Funktion kann sich selbst als fn-Pointer rückgeben.

Rekursive Higher-Order-Pattern sind möglich: eine Funktion gibt einen Pointer auf sich selbst oder eine andere fn-Funktion zurück. State-Machines à la „mache X, dann gib Pointer auf nächsten Step zurück" werden so kompakt.

Option ist nicht größer als fn(...).

Niche-Optimization nutzt den Null-Pointer als None-Marker. Damit kostet ein optionaler Callback in einem Struct nichts extra. Klassisch im Plugin-Design (Hook ist optional).

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Funktionen

Zur Übersicht