November 12, 20255 min

Comprendre les Move Closures en Rust : Guide pour les Développeurs JavaScript

m
mayo

Vous venez de JavaScript ? Les closures fonctionnent différemment en Rust. Une closure move force le transfert d'ownership des variables capturées—pas de références partagées comme en JS. C'est le pont entre les closures automatiques de JavaScript et le modèle d'ownership de Rust.

La Base JavaScript

En JavaScript, les closures capturent les variables par référence automatiquement :

const makeCounter = () => {
  let count = 0;
  return () => count++; // capture `count` par référence
};

const counter = makeCounter();
console.log(counter()); // 0
console.log(counter()); // 1

La closure partage la même variable count. Pas de copie, pas de déplacement—juste une référence qui vit aussi longtemps que la closure.

Le Choix Explicite de Rust

Rust vous oblige à choisir : emprunter ou posséder. Les closures régulières empruntent :

let mut count = 0;
let increment = || count += 1; // emprunte `count` mutablement

Les closures move prennent l'ownership :

let count = 0;
let increment = move || count + 1; // `count` déplacé/copié dans la closure

Mécanismes de Transfert d'Ownership

Pour les types non-Copy comme String ou Vec, la closure prend l'ownership :

let s = String::from("hello");
let closure = move || println!("{}", s); // `s` déplacé dans la closure
// println!("{}", s); // ERREUR : `s` n'est plus valide

Pour les types Copy comme i32 ou bool, la valeur est copiée :

let x = 42;
let closure = move || println!("{}", x); // `x` copié
println!("{}", x); // OK : le `x` original est toujours valide

Quand Vous Avez Besoin de move

Threading

En JavaScript, vous partageriez l'état entre opérations async sans y penser :

const data = [1, 2, 3];
setTimeout(() => {
  console.log(data); // fonctionne simplement
}, 100);

Les threads Rust doivent posséder leurs données :

use std::thread;

let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
    println!("{:?}", data); // `data` possédé par le thread
});
// println!("{:?}", data); // ERREUR : déplacé
handle.join().unwrap();

Sans move, le compilateur rejette ce code—le thread pourrait survivre à data.

Retour de Closures

Les factories JavaScript fonctionnent par référence :

const makeAdder = (x) => (y) => x + y; // `x` capturé par référence

const addFive = makeAdder(5);
console.log(addFive(3)); // 8

Les closures Rust doivent posséder ce qu'elles survivent :

fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y // `x` doit être déplacé
}

let add_five = make_adder(5);
println!("{}", add_five(3)); // 8

La closure survit à la portée de la fonction, donc elle a besoin de l'ownership de x.

Blocs Async

Similairement aux threads, les blocs async ont souvent besoin de move quand ils sont envoyés entre tâches :

let value = String::from("async");
let future = async move {
    println!("{}", value);
};
// tokio::spawn requiert une durée de vie 'static
tokio::spawn(future);

Emprunt vs Possession : La Différence Fondamentale

Les closures JavaScript partagent toujours :

let count = 0;
const increment = () => count += 1;
increment();
console.log(count); // 1 - même `count`

Les closures régulières Rust empruntent :

let mut count = 0;
let mut increment = || count += 1; // emprunt mutable
increment();
println!("{}", count); // 1 - même `count`

Les closures move Rust possèdent :

let mut count = 0;
let mut increment = move || count += 1; // `count` déplacé
increment();
// println!("{}", count); // ERREUR : `count` déplacé

Le count déplacé est indépendant—les changements internes n'affectent pas l'original.

Le Changement de Paradigme depuis JavaScript

JavaScript : les closures capturent par référence implicitement. Le GC gère la durée de vie. Vous ne pensez jamais à l'ownership :

const createHandler = () => {
  const state = { count: 0 };
  return () => state.count++; // la référence vit aussi longtemps que nécessaire
};

Rust : vous choisissez explicitement. Empruntez pour un usage local. Déplacez pour un transfert d'ownership :

fn create_handler() -> impl FnMut() -> i32 {
    let mut state = 0;
    move || {
        state += 1;
        state
    } // `state` possédé par la closure
}

Cela empêche les data races et les use-after-free au moment de la compilation—des garanties que JavaScript ne peut pas offrir.

Résumé

Scénario Utiliser move Raison
Threading Oui Le thread peut survivre à la portée
Retour de closures Oui La closure survit à la fonction
Tâches async Souvent La tâche a besoin d'une durée de vie 'static
Usage local Non L'emprunt est suffisant

Principe fondamental : Si une closure survit à son environnement ou doit être Send, utilisez move. Sinon, laissez le borrow checker choisir le mode de capture minimal.

Le mot-clé move est la façon de Rust de dire : "Cette closure possède maintenant ces variables." Ce n'est pas juste de la syntaxe—c'est un contrat appliqué au moment de la compilation, éliminant des classes entières d'erreurs runtime qui affectent les langages avec garbage collection.

Retour au blog
Partager ::