ProgrammingRust Developer

How are closures implemented in Rust? What types of closures exist, how do they differ, and when are they used?

Pass interviews with Hintsage AI assistant

Answer

In Rust, closures are anonymous functions that can "capture" variables from the surrounding scope. The syntax is:

let add = |a: i32, b: i32| a + b;

There are three types of closures in Rust (distinguished by how they capture variables):

  • Fn: captures references (&T), can be called multiple times without mutating the environment.
  • FnMut: captures mutable references (&mut T), can modify the closure’s data when called.
  • FnOnce: can only be called once, as it takes ownership of the captured data (T).

Rust automatically determines the required type, but you can specify it explicitly.

Example of differences:

let s = String::from("hello"); // FnOnce let consume = move || println!("{}", s); // s is no longer accessible after the call

Trick Question

Why can a closure with the keyword move not necessarily be FnOnce, but instead be Fn or FnMut?

Answer: The move keyword transfers variable capture by ownership, but if the data inside the closure is not mutated and not dropped, the closure remains compatible with Fn/FnMut. For example:

let s = String::from("abc"); let c = move || println!("String: {}", s); // c: impl Fn() c();

c can be called multiple times: the value of s is copied into the closure but not destroyed.

Examples of real errors due to ignorance of nuances of the topic


Story

A beginner Rust developer in a data streaming project tried to write a closure without move, passing it to another thread. As a result, the compiler complained about outlived borrow, and they had to urgently figure out the intricacies of lifetimes and move.


Story

They introduced mutability inside the closure without changing the type to FnMut, which caused "mismatched types" in all instances of using iterators.


Story

When processing a large array, the closure took ownership of the entire collection by move, unconsciously moving ownership, which resulted in the outer code losing access to the data after the first iteration, leading to attempts to access moved values.