Closures avec État en Rust: Passer et Muter à Travers Plusieurs Appels
Table des matières
Pour passer une closure à une fonction Rust qui doit l'appeler plusieurs fois tout en maintenant l'état entre les appels, la closure doit implémenter le trait FnMut pour permettre la mutation de son environnement capturé. Je vais expliquer comment concevoir cela, en utilisant l'ownership, les traits et les lifetimes de Rust, et mettre en évidence quand utiliser des closures simples versus des approches structurées.
Solution: Utiliser FnMut et Closure Mutable
Une closure qui mute l'état doit implémenter FnMut, qui permet plusieurs appels avec accès mutable aux variables capturées. La fonction qui reçoit la closure la prend comme &mut impl FnMut pour retenir l'ownership tout en permettant la mutation.
Exemple :
fn call_repeatedly<F: FnMut() -> i32>(f: &mut F) {
println!("First call: {}", f()); // 1
println!("Second call: {}", f()); // 2
}
fn main() {
let mut counter = 0; // État stocké en dehors de la closure
let mut closure = || {
counter += 1; // Mute l'état capturé → `FnMut`
counter
};
// Passer comme `&mut closure` pour retenir ownership
call_repeatedly(&mut closure);
// closure peut encore être utilisée ici
println!("After: {}", closure()); // 3
}
Mécaniques Clés
- État Mutable : La closure capture
countervia un mutable borrow (&mut i32). La closure elle-même est déclaréemutpour permettre la mutation. - Signature de Function :
fn call_repeatedly<F: FnMut() -> i32>(f: &mut F)assure que la closure peut être appelée plusieurs fois avec accès mutable. - Sécurité des Lifetimes : La closure emprunte
counter, donc elle ne peut pas outlivecounter, appliqué par le borrow checker de Rust.
Alternative: Encapsuler l'État dans une Struct
Pour un état complexe, encapsule-le dans une struct avec une implémentation FnMut explicite :
struct Counter {
count: i32,
}
impl Counter {
fn new() -> Self {
Counter { count: 0 }
}
fn call(&mut self) -> i32 {
self.count += 1;
self.count
}
}
fn main() {
let mut counter = Counter::new();
// Closure qui capture counter par mutable borrow
let mut closure = || counter.call();
call_repeatedly(&mut closure);
println!("After: {}", counter.call()); // Continue l'état
}
Exemples Avancés
1. État Complexe avec Plusieurs Variables
fn demonstrate_complex_state() {
let mut sum = 0;
let mut count = 0;
let mut max_seen = i32::MIN;
let mut accumulator = |value: i32| {
sum += value;
count += 1;
max_seen = max_seen.max(value);
// Retourne moyenne, count, et max
(sum as f64 / count as f64, count, max_seen)
};
// Function qui utilise notre closure stateful
fn process_values<F>(mut processor: F, values: &[i32])
where
F: FnMut(i32) -> (f64, i32, i32),
{
for &value in values {
let (avg, count, max) = processor(value);
println!("Value: {}, Avg: {:.2}, Count: {}, Max: {}",
value, avg, count, max);
}
}
let values = vec![10, 5, 15, 3, 20];
process_values(&mut accumulator, &values);
}
2. Closure avec État dans des Collections
fn demonstrate_closure_collection() {
// Vector de closures stateful
let mut counters: Vec<Box<dyn FnMut() -> i32>> = Vec::new();
// Créer plusieurs closures avec états indépendants
for i in 0..3 {
let mut count = i * 10; // État initial différent
counters.push(Box::new(move || {
count += 1;
count
}));
}
// Appeler chaque closure
for (i, counter) in counters.iter_mut().enumerate() {
println!("Counter {}: {}", i, counter()); // 1, 11, 21
println!("Counter {}: {}", i, counter()); // 2, 12, 22
}
}
3. Closure Stateful avec Move
use std::collections::HashMap;
fn demonstrate_move_stateful() {
// Créer une closure qui maintient un cache
fn create_cached_processor() -> impl FnMut(&str) -> usize {
let mut cache = HashMap::new();
let mut call_count = 0;
move |input: &str| {
call_count += 1;
if let Some(&cached_result) = cache.get(input) {
println!("Cache hit for '{}' (call #{})", input, call_count);
cached_result
} else {
let result = input.len(); // Calcul "coûteux"
cache.insert(input.to_string(), result);
println!("Computed '{}' = {} (call #{})", input, result, call_count);
result
}
}
}
let mut processor = create_cached_processor();
// Test du cache
println!("Result: {}", processor("hello")); // Compute
println!("Result: {}", processor("world")); // Compute
println!("Result: {}", processor("hello")); // Cache hit
println!("Result: {}", processor("rust")); // Compute
println!("Result: {}", processor("world")); // Cache hit
}
Patterns Avancés
1. Builder Pattern avec Closures Stateful
struct StatefulProcessor<F> {
processor: F,
}
impl<F> StatefulProcessor<F>
where
F: FnMut(i32) -> i32,
{
fn new(processor: F) -> Self {
Self { processor }
}
fn process_batch(&mut self, values: &[i32]) -> Vec<i32> {
values.iter().map(|&x| (self.processor)(x)).collect()
}
fn process_single(&mut self, value: i32) -> i32 {
(self.processor)(value)
}
}
fn builder_pattern_example() {
let mut multiplier = 2;
let mut processor = StatefulProcessor::new(|x| {
let result = x * multiplier;
multiplier += 1; // L'état change à chaque appel
result
});
println!("Single: {}", processor.process_single(5)); // 5 * 2 = 10
println!("Single: {}", processor.process_single(5)); // 5 * 3 = 15
let batch = vec![1, 2, 3];
let results = processor.process_batch(&batch);
println!("Batch: {:?}", results); // [4, 10, 18] (multipliers: 4, 5, 6)
}
2. State Machine avec Closures
#[derive(Debug, Clone, Copy)]
enum State {
Idle,
Processing,
Complete,
Error,
}
fn state_machine_example() {
let mut current_state = State::Idle;
let mut processed_count = 0;
let mut state_machine = |input: &str| -> (State, String) {
let previous_state = current_state;
match (current_state, input) {
(State::Idle, "start") => {
current_state = State::Processing;
("Started processing".to_string())
}
(State::Processing, "process") => {
processed_count += 1;
if processed_count >= 3 {
current_state = State::Complete;
("Processing complete".to_string())
} else {
(format!("Processed item {} of 3", processed_count))
}
}
(State::Complete, "reset") => {
current_state = State::Idle;
processed_count = 0;
("Reset to idle".to_string())
}
(_, "error") => {
current_state = State::Error;
("Error occurred".to_string())
}
_ => {
("Invalid transition".to_string())
}
}
.map(|msg| (current_state, msg))
.unwrap_or_else(|| (current_state, format!("Invalid input '{}' in state {:?}", input, previous_state)))
};
// Test du state machine
let inputs = vec!["start", "process", "process", "process", "reset", "start", "error"];
for input in inputs {
let (new_state, message) = state_machine(input);
println!("Input: '{}' -> State: {:?}, Message: {}", input, new_state, message);
}
}
Pourquoi Pas FnOnce ou Fn ?
FnOnce: Ne peut être appelée qu'une fois, consommant la closure. Inapproprié pour plusieurs appels.Fn: Utilise des emprunts immutables, empêchant la mutation d'état, donc ne peut pas modifier les variables capturées.
fn demonstrate_trait_differences() {
let x = 5;
let mut counter = 0;
// Fn - read-only, peut être appelée plusieurs fois
let fn_closure = || {
println!("Reading x: {}", x); // Pas de mutation
x * 2
};
// FnMut - peut muter, peut être appelée plusieurs fois
let mut fn_mut_closure = || {
counter += 1; // Mutation
counter
};
// FnOnce - peut muter et consommer, une seule fois
let data = String::from("hello");
let fn_once_closure = || {
println!("Consuming: {}", data);
data // Move data out
};
// Tests
println!("Fn: {} {}", fn_closure(), fn_closure()); // OK multiple fois
println!("FnMut: {} {}", fn_mut_closure(), fn_mut_closure()); // OK multiple fois
println!("FnOnce: {}", fn_once_closure()); // OK une fois
// println!("FnOnce again: {}", fn_once_closure()); // ERREUR: déjà consumed
}
Pièges Courants
1. Oublier mut
fn common_mistake_1() {
let mut counter = 0;
// ❌ Erreur: closure pas déclarée mut
let closure = || {
counter += 1; // Essaie de muter
counter
};
// call_repeatedly(&mut closure); // ERREUR: cannot borrow as mutable
// ✅ Solution: déclarer closure comme mut
let mut closure = || {
counter += 1;
counter
};
call_repeatedly(&mut closure); // OK
}
2. Dangling References
fn common_mistake_2() {
// ❌ Erreur: lifetime problem
fn bad_factory() -> impl FnMut() -> i32 {
let counter = 0; // Locale à cette fonction
|| {
// counter += 1; // ERREUR: counter doesn't live long enough
// counter
42 // Contournement pour l'exemple
}
}
// ✅ Solution: move ou ownership approprié
fn good_factory() -> impl FnMut() -> i32 {
let mut counter = 0;
move || { // Move counter dans la closure
counter += 1;
counter
}
}
let mut closure = good_factory();
println!("Result: {}", closure()); // 1
println!("Result: {}", closure()); // 2
}
3. Confusion entre &mut et move
fn borrow_vs_move_confusion() {
let mut data = vec![1, 2, 3];
// Avec &mut - data reste accessible
{
let mut closure = || {
data.push(data.len() + 1); // Mute via &mut
data.len()
};
println!("Length: {}", closure()); // 4
println!("Length: {}", closure()); // 5
} // closure dropped ici
println!("Original data: {:?}", data); // OK: [1, 2, 3, 4, 5]
// Avec move - data n'est plus accessible
let mut data2 = vec![10, 20, 30];
let mut closure2 = move || {
data2.push(data2.len() + 10); // Move data2 dans closure
data2.len()
};
println!("Length: {}", closure2()); // 4
// println!("Data2: {:?}", data2); // ERREUR: data2 was moved
}
Testing et Debugging
1. Test des Closures Stateful
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stateful_closure() {
let mut sum = 0;
let mut adder = |x: i32| {
sum += x;
sum
};
assert_eq!(adder(5), 5);
assert_eq!(adder(3), 8);
assert_eq!(adder(2), 10);
}
#[test]
fn test_closure_with_function() {
fn apply_three_times<F: FnMut() -> i32>(f: &mut F) -> Vec<i32> {
vec![f(), f(), f()]
}
let mut counter = 0;
let mut increment = || {
counter += 1;
counter
};
let results = apply_three_times(&mut increment);
assert_eq!(results, vec![1, 2, 3]);
}
}
2. Debugging State Changes
fn debug_state_changes() {
let mut state = 0;
let mut debug_closure = |action: &str| {
println!("Before {}: state = {}", action, state);
match action {
"increment" => state += 1,
"double" => state *= 2,
"reset" => state = 0,
_ => println!("Unknown action: {}", action),
}
println!("After {}: state = {}", action, state);
state
};
let actions = vec!["increment", "increment", "double", "increment", "reset"];
for action in actions {
debug_closure(action);
println!("---");
}
}
Performance et Optimisations
1. Éviter les Allocations Inutiles
fn performance_considerations() {
// ❌ Mauvais - allocation à chaque appel
let mut bad_closure = || {
let mut vec = Vec::new(); // Nouvelle allocation
vec.push(1);
vec.push(2);
vec.len()
};
// ✅ Bon - réutilisation du buffer
let mut buffer = Vec::new();
let mut good_closure = || {
buffer.clear(); // Réutilise l'allocation existante
buffer.push(1);
buffer.push(2);
buffer.len()
};
// Test performance (simplifié)
use std::time::Instant;
let start = Instant::now();
for _ in 0..10000 {
bad_closure();
}
println!("Bad closure time: {:?}", start.elapsed());
let start = Instant::now();
for _ in 0..10000 {
good_closure();
}
println!("Good closure time: {:?}", start.elapsed());
}
Points Clés
✅ Utilise FnMut pour les closures qui mutent l'état à travers plusieurs appels.
✅ Marque les closures et paramètres comme mut pour permettre la mutation.
✅ Préfére les closures simples pour l'état basique ; utilise les structs pour la gestion d'état complexe.
Règles de Décision
- État simple (1-2 variables) → Closure avec captures mutables
- État complexe → Struct avec méthodes
- État partagé → Arc<Mutex
> ou RefCell - Performance critique → Éviter allocations dans la closure
- Testing important → Struct pour meilleure testabilité
Exemple Réel : Les closures stateful sont communes dans les event loops ou tâches async (ex : tokio) où une closure maintient des compteurs ou buffers à travers les itérations.
Expérimente : Essaie de passer une closure non-mut à call_repeatedly.
Réponse : Erreur de compilation ! La closure doit être mutable pour implémenter FnMut.
Exemple Pratique Complet
use std::collections::VecDeque;
// Simulateur de fenêtre glissante avec closure stateful
fn sliding_window_example() {
let window_size = 3;
let mut window = VecDeque::new();
let mut sum = 0.0;
let mut sliding_average = |value: f64| -> f64 {
// Ajouter nouvelle valeur
window.push_back(value);
sum += value;
// Retirer valeur si fenêtre trop grande
if window.len() > window_size {
if let Some(old_value) = window.pop_front() {
sum -= old_value;
}
}
// Calculer moyenne
sum / window.len() as f64
};
// Function qui utilise notre closure
fn process_stream<F>(mut processor: F, values: &[f64])
where
F: FnMut(f64) -> f64,
{
for &value in values {
let avg = processor(value);
println!("Value: {:.1}, Moving Average: {:.2}", value, avg);
}
}
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 2.0, 1.0];
process_stream(&mut sliding_average, &values);
}
fn main() {
sliding_window_example();
}
Conclusion : Les closures stateful avec FnMut offrent un moyen puissant et ergonomique de maintenir l'état à travers plusieurs appels de fonction. Maîtrise ces patterns pour écrire du code Rust expressif et performant !