Closures
If you know about closures already, this chapter will hold no big surprises for you, they function about how you would expect. However, some Rust features rely heavily on them, so of course there's a chapter for them. If you don't know what closures are, they go by "lambdas" in some languages like C++.
In short, they are anonymous functions which can capture parts of the scope they are defined in. They are written as two pipes with their arguments in between, followed by an expression evaluating to data of their return type. Argument types and return type should be able to be inferred in almost all cases, but you can also make them explicit if you want to. Their main use is in customising behaviour of some functions, which we will see extensively shortly. Depending on what they capture from their environment, they implement one of three traits:
Fn
doesn't capture anything at all, or only immutable references. Regular functions implement this trait, too. Closures implementing this can be used where the following traits are expected, as well.FnMut
captures mutable references of the environment, so it can mutate them. Closures implementing this can be used anywhere whereFnOnce
is expected, too.FnOnce
fully captures and consumes parts of the environment, as it moves it into itself. As the name and the ownership rules imply, this type of closure can only be called once. Closures implementing this have amove
keyword in front of it.
One important use is in spawning a thread. As spawned threads start working immediately, they don't take functions with arguments (which would immediately need those arguments anyway), but instead a closure that can capture any items the thread needs.
#![allow(unused)] fn main() { let (sender, receiver) = std::sync::mpsc::channel(); // Closure captures sender and returns a boolean let thread = std::thread::spawn(move || { if let Ok(()) = sender.send(String::from("Hello world!")) { true } else { false } }); let success = thread.join().unwrap(); if success { println!("Message from thread: {}", receiver.recv().unwrap()); } }
The second important use case is when sorting or searching for items. When they don't have Ord
implemented, but contain a value that does and you want to use that, you can use the *_by_key
version of the respective function.
It accepts a closure that takes an item reference as its argument and returns one that implements the trait.
You can also define the entire comparison function yourself by using the *_by
versions of those functions. Of course, we're going to need a nice little example for that, don't we?
#![allow(unused)] fn main() { use std::cmp::Ordering; struct MyStruct { pub id: usize, pub some_data: usize, } // Let's just assume that we get some // data from somewhere else fn example(data: &mut [MyStruct]) { // Doesn't work, we don't have the // Ord trait implemented! data.sort(); // Assume we only want to sort by the ID data.sort_by_key(|item| item.id); // Assume we want to sort by ID first // and data second data.sort_by(|first, second| { match first.id.cmp(&second.id) { Ordering::Equal => first.some_data.cmp(&second.some_data), other => other } }); } }
The third important use case is iterators, which we will go into now.