Invalid Date5 min

Comprendre les fermetures move en Rust : Guide pour développeurs JavaScript

m
mayo

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

La base JavaScript

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

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 fermeture partage la même variable count. Pas de copie, pas de déplacement—juste une référence qui vit aussi longtemps que la fermeture.

Le choix explicite de Rust

Rust vous oblige à choisir : emprunter ou posséder. Les fermetures normales empruntent :

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

Les fermetures move prennent possession :

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

Mécanismes de transfert de propriété

Pour les types non-Copy comme String ou Vec, la fermeture prend possession :

let s = String::from("hello");
let closure = move || println!("{}", s); // `s` déplacé dans la fermeture
// 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 : `x` original toujours valide

Quand vous avez besoin de move

Threading

En JavaScript, vous partageriez l'état entre opérations asynchrones 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 fermetures

Les fabriques 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 fermetures 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 fermeture survit à la portée de la fonction, donc elle a besoin de la propriété de x.

Blocs async

Comme pour les 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 nécessite une durée de vie 'static
tokio::spawn(future);

Emprunt vs Possession : La différence fondamentale

Les fermetures JavaScript partagent toujours :

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

Les fermetures Rust normales empruntent :

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

Les fermetures Rust move 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 modifications internes n'affectent pas l'original.

Le changement de paradigme depuis JavaScript

JavaScript : les fermetures capturent par référence implicitement. Le GC gère la durée de vie. Vous ne pensez jamais à la propriété :

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 de propriété :

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

Cela empêche les courses aux données et les use-after-free à 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 fermetures Oui La fermeture 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 fermeture survit à son environnement ou doit être Send, utilisez move. Sinon, laissez le vérificateur d'emprunt choisir le mode de capture minimal.

Le mot-clé move est la façon de Rust de dire : "Cette fermeture possède maintenant ces variables." Ce n'est pas juste de la syntaxe—c'est un contrat appliqué à la compilation, éliminant des classes entières d'erreurs d'exécution qui affligent les langages avec ramasse-miettes.

Retour au blog
Partager ::