Historique : Lorsque la bibliothèque standard de Rust a introduit Cow (Clone-on-Write), l'objectif était d'abstraire les données pouvant être empruntées ou possédées sans imposer d'allocation immédiate. Le trait Clone avait été initialement envisagé, mais il ne permet que de produire une copie identique du même type. Pour les données empruntées telles que &str, le clonage produit une autre référence plutôt que le String possédant nécessaire à la mutation. Le trait ToOwned a été spécifiquement conçu pour exprimer la relation entre les formes empruntées et possédées à travers son type associé Owned.
Problème : Si Cow s'appuyait sur Clone, la conversion d'une Cow::Borrowed(&str) en une représentation possédée pour modification nécessiterait une logique de conversion externe. Clone manque du mécanisme de niveau type pour transformer &str en String, forçant soit une allocation prématurée au moment de la construction, soit une gestion d'état manuelle complexe. Cela violerait le principe d'abstraction à coût zéro de Cow en rendant impossible le report de l'allocation sur le tas jusqu'à ce que la mutation soit réellement nécessaire.
Solution : ToOwned définit type Owned et fn to_owned(&self) -> Self::Owned, permettant à &str de spécifier Owned = String. Cela permet à Cow::to_mut() d'allouer paresseusement uniquement lorsque la mutation est demandée. Si la Cow est déjà Owned, elle retourne une référence mutable aux données existantes sans allocation. L'exemple suivant démontre cette efficacité :
use std::borrow::Cow; fn normalize_whitespace(input: &str) -> Cow<'_, str> { if input.contains(" ") { let cleaned = input.replace(" ", " "); Cow::Owned(cleaned) // Alloue seulement ici } else { Cow::Borrowed(input) // Emprunt à coût zéro } }
Un service de traitement de logs à fort débit devait normaliser les horodatages dans les entrées provenant de fichiers mappés en mémoire. L'entrée arrivait sous forme de tranches &str pointant dans la carte, mais environ 10 % des entrées nécessitaient des ajustements de fuseau horaire nécessitant une allocation de String. L'implémentation initiale utilisait un enum personnalisé avec des variantes String et &str, nécessitant une correspondance de motifs exhaustive à chaque point d'accès et une logique de clonage manuelle qui était sujette aux erreurs et verbeuse.
Alternative 1 : Conversion enthousiaste en String. L'équipe a envisagé de convertir toutes les entrées en String immédiatement après ingestion. Cette approche simplifiait le modèle de données et éliminait les préoccupations liées à la durée de vie, mais elle imposait une surcharge mémoire sévère. Pendant les pics de charge, cela doublait l'utilisation de la mémoire pour les 90 % des logs qui n'ont jamais nécessité de modification, ce qui entraînait des erreurs OOM lors du traitement de fichiers de 10 Go.
Alternative 2 : Utilisation de Arc<str> avec copy-on-write. Une autre option a consisté à utiliser Arc<str> pour le partage immuable combiné avec Arc::make_mut pour les modifications. Bien que cela ait fourni des sémantiques de propriété partagées, cela a introduit une surcharge de comptage de référence atomique pour chaque accès. De plus, cela nécessitait toujours une logique explicite pour gérer la transition de partagé à mutable, compliquant le modèle d'emprunt sans fournir l'ergonomie souhaitée.
Alternative 3 : Adoption de Cow<'_, str>. L'équipe a choisi Cow pour abstraire les deux états. Les variantes Borrowed pointaient directement dans la carte mémoire sans allocation, tandis que les variantes Owned contenaient des chaînes modifiées. Cette solution a été choisie car to_mut() reportait l'allocation jusqu'à la première mutation, conservant un coût zéro pour les chemins en lecture seule tout en offrant une API unifiée.
Résultat : Le parseur maintenait un haut débit, gérant des fichiers de log de 10 Go avec seulement 200 Mo d'allocations réelles sur le tas. En tirant parti de Cow, le système a éliminé le suivi manuel de l'état, maintenu les propriétés Send et Sync pour le traitement parallèle et réduit la complexité du code de 60 % par rapport à l'approche d'énumération personnalisée.
into_owned retourne ToOwned::Owned par valeur, ce qui nécessite une taille connue à la compilation pour allouer de l'espace sur la pile. Bien que Cow puisse envelopper des types non dimensionnés comme str via Cow<'_, str>, le type Owned (String) est dimensionné. Les candidats confondent souvent Cow<'_, T> avec Cow<'_, &T>, essayant d'implémenter des traits pour la référence plutôt que le type emprunté. Sans la contrainte Sized sur ToOwned::Owned, le compilateur ne pourrait pas construire la valeur de retour pour into_owned, car il tenterait de retourner un str non dimensionné directement plutôt que le conteneur String dimensionné.
Cow implémente Borrow<Borrowed> où Borrowed: ToOwned, permettant à Cow<String> d'être recherché avec &str. Cependant, Borrow impose un contrat strict : si deux valeurs sont égales via Eq, elles doivent produire des valeurs de hachage identiques. Les candidats implémentent souvent PartialEq personnalisé pour Cow (par exemple, comparaison insensible à la casse) tout en conservant l'implémentation standard de Hash. Cela viole le contrat car deux valeurs de Cow pourraient se comparer comme égales selon une logique personnalisée mais hacher différemment si l'implémentation de Hash considère les octets originaux. Cela entraîne des échecs de recherche dans HashMap où une clé semble exister mais ne peut pas être trouvée.
Pour construire une variante Borrowed, Cow nécessite une référence &'a B avec une durée de vie 'a. Une implémentation par défaut standard devrait produire une référence valide pour 'static (par exemple, &'static str pour ""), mais &str lui-même n'implémente pas Default car il n'y a pas de valeur de référence universelle à retourner. Les candidats suggèrent souvent de par défaut à Cow::Borrowed(""), mais cela nécessite soit une contrainte de durée de vie 'static sur B, soit une spécialisation non disponible dans le Rust stable. En conséquence, la bibliothèque standard exige ToOwned::Owned: Default, forçant Cow::Owned(String::new()) (une allocation) même pour des valeurs par défaut vides. Les candidats manquent cette distinction car ils confondent la disponibilité des littéraux de chaîne dans des portées spécifiques avec une implémentation générale de Default pour les références.