Ferris Talk #3: Neue Rust-Edition 2021 ist da – Closures, das Cinderella-Feature

Seite 2: Einweg-Aufruf und Mutable Borrow: FnOnce und FnMut

Inhaltsverzeichnis

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.

  • FnMut kann die gebundenen Variablen ändern (Mutable Borrow).
  • Fn kann die gebundenen Variablen nicht ändern (Immutable Borrow).

Empfohlener redaktioneller Inhalt

Mit Ihrer Zustimmmung 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);
}

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)());
}

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);
}