Vous connaissez cette sensation quand vous commencez un nouveau langage et que vous vous attendez à sprinter, mais que vous trébuchez dès la première marche ? C'était mon cas avec Rust. J'avais un utilitaire JavaScript tout à fait fonctionnel — quelque chose qui analysait et modifiait quelques tableaux — et je me suis dit que je le réécrirais pendant une pause café. J'ai ouvert mon éditeur, j'ai tapé ce qui me semblait être du code naturel, et j'ai lancé le compilateur. Il a explosé. Pas avec des erreurs de syntaxe. Pas avec des points-virgules manquants. Il m'a dit que j'avais essayé d'utiliser une valeur qui avait déjà été déplacée (moved). J'ai fixé l'écran. Déplacée ? Je n'ai rien déplacé. Je l'ai juste assignée à une autre variable.

La zone de confort de JavaScript

JavaScript vous entraîne à traiter les données comme un tableau blanc communautaire. Vous déclarez un objet ou un tableau, vous le passez à une fonction, vous laissez cette fonction ajouter une propriété, puis vous le passez ailleurs. Le ramasse-miettes (garbage collector) se tient en arrière-plan comme un concierge invisible, attendant de balayer tout ce que vous abandonnez. Vous ne vous demandez jamais : « À qui appartient cette chaîne ? ». Vous demandez : « Puis-je la lire ? ». Si la réponse est oui, vous la manipulez. Si vous avez besoin d'une autre référence, vous dites simplement let b = a et vous continuez. Les deux variables pointent vers la même zone de mémoire, et si a la modifie, b voit le changement immédiatement. C'est pratique. C'est aussi chaotique, mais ce chaos vous mord rarement car l'environnement d'exécution (runtime) gère le nettoyage.

Le premier choc du compilateur

Rust ne vous fait pas confiance. Cela semble dur, mais c'est la première chose que l'on apprend. Le langage est construit autour d'un système de propriété (ownership) avec trois règles rigides. Premièrement, chaque valeur a exactement un propriétaire. Deuxièmement, il n'y a qu'un seul propriétaire à la fois. Troisièmement, lorsque ce propriétaire sort du champ d'application (scope), Rust libère la valeur automatiquement. Pas de ramasse-miettes. Pas de comptage de références en arrière-plan, à moins que vous n'en fassiez l'option explicite. Juste ces trois règles, appliquées par le compilateur avant même que votre code ne s'exécute.

Lorsque j'ai écrit ma version Rust de cet utilitaire JavaScript, j'ai fait ce qui me semblait naturel. J'ai créé un vecteur, qui est la réponse de Rust aux tableaux, et je l'ai assigné à une seconde variable. Ensuite, j'ai essayé d'utiliser la première. Le compilateur a refusé. En JavaScript, let b = a copie la référence. En Rust, cela déplace la propriété (moves ownership). La variable d'origine devient invalide. Le compilateur fait cela pour garantir que vous n'ayez jamais deux chemins accédant accidentellement à la même zone mémoire, ce qui est la cause des data races et des bugs de type use-after-free dans d'autres systèmes.

Pourquoi Rust vous retire vos jouets

Regardez ce modèle en JavaScript :

let a = [1, 2, 3];
let b = a;
a.push(4);
// Both a and b see 4.

C'est une seconde nature. Vous n'y réfléchiriez pas à deux fois. En Rust, cette même intuition vous fera rejeter par le compilateur avant même d'avoir fini de taper. Une fois que b possède le vecteur, a n'est plus qu'un nom vide. Vous ne pouvez pas y ajouter d'éléments (push). Vous ne pouvez même pas le regarder. La mémoire appartient désormais à b.

Cela ressemble à une punition jusqu'à ce que vous réalisiez ce que cela empêche. Si deux variables pouvaient toutes deux modifier les mêmes données sur le tas (heap) sans coordination, vous risqueriez une corruption de la mémoire. Rust élimine toute cette catégorie de bugs en rendant le transfert explicite. Le compilateur n'est pas pédant. Il joue le rôle de gardien. Il vous oblige à décider, à chaque étape, quelle partie de votre code est responsable de quelles données.

L'emprunt (Borrowing) : la solution de contournement qui est en fait une fonctionnalité

Bien sûr, si chaque assignation transférait la propriété pour toujours, écrire des programmes serait épuisant. Vous devriez tout cloner, gaspillant ainsi mémoire et vitesse. Rust résout ce problème grâce à l'emprunt (borrowing).

L'emprunt vous permet d'utiliser une valeur sans la prendre. Il en existe deux types, et la distinction est importante.

  • Emprunts immuables, écrits &T : Ils vous permettent de lire les données. Vous pouvez en avoir autant que vous le souhaitez en même