Ownership et emprunt
Le sujet central de Rust. L’objectif ici est de te donner un modèle mental clair plutôt qu’une liste de règles.
La règle fondamentale : un seul propriétaire
En Rust, chaque valeur a exactement un propriétaire à la fois. Quand le propriétaire sort de portée, la valeur est libérée (mémoire désallouée). C’est ça qui remplace le garbage collector.
Tout le système de Rust découle de cette règle. Les références, les moves, Clone, c’est juste trois façons différentes de partager ou transférer cette propriété.
Les 3 mécaniques, dans l’ordre de “coût”
1. Référence (&T ou &mut T) — gratuite, à privilégier
Une référence dit : “je veux regarder la valeur sans la posséder”. Le propriétaire reste le même. C’est l’équivalent d’un pointeur en C, mais avec des garanties à la compilation.
Deux variantes :
&T— référence partagée, lecture seule. Tu peux en avoir autant que tu veux simultanément&mut T— référence mutable, lecture/écriture. Tu peux en avoir une seule à la fois, et aucune&Ten parallèle
Cette règle (XOR entre lecture multiple et écriture unique) est ce qui élimine les data races à la compilation.
Dans ton code : find_all(db: &DatabaseConnection) — le repository emprunte la connexion, il ne la possède pas. Plusieurs fonctions peuvent partager &db en même temps.
2. Move (transfert de propriété) — gratuit, mais consomme
Quand tu passes une valeur sans &, tu transfères la propriété. L’ancien nom n’est plus utilisable.
C’est le comportement par défaut en Rust. Pas de copie cachée, pas de pointeur partagé : la propriété passe d’une variable à l’autre.
Dans ton code :
Pas de & devant user::Model → la fonction consomme le model. Dans users.into_iter().map(UserDto::from), chaque model est consommé pour produire un dto. Plus de doubles allocations.
3. Clone — explicite, coûteux
.clone() te donne une copie indépendante de la valeur. Nouvelle allocation, deux propriétaires distincts.
Rust te force à écrire .clone() explicitement, justement pour que tu voies où il y a un coût. C’est pas un anti-pattern, mais ça doit être un choix conscient.
Dans ton code : #[derive(Clone)] pub struct AppState { ... } — on l’a marqué Clone parce qu’Axum clone l’état à chaque requête pour le distribuer aux handlers. C’est OK ici parce que DatabaseConnection clone juste un pointeur en interne (Arc).
Cas spécial : les types Copy
Les types simples (i32, bool, f64, les références &T, les tuples de Copy…) sont marqués Copy. Pour eux, l’assignation est une copie automatique, pas un move.
C’est pour ça que dans ton DTO, id: model.id ne consomme pas model.id — c’est un i32, donc Copy.
Règle simple : une valeur est Copy si elle tient entièrement sur la pile et ne possède aucune ressource (pas de tas, pas de fichier, pas de socket). String, Vec, Box ne sont pas Copy.
Le décideur : quand utiliser quoi ?
Voici un arbre de décision quand tu écris une fonction :
Heuristique simple : commence toujours par &T. Si le compilateur râle parce que tu dois consommer la valeur, passe à T. Recoure à .clone() seulement quand tu n’as pas le choix (et envisage Arc<T> si la même chose doit être partagée par beaucoup de monde).
Les pièges classiques (et comment les lire)
“value borrowed here after move”
→ Tu utilises s après l’avoir donné. Solutions : ne pas le donner (&s), ou cloner (s.clone() au lieu de s).
“cannot borrow as mutable because it is also borrowed as immutable”
→ Tu as un & actif et tu essaies un &mut. Solution : raccourcir la durée du & (le sortir de portée avant le &mut).
“borrowed value does not live long enough”
→ Tu retournes une référence vers une variable locale qui va être détruite. Solution : retourner String (la valeur), pas &String.
Le formatage emprunte toujours
Une source de confusion : pourquoi peut-on logger ou afficher une valeur sans la consommer, alors que toute autre opération sans & est un move ?
Le trait Display
Display est le trait standard qui dit “voici comment me formater pour un humain”. C’est ce qui se passe quand tu écris println!("{x}"), format!("{x}"), ou tracing::info!("{x}").
Sa signature :
Toutes les traits de formatage (Display, Debug pour {:?}, LowerHex pour {:x}, etc.) prennent &self. C’est cohérent : afficher quelque chose ne devrait pas le détruire.
{x} se désucre en &x
Quand tu écris :
Le compilateur transforme ça (en très simplifié) en :
Les formatages {}, {var}, {var:?}, etc. prennent toujours leur argument par référence. Tu n’as pas besoin d’écrire &err toi-même — la macro le fait pour toi. C’est du sucre syntaxique.
Le mental model à retenir
En Rust, tout est emprunt par défaut quand tu lis (formatage, comparaison, hash…) et tout est move par défaut quand tu donnes (passage à une fonction qui prend
T, retour, assignation).
Les macros println!, format!, tracing::*! tombent dans la première catégorie : elles lisent. Donc elles empruntent. Tu ne consommes jamais une valeur en l’affichant.