|
Abbiamo introdotto il concetto di barrowing nel paragrafo precedente. In questo capitolo introduciano la nozione di ownership altro pilastro di questo linguaggio. Ne parliamo grazie alle stringhe o meglio le stringhe dinamiche, differenti da quelle immutabili incontrate nello scorso paragrafo.. La keyword che le identifica è String e rappresenta qualcosa di un po' più complesso rispetto alle string slice viste in precedenza. Dal punto di vista della "velocità" esse sono forse un po' più lente nell'uso rispetto a &str ma sono anche modificabili "in place" ovvero senza necessità che in caso di modifiche se ne debba creare una nuova, possono quindi crescere di dimensione, diminuire ecc.. il che le rende strumenti estremamente duttili. D'altro canto, è anche possibile che modifiche apportate potrebbero obbligare ad una completa riallocazione degli elementi ed ecco un motivo per cui potrebbero essere più pesanti dal punto di vista prestazionale. Una caratteristica fondamentale delle stringhe è che i suoi elementi costitutivi sono allocati nello heap, cosa che è intuibile considerando la natura dinamica di questa struttura dati ma viene coinvolto anche lo stack per cui, a questo punto, direi che è bene chiarire la stituazione che si presenta nel momento in cui avremo creato la nostra stringa: nello stack abbiamo: un puntatore - diretto all'indirizzo dello heap in cui si trovano gli elementi della stringa, o meglio, diretto al primo elemento della sequenza una dimensione - quella attuale della stringa una capacità - lo spazio allocato per la stringa indipendentemente dalle sue dimensioni attuali. La capacità è importante perchè può limitare la necessità di riallocazioni che potrebbero influire sulle prestazioni. nello heap abbiamo: gli elementi costitutivi della stringa. In pratica se "ciao" è la nostra String questa è la situazione in memoria: ![]() Vediamo ora come creare una stringa, per uniformità e coerenza le presentiamo tutte come mutabili: 1) attraverso new let mut s1 = String::new() // crea una stringa vuota alla quale si potranno aggiungere altri elementi 2) partendo da una string slice con il metodo to_string oppure to_owned let mut s1 = "ciao".to_string(); let mut s1 = "ciao".to_owned(); questi due metodi sono in pratica equivalenti ma il secondo è operativamente più leggero e in linea di massima quest'ultimo è quello da preferire nell'uso pratico. 3) usando from() let mut s1 = String::from("ciao"); 4) usando into() let mut my_str = "Questo è un test";
let mut s1: String = my_str.into();
5) usando la macro format! let mut s1 = format!("{} {}", "hello", "world"); 6) partendo da un vettore di byte (dobbiamo ancora parlare dei vettori e degli array) tenendo presente che i valori nell'array devono essere UTF-8 validi: let vec = vec![104, 101, 108, 108, 111]; // 'hello' in ASCII let mut s1 = String::from_utf8(vec).unwrap(); 7) partendo da una sequenza di caratteri:
use std::iter::FromIterator;
fn main() {
let chars: Vec<char>= vec!['a', 'b', 'c'];
let mut s1 = String::from_iter(chars);
}
Esistono altre strade, ma per ora queste possono bastare. Non è necessario che tutti questi sistemi siano chiari adesso, potrete tornarci sopra in seguito quando avremo affrontato altri argomenti, abbiamo già accennato ad array e vettori, ci aggiungo anche gli iteratori. In questo paragrafo useremo sostanzialmente le prime tre strade. Già che ci siamo, notiamo che così come è possibile passare da &str a string, attraverso i vari metodi che abbiamo osservato, è possibile anche l'opposto: let s1: String = "abc".to_owned(); let s2: &str = &s1[..]; Quando definiamo una stringa in uno dei modi visti in precedenza, il contenuto è in suo pieno possesso per questo motivo si parla di ownership. Le basi dell'owneship sono tre:
L'ultimo punto in particolare è importante per capire come Rust provveda automaticamente a ripulire in maniera sicura lo heap da elementi non più necessari. La pulizia è sicura sulla base dei primi due punti che garantiscono che non ci sia il rischio di imbattersi nei famosi e temuti dangling pointers. Più avanti nel paragrafo vedremo come lavorare con le stringhe,c'è parecchio da vedere, per ora proviamo a vedere come lavora l'ownership. Il seguente codice, che si basa su una string slice, compila e stampa due volte la stringa "abc" a video: let s1: &str = "abc"; let s2 = s1; println!("{}", s1); println!("{}", s2); questo secondo codice invece non compila: let s1 = String::from("abc"); let s2 = s1; println!("{}", s1); println!("{}", s2); error[E0382]: borrow of moved value: `s1` --> r141.rs:4:20 | 2 | let s1 = String::from("abc"); | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait 3 | let s2 = s1; | -- value moved here 4 | println!("{}", s1); | ^^ value borrowed here after move | = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) help: consider cloning the value if the performance cost is acceptable | 3 | let s2 = s1.clone(); | ++++++++ Error: aborting due to 1 previous error For more information about this error, try `rustc --explain E0382`. Le due righe in grassetto sono quelle a mio avviso più significative. La macro di stampa cerca di prendere in prestito (borrow) la stringa s1 ma il valore di questa è stato mosso, ovvero ha cambiato padrone e noi sappiamo che a seguito della seconda riga che questo valore è stato assegnato alla variabile s2 che ora ne è il proprietario, l'unico proprietario, come da regole che delimitano l'ownership. La seconda riga evidenziata ci spiega che le stringhe non implementano il trait copy quindi l'unico modo per completare l'assegnazione è il passaggio di proprietà verso s2. s1 pertanto è uscita dallo stack (non ci sarà il temuto double free error) ma la stringa nello heap non subirà un dropping perchè s2 punta ancora ad essa. E siccome il compilatore vuole andare fino in fondo e si traveste un po' da insegnante del linguaggio, ci invita ad usare il metodo clone che lavora diversamente da copy come vedremo. Tenete presente fin da subito che clone è piuttosto pesante come operazione. Il primo esempio invece che si basa sulle string slice funziona senza problemi perchè il borrowing permette più puntatori ad una stringa letterale come abbiamo visto nel precedente paragrafo. Avremo modo di vedere il concetto di ownership in azione in vari ambiti, dalla concorrenza a struttura dati interessanti come gli enumerativi o i vettori. E' il momento di presentare come potrete lavorare con le String. Il materiale è tanto ne presento una parte che ritengo utile ed interessante. Da un punto di vista tecnico una stringa è array di u8 (vec<u8> non preoccupatevi per la definizione per adesso) e tutti i suoi elementi sono UTF8 validi. Anche per loro esiste una indicizzazione che inizia da 0 e prosegue progressivamente di una unità per ogni elemento. Gli indici devomo essere di tipo usize come nel caso delle string slice. La lunghezza di una stringa è rilevabile tramite il consueto metodo len() let s1: String = "abc".to_owned(); println!("{}", s1.len()); Se vogliamo aggiungere, o meglio appendere elementi in una stringa abbiamo il metodo push(carattere), vediamo un esempio che parte da una stringa vuota:
che costruisce la stringa "ab2". il metodo clear() cancella tutti gli elementi let mut s1 = "ciao".to_string(); s1.clear(); mentre remove(indice) elimina l'elemento alla posizione indicata dall'indice: let mut s1 = "ciao".to_string(); s1.remove(3); println!("{}", s1); ci propone in output la stringa "cia" privata quindi dell'elemento alla posizione 3, la lettera 'o'. Al contrario insert() inserisce un carattere alla posizione indicata (più avanti vedremo un altro sistema): let mut s1 = "ciao".to_string(); s1.insert(3, 'r'); println!("{}", s1); che ci restituisce la parola "ciaro" avendo inserito la lettera 'r' alla poszione con indice 3. Forse ancora più utile è insert_str(indice, string slice) tramite il quale possiamo inserire una string slice alla posizione che indichiamo con l'indice. let mut s1 = "ciao".to_string(); s1.insert_str(3, "sss"); println!("{}", s1); e il nostro "ciao" diventa uno strambo "ciassso". Possiamo invece sostituire uno o più caratteri nella stringa e un modo semplice per fare questa operazione è ricorrere a replace_range(range, stringa).
con output:
- la riga 5 sostituisce gli elementi con indice 1 e 2 (il range non è inclusivo) con la stringa "www" - la riga 7 sostituisce l'elemento all'indce 0 con la stringa (non carattere) "z" - la riga 9 inserisce la stringa "k" all'indice zero (fa lo stesso lavoro di insert()). Tutte le modifiche effettuate in questi esempi modificano la stringa senza crearne un'altra. L'ownership esercitata sul contenuto significa proprio questo. Sempre su questo argomento, è comprensibile che vi siano problemi di performance quando si operano molte modifiche. Un modo per calmierare le cose può essere quello che fa uso del metodo with_capacity(n). Tale metodo predispone lo spazio indicato per accogliere una stringa ed è quindi indicato qualora si sappia le dimensioni di quello che sarà accolto. let mut s1 = String::with_capacity(50); Qui s1 ha lunghezza 0 ma per essa è già predisposta una capacità di 50 elementi. Qualora dovessimo aggiungerne non sarà necessario quindi allocare nuovo spazio, tutto a vantaggio delle prestazioni. L'accesso al singolo elemento è il medesimo visto per le string slice: let s1 = "abcd".to_owned(); println!("{:?}", s1.chars().nth(2).unwrap()); è interessante notare che il seguente codice funziona: fn main() { let s1 = "abcd".to_owned(); let x = 2; println!("{:?}", s1.chars().nth(x).unwrap()); } mentre quest'altro origina un errore: fn main() { let s1 = "abcd".to_owned(); let x: i32 = 2; println!("{:?}", s1.chars().nth(x).unwrap()); } questo perchè nel secondo caso Rust non può convertire in usize un elemento che vogliamo sia un i32. Selezionare un indice fuori range, in questa fase almeno, dà origine ad un crash del programma. Questo vale anche per le string slice, naturalmente. Dico in questa fase perchè più avanti impareremo a gestire anche queste sictuazioni. La concatenazione delle stringhe è ottenibile in vari modi: il primo è il classico concat! che si applica su string slice let s3 = concat!("ciao", "ciao", "ciao"); oppure concat, come visto con le string slice let s1 = "ciao".to_string(); let s2 = "ciao".to_string(); let s3 = [s1, s2].concat(); anche push_str(stringa) permette di unire una stringa con una string slice let mut s1 = "ciao".to_string(); s1.push_str("ddd"); oppure operando sulle stringhe: let mut s1 = "aaa".to_string(); let s2 = "bbb".to_string(); s1.push_str(&s2); //s2 usata per riferimento e poi abbiamo già visto, ovviamente format! che lavora direttamente su stringhe let s1 = "aaa".to_string(); let s2 = "bbb".to_string(); let s3 = format!("{}{}", s1, s2); Si può anche usare l'operatore +: let s1 = "aaa".to_string(); let s2 = "bbb".to_string(); let s3 = s1 + &s2; // s2 è usata per riferimento è interessante notare appunto che con il + il secondo termine deve essere usato per riferimento. Questo perchè usa l'implementazione interna di Add che è definita, in parte, come segue: fn add(mut self, other: &str) -> String Per trovare la prima occorrenza di un carattere in una stringa potreste usare il metodo find(carattere). Questo restituisce un tipo Option, che vedremo nella apposita sezione per cui potreste, di primo acchito avere qualche sorpresa. Per ora prendete l'esempio così come viene:
che restituisce
Come detto questo sarà più chiaro quando vedremo il tipo Option. Evidentemente 0 è l'indice della prima occorrenza di 'a' mentre None si riferisce al fatto che 'k' non è presente nella stringa "aaa". Un altro metodo fa uso dell'iteratore chars(): println!("{}", s1.chars().position(|x| x == 'a').unwrap()); Per copiare una stringa in un'altra seguiamo il suggerimento che ci ha dato in precedenza il compilatore quando avevamo cercato di usare il segno = per effettuare l'operazione. Facciamo uso quindi di clone()
con output:
le due stringhe sono indipendenti e le variazioni su s1 non influiscono su s2. Chiudiamo con una annotazione più leggera: anche con le nostre String, è possibile scrivere l'output su più righe inserendo un " a capo" che può essere anche aggirato, proprio come abbiamo visto nel capitolo precedente. let s1 = "ciao Mondo".to_owned(); let s2 = "ciao\ Mondo".to_owned(); Questa è solo una panoramica superficiale di quanto disponibile per le stringhe. Il sito ufficiale, nella pagina dedicata, riporta ovviamente tutto e, come per le string slice, vi consiglio di dare una lettura a quanto vi troverete. Torniamo per un attimo, per concludere, ad una questione che a volte sorge riguarda la convenienza dell'uso delle slice string o delle stringhe. In generale le prime si fanno preferire in quanto più leggere e performanti, specialmente in progetti pesanti. Se avete bisogno della possibilità di modificare una sequenza di caratteri le stringhe sono il tipo adatto, in particolare se ne potrete definire in anticipo la capacità, così come quando non sapete quanto sarà la durata, nell'ambito del programma, della vostra sequenza. |