Ferris Talk #3: Neue Rust-Edition 2021 ist da – Closures, das Cinderella-Feature
Seite 2: Einweg-Aufruf und Mutable Borrow: FnOnce und FnMut
Der Hinweis auf den Trait Fn war vereinfacht: In Wirklichkeit gibt es nicht nur Fn, sondern drei Closure-bezogene Traits, aus denen der Rust-Compiler je nach Verwendungszweck der gebundenen Variablen auswählt:
FnOnce übernimmt die Ownership der gebundenen Variablen, konsumiert sie also. Der Zusatz Once verdeutlicht, dass eine FnOnce-Closure sich nur einmal aufrufen lässt. Jede Closure ist kompatibel mit FnOnce, da jede Closure sich mindestens einmal aufrufen lässt.
FnMutkann die gebundenen Variablen ändern (Mutable Borrow).Fnkann die gebundenen Variablen nicht ändern (Immutable Borrow).
Empfohlener redaktioneller Inhalt
Mit Ihrer Zustimmung wird hier ein externes YouTube-Video (Google Ireland Limited) geladen.
Ich bin damit einverstanden, dass mir externe Inhalte angezeigt werden. Damit können personenbezogene Daten an Drittplattformen (Google Ireland Limited) übermittelt werden. Mehr dazu in unserer Datenschutzerklärung.
Das folgende Beispiel zeigt exemplarisch eine Closure, die eine gebundene Variable result ändert. Es ist also eine Closure mit Mutable Borrow, der entsprechende Trait ist daher FnMut, nicht mehr wie in den oben gezeigten Beispielen Fn.
fn main() {
let mut result = 0;
// Closure with mutable borrow
let mut calc_result = |x, y| { result = x + y; };
calc_result(1, 2);
println!("{}", result);
// Store closure in `FnMut` variable before calling it
let mut result_calculator: Box<dyn FnMut(i32, i32)>
= Box::new(|x, y| { result = x + y; });
result_calculator(1, 2);
drop(result_calculator);
println!("{}", result);
}
Wenn die Closure die Ownership ĂĽber alle gebundenen Variablen ĂĽbernehmen soll, gibt man vor der Closure das SchlĂĽsselwort move an. Eine solche Closure implementiert dann den FnOnce-Trait. Das folgende Beispiel zeigt den Zusammenhang:
fn main() {
// Closure consuming iterator
let numbers_iter = vec![1, 2, 3, 5, 6].into_iter();
let sum_calculator = move || numbers_iter.sum();
let result: i32 = sum_calculator();
println!("{}", result);
// Store closure in [code]FnOnce[/code] variable before calling it
let numbers_iter = vec![1, 2, 3, 5, 6].into_iter();
let sum_calculator: Box<dyn FnOnce() -> i32>
= Box::new(move || numbers_iter.sum());
let result: i32 = sum_calculator();
println!("{}", result);
}
Besonders relevant ist move in Verbindung mit Multithreading. Das folgende Beispiel zeigt, wie die main-Methode die Ownership ĂĽber zwei Variablen (counter und counter_string) an die Closure ĂĽbergibt, die im Hintergrund-Thread ausgefĂĽhrt wird. In der Praxis lieĂźe sich damit beispielsweise ein Channel fĂĽr Interprozesskommunikation an einen Thread weitergeben.
use std::time::Duration;
use std::thread;
fn main() {
let mut counter = 0;
let mut counter_string = String::new();
// Move ownership of captured variables into child thread.
let background = thread::spawn(move || {
loop {
counter = counter + 1;
// Add counter to counter string
if counter_string.len() > 0 {
counter_string.push_str(", ");
}
counter_string.push_str(&counter.to_string());
// Print counter string on the screen
println!("{}", counter_string);
// Exit after five iterations
if counter == 5 { break; }
// For demo purposes, wait a moment
thread::sleep(Duration::from_millis(100));
}
});
// Wait for background thread to complete
background.join().unwrap();
// The next line would not compile because ownership
// has been moved into the child thread's closure.
//println!("{}", counter_string);
}
Disjoint Capture in Closures
In Rust 2021 wurde die Logik des Capturings der Variablen in den Closures verfeinert. Der technische Name der neuen Logik ist Disjoint Capture in Closures. Sie führt dazu, dass Closures nicht mehr Ownership über die gesamte Struktur übernehmen, wenn ihr Code nur auf Teile der Struktur zugreift. Klarer wird das an einem kurzen Codebeispiel. Es kompiliert nicht mit Rust 2018. Erst mit Rust 2021 lässt sich der Code ausführen. Der Beispielcode enthält Kommentarzeilen, in denen der Unterschied zwischen den Rust-Editionen in Hinblick auf das Capturing in den Closures erklärt wird.
struct Something {
anything: String,
anything_else: String,
}
impl Something {
fn new() -> Something {
Something {
anything: "Foo".to_string(),
anything_else: "Bar".to_string()
}
}
}
fn main() {
let st = Something::new();
drop(st.anything);
// In Rust 2018, the following closure captures st.
// Therefore, the code does not compile because a part of
// st has already been dropped (see previous line of code).
// In Rust 2021, the closure only captures st.anything_else.
// Therefore, the code compiles.
println!("{}", (|| st.anything_else)());
}
Aschenputtels Linsen: Rust 2021 und die Capturing-Logik einfach erklärt
Es stellt sich die Frage, ob die neue Rust-Sprachfunktion in der Praxis überhaupt eine Rolle spielt. Tatsächlich ermöglicht sie Datenstrukturen, die sich bisher in Rust mit Closures nicht verarbeiten ließen, wie das folgende Beispiel zeigt. Es bildet ein Problem aus dem Märchen Aschenputtel ab: Am Boden liegen Linsen (Lentils), und Tauben sollen die schlechten, kleinen Linsen fressen, die guten, großen Linsen hingegen übrig lassen. Linsen und die "Verarbeitungslogik" für die Tauben sind in der Datenstruktur CinderellaTask zusammengefasst. In der Methode sort_lentils wird die Verarbeitungslogik auf alle Linsen angewandt, und genau dabei kommt die neue Capturing-Logik von Rust 2021 zum Einsatz. Erst sie ermöglicht, dass Daten (lentils) und Closure (eat) in einer Struktur zusammengefasst sind.
#[derive(Debug)]
struct Lentil {
size: f32
}
struct CinderellaTask {
lentils: Vec<Lentil>,
eat: Box<dyn Fn(&Lentil) -> bool>,
}
impl CinderellaTask {
fn sort_lentils(&mut self) {
// The following line works in Rust 2021, it does not in 2018.
// In the 2018 edition, the closure captures the entire [code]self[/code].
// In the 2021 edition, the closure just captures [code]self.eat[/code].
// So using [code]self.lentils[/code] outside is now possible.
self.lentils.retain(|l| !(self.eat)(l));
}
}
fn main() {
let lentils = vec![
Lentil{ size: 5.0 },
Lentil{ size: 6.0 },
Lentil{ size: 1.0 },
Lentil{ size: 2.0 },
];
let mut task = CinderellaTask {
lentils: lentils,
eat: Box::new(|l| l.size < 5.0),
};
task.sort_lentils();
println!("{:?}", task.lentils);
}