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
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
use std::mem::size_of;
println!("{}", size_of::<fn(i32) -> i32>()); // 8 (auf 64-bit)
println!("{}", size_of::<Option<fn(i32) -> i32>>()); // 8 — Niche-OptimizationEin 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
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
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:
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, nichtfn(...).
Das ist die Stelle, an der die Fn-Traits ins Spiel kommen.
Die Fn-Trait-Familie
Drei Traits beschreiben „aufrufbare Werte":
| Trait | Bedeutet |
|---|---|
Fn(Args) -> Output | Mehrfach aufrufbar, captures werden nur gelesen |
FnMut(Args) -> Output | Mehrfach aufrufbar, captures dürfen mutiert werden |
FnOnce(Args) -> Output | Nur einmal aufrufbar, kann captures verbrauchen |
Hierarchie: Fn ⊆ FnMut ⊆ FnOnce. Wer Fn ist, ist automatisch auch FnMut und FnOnce. Wer FnMut ist, ist auch FnOnce.
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
let f: fn(i32) -> i32 = |x| x + 1; // Closure ohne CaptureFunktioniert nur bei Closures ohne Captures.
Methode → fn-Pointer
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:
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
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
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
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
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
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
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
- Rust Reference – Function types
- Rust Reference – Closures
- std::ops::Fn
- std::ops::FnMut
- std::ops::FnOnce
- The Rustonomicon – Function Pointers