Storia: Quando la libreria standard di Rust ha introdotto Cow (Clone-on-Write), l'obiettivo era astrarre su dati che potrebbero essere presi in prestito o posseduti senza forzare un'allocazione immediata. Il tratto Clone era inizialmente considerato, ma consente solo di produrre una copia identica dello stesso tipo. Per i dati presi in prestito come &str, clonare produce un'altra referenza piuttosto che la String posseduta necessaria per la modifica. Il tratto ToOwned è stato progettato specificamente per esprimere la relazione tra forme prese in prestito e possedute attraverso il suo tipo associato Owned.
Problema: Se Cow si basasse su Clone, convertire un Cow::Borrowed(&str) in una rappresentazione posseduta per la modifica richiederebbe logica di conversione esterna. Clone manca del meccanismo a livello di tipo per trasformare &str in String, costringendo a un'allocazione prematura al momento della costruzione o a una complessa gestione manuale dello stato. Questo violerebbe il principio di astrazione a costo zero di Cow, rendendo impossibile posticipare l'allocazione della memoria fino a quando la mutazione è effettivamente necessaria.
Soluzione: ToOwned definisce type Owned e fn to_owned(&self) -> Self::Owned, consentendo a &str di specificare Owned = String. Questo consente a Cow::to_mut() di allocare pigramente solo quando viene richiesta una mutazione. Se il Cow è già Owned, restituisce un riferimento mutabile ai dati esistenti senza allocazione. Il seguente esempio dimostra questa efficienza:
use std::borrow::Cow; fn normalize_whitespace(input: &str) -> Cow<'_, str> { if input.contains(" ") { let cleaned = input.replace(" ", " "); Cow::Owned(cleaned) // Alloca solo qui } else { Cow::Borrowed(input) // Prestito a costo zero } }
Un servizio di elaborazione di log ad alta capacità ha bisogno di normalizzare i timestamp nelle voci provenienti da file mappati in memoria. L'input arrivava come fette &str che puntavano nella mappa, ma circa il 10% delle voci richiedeva aggiustamenti del fuso orario, necessitando di un'allocazione di String. L'implementazione iniziale utilizzava un enum personalizzato con varianti String e &str, richiedendo un esauriente pattern matching in ogni punto di accesso e una logica di clone manuale che era soggetta a errori e verbosa.
Alternativa 1: Conversione anticipata in String. Il team ha considerato di convertire tutti gli input in String immediatamente al momento dell'ingestione. Questo approccio ha semplificato il modello dati ed eliminato le preoccupazioni relative al tempo di vita, ma ha imposto un grave sovraccarico di memoria. Durante i carichi di picco, ciò ha raddoppiato l'uso della memoria per il 90% dei log che non richiedevano mai modifiche, causando errori OOM durante l'elaborazione di file da 10GB.
Alternativa 2: Utilizzo di Arc<str> con copy-on-write. Un'altra opzione prevedeva Arc<str> per la condivisione immutabile insieme a Arc::make_mut per le modifiche. Mentre questo forniva semantiche di proprietà condivisa, introduceva un sovraccarico di conteggio di riferimenti atomici per ogni accesso. Inoltre, richiedeva ancora una logica esplicita per gestire la transizione da condiviso a mutabile, complicando il modello di prestito senza fornire l'ergonomia desiderata.
Alternativa 3: Adottare Cow<'_, str>. Il team ha scelto Cow per astrarre sui due stati. Le varianti Borrowed punterebbero direttamente nella mappa di memoria senza allocazione, mentre le varianti Owned contenevano stringhe modificate. Questa soluzione è stata selezionata perché to_mut() ha posticipato l'allocazione fino alla prima mutazione, preservando il costo zero per i percorsi di sola lettura mentre offriva un'API unificata.
Risultato: Il parser ha mantenuto un'elevata capacità di elaborazione, gestendo file di log da 10GB con soli 200MB di allocazioni in heap effettive. Sfruttando Cow, il sistema ha eliminato la tracciatura manuale dello stato, mantenuto le proprietà Send e Sync per l'elaborazione parallela e ridotto la complessità del codice del 60% rispetto all'approccio dell'enum personalizzato.
into_owned restituisce ToOwned::Owned per valore, il che richiede una dimensione nota a tempo di compilazione per allocare spazio nello stack. Sebbene Cow possa avvolgere tipi non dimensionati come str tramite Cow<'_, str>, il tipo Owned (String) è dimensionato. I candidati spesso confondono Cow<'_, T> con Cow<'_, &T>, tentando di implementare i tratti per il riferimento piuttosto che per il tipo preso in prestito. Senza il vincolo Sized su ToOwned::Owned, il compilatore non potrebbe costruire il valore restituito per into_owned, poiché tenterebbe di restituire un str non dimensionato direttamente piuttosto che il contenitore dimensionato String.
Cow implementa Borrow<Borrowed> dove Borrowed: ToOwned, consentendo a Cow<String> di essere cercato con &str. Tuttavia, Borrow impone un contratto rigoroso: se due valori sono uguali tramite Eq, devono produrre valori hash identici. I candidati spesso implementano PartialEq personalizzato per Cow (ad esempio, confronti senza distinzione tra maiuscole e minuscole) mantenendo l'implementazione standard di Hash. Questo viola il contratto perché due valori Cow potrebbero confrontarsi come uguali secondo una logica personalizzata ma avere hash diversi se l'implementazione di Hash vede i byte originali. Ciò porta a fallimenti di ricerca in HashMap dove una chiave sembra esistere ma non può essere trovata.
Per costruire una variante Borrowed, Cow richiede un riferimento &'a B con durata 'a. Un'implementazione Default generale dovrebbe produrre un riferimento valido per 'static (ad esempio, &'static str per ""), ma &str stesso non implementa Default perché non esiste un valore di riferimento universale da restituire. I candidati spesso suggeriscono di impostare come predefinito Cow::Borrowed(""), ma ciò richiede o un vincolo di durata 'static su B o specializzazione non disponibile in Rust stabile. Di conseguenza, la libreria standard richiede ToOwned::Owned: Default, costringendo Cow::Owned(String::new()) (un'allocazione) anche per valori predefiniti vuoti. I candidati mancano questa distinzione perché confondono la disponibilità delle stringhe letterali in determinati ambiti con un'implementazione generale di Default per i riferimenti.