Höherwertige Funktionen sind Funktionen, die andere Funktionen als Parameter annehmen oder sie als Ergebnis zurückgeben. Rust hat von Anfang an großen Wert auf Typsicherheit und Leistung gelegt, was sich in der Arbeit mit solchen Funktionen widerspiegelt.
Geschichte der Frage:
In funktionalen Sprachen gelten höherwertige Funktionen als Standard, in vielen systemnahen Sprachen führten sie jedoch häufig zu Leistungseinbußen (z. B. aufgrund von Allokationen oder der Unfähigkeit, Code "inline" zu setzen). In Rust wird diese Funktionalität durch ein strenges Typsystem, statische Dispatching oder Traits (Fn, FnMut, FnOnce) umgesetzt, was in den meisten Fällen Overheads vermeidet.
Problem:
Das Hauptproblem besteht darin, eine Funktion oder Closure zu übergeben, während die Typsicherheit, die Möglichkeit, Variablen zu erfassen (die Leichtigkeit von Lambda-Ausdrücken), und die Leistung ohne Allokationen oder virtuelle Aufrufe gewahrt bleibt.
Lösung:
In Rust werden höherwertige Funktionen durch generische Parameter und Trait-Wrapper für Funktionen/Closures implementiert. Die Standard-Traits Fn, FnMut und FnOnce ermöglichen eine sehr präzise Deklaration der Anforderungen an die übergebene Funktion (ob sie das Umfeld ändern oder konsumieren kann). Die Übergabe über Generics erlaubt es, Aufrufe zur Kompilierungszeit inline zu setzen. Es gibt auch eine dynamische Dispatching-Möglichkeit über Box<dyn Fn...>, wenn der Typ im Voraus nicht bekannt ist.
Beispielcode:
fn apply_to_vec<F: Fn(i32) -> i32>(v: Vec<i32>, f: F) -> Vec<i32> { v.into_iter().map(f).collect() } let nums = vec![1, 2, 3]; let doubled = apply_to_vec(nums, |x| x * 2); // doubled == [2, 4, 6]
Wesentliche Merkmale:
Was sind die Unterschiede zwischen Fn, FnMut und FnOnce?
Viele denken, sie unterscheiden sich nur syntaktisch oder dass Fn und FnMut alles austauschbar machen können. In Wirklichkeit:
FnOnce kann nur einmal aufgerufen werden (z. B. wenn das Lambda einen erfassten Wert verschiebt).FnMut kann den Zustand der erfassten Umgebung ändern, kann aber mehrmals aufgerufen werden.Fn verändert nicht das Umfeld.Beispiel:
let mut sum = 0; let mut add = |x| { sum += x; }; // add implementiert FnMut, aber nicht Fn
Kann man eine Funktion ohne Boxing als Wert übergeben?
Oft wird gedacht, dass alle Funktionsargumente unbedingt boxed (Box<dyn Fn...>) sein müssen. In Wirklichkeit ist Boxing NUR für dynamisches Dispatching notwendig, wenn der Typ bis zur Ausführung nicht bekannt ist. Durch generische Parameter kann die Funktion vollständig statisch typisiert werden, ohne Allokationen und Boxing.
Wann hört eine Closure auf, Copy zu sein?
Einige glauben, dass eine einfache Closure immer Copy oder Clone ist, wenn die Variable innerhalb Copy ist. In Wirklichkeit sind Closures standardmäßig nicht Copy, selbst wenn die erfassten Variablen Copy sind. Man muss den Trait explizit implementieren oder sich mit einfachen Funktionen behelfen.
Im Projekt wurden für alle Rückrufe in Sammlungen nur Box<dyn Fn()> verwendet, ohne über Inlining und Allokationen nachzudenken. Infolgedessen konnte kein Leistungsverlust erzielt werden, häufige Allokationen führten zu Verzögerungen.
Vorteile:
Nachteile:
Ereignis-Handler wurden über generische Funktionen mit Trait-Beschränkung FnMut konfiguriert, völlig ohne Allokationen.
Vorteile:
Nachteile: