Il Tipo Slice
Le Slices ti permettono di fare riferimento a una sequenza contigua di elementi in una collezione anziché all'intera collezione. Una slice è un tipo di riferimento, quindi non ha ownership.
Ecco un piccolo problema di programmazione: scrivi una funzione che prenda una stringa di parole separate da spazi e restituisca la prima parola trovata in quella stringa. Se la funzione non trova uno spazio nella stringa, l'intera stringa deve essere una parola, quindi l'intera stringa dovrebbe essere restituita.
Vediamo come scriveremmo la firma di questa funzione senza usare slices, per capire il problema che le slices risolveranno:
fn first_word(s: &String) -> ?
La funzione first_word
ha un &String
come parametro. Non vogliamo
ownership, quindi questo va bene. Ma cosa dovremmo restituire? Non abbiamo davvero un
modo per parlare di parte di una stringa. Tuttavia, potremmo restituire l'indice della
fine della parola, indicato da uno spazio. Proviamoci, come mostrato nell'Elenco 4-7.
Nome file: src/main.rs
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() {}
Poiché dobbiamo attraversare il String
elemento per elemento e verificare se
un valore è uno spazio, convertiremo il nostro String
in un array di byte usando il
metodo as_bytes
.
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
Successivamente, creiamo un iteratore sull'array di byte usando il metodo iter
:
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
Discuteremo degli iteratori in maggior dettaglio nel Capitolo 13.
Per ora, sappi che iter
è un metodo che restituisce ciascun elemento in una collezione
e che enumerate
avvolge il risultato di iter
e restituisce ciascun elemento come
parte di una tupla invece. Il primo elemento della tupla restituita da
enumerate
è l'indice, e il secondo elemento è un riferimento all'elemento.
Questo è un po' più conveniente rispetto a calcolare l'indice noi stessi.
Poiché il metodo enumerate
restituisce una tupla, possiamo usare le patterns per
distruggere quella tupla. Discuteremo di più sui patterns nel Capitolo
6. Nel for
loop, specifichiamo un pattern che ha i
per l'indice nella tupla e &item
per il singolo byte nella tupla.
Poiché otteniamo un riferimento all'elemento da .iter().enumerate()
, usiamo
&
nel pattern.
All'interno del for
loop, cerchiamo il byte che rappresenta lo spazio usando
la sintassi del byte letterale. Se troviamo uno spazio, restituiamo la posizione.
Altrimenti, restituiamo la lunghezza della stringa usando s.len()
.
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
Ora abbiamo un modo per scoprire l'indice della fine della prima parola nella
stringa, ma c'è un problema. Stiamo restituendo un usize
da solo, ma è
solo un numero significativo nel contesto del &String
. In altre parole,
poiché è un valore separato dalla String
, non c'è garanzia che
sarà ancora valido in futuro. Considera il programma nell'Elenco 4-8 che
usa la funzione first_word
dell'Elenco 4-7.
Nome file: src/main.rs
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() { let mut s = String::from("hello world"); let word = first_word(&s); // word will get the value 5 s.clear(); // this empties the String, making it equal to "" // word still has the value 5 here, but there's no more string that // we could meaningfully use the value 5 with. word is now totally invalid! }
Questo programma compila senza errori e lo farebbe anche se usassimo word
dopo aver chiamato s.clear()
. Poiché word
non è collegato allo stato di s
affatto, word
contiene ancora il valore 5
. Potremmo usare quel valore 5
con
la variabile s
per provare a estrarre la prima parola, ma questo sarebbe un bug
poiché i contenuti di s
sono cambiati da quando abbiamo salvato 5
in word
.
Doversi preoccupare che l'indice in word
non sia sincronizzato con i dati in s
è noioso e soggetto a errori! Gestire questi indici è ancora più fragile se
scriviamo una funzione second_word
. La sua firma dovrebbe avere questo aspetto:
fn second_word(s: &String) -> (usize, usize) {
Ora stiamo tenendo traccia di un indice di inizio e uno di fine, e abbiamo ancora più valori che sono stati calcolati dai dati in uno stato particolare ma non sono legati a quello stato. Abbiamo tre variabili non correlate in giro che devono essere mantenute sincronizzate.
Fortunatamente, Rust ha una soluzione a questo problema: le string slice.
Le String Slice
Una string slice è un riferimento a parte di una String
, e appare così:
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; }
Piuttosto che un riferimento all'intero String
, hello
è un riferimento a una
porzione del String
, specificato nella parte extra [0..5]
. Creiamo slices
usando un intervallo all'interno delle parentesi specificando [starting_index..ending_index]
,
dove starting_index
è la prima posizione nella slice e ending_index
è
un numero maggiore dell'ultima posizione nella slice. Internamente, la struttura di dati della
slice memorizza la posizione di inizio e la lunghezza della slice, che
corrisponde a ending_index
meno starting_index
. Quindi, nel caso di let world = &s[6..11];
, world
sarebbe una slice che contiene un puntatore al
byte all'indice 6 di s
con una lunghezza di 5
.
La Figura 4-6 mostra questo in un diagramma.
Con la sintassi di intervallo ..
di Rust, se vuoi iniziare all'indice 0, puoi omettere
il valore prima dei due punti. In altre parole, questi sono uguali:
#![allow(unused)] fn main() { let s = String::from("hello"); let slice = &s[0..2]; let slice = &s[..2]; }
Allo stesso modo, se la tua slice include l'ultimo byte del String
, puoi omettere
il numero finale. Ciò significa che questi sono uguali:
#![allow(unused)] fn main() { let s = String::from("hello"); let len = s.len(); let slice = &s[3..len]; let slice = &s[3..]; }
Puoi anche omettere entrambi i valori per prendere una slice dell'intera stringa. Quindi questi sono uguali:
#![allow(unused)] fn main() { let s = String::from("hello"); let len = s.len(); let slice = &s[0..len]; let slice = &s[..]; }
Nota: Gli indici di intervallo delle String slice devono trovarsi a confini di caratteri UTF-8 validi. Se tenti di creare una string slice nel mezzo di un carattere multibyte, il tuo programma terminerà con un errore. Ai fini di introdurre le string slice, stiamo assumendo solo ASCII in questa sezione; una discussione più approfondita sulla gestione UTF-8 è nella sezione “Memorizzare Testo Codificato UTF-8 con Stringhe” del Capitolo 8.
Con tutte queste informazioni in mente, riscriviamo first_word
per restituire una
slice. Il tipo che indica “string slice” è scritto come &str
:
Nome file: src/main.rs
fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() {}
Otteniamo l'indice per la fine della parola allo stesso modo in cui abbiamo fatto nell'Elenco 4-7, cercando la prima ocorrenza di spazio. Quando troviamo uno spazio, restituiamo una string slice usando l'inizio della stringa e l'indice dello spazio come indici di inizio e fine.
Ora, quando chiamiamo first_word
, otteniamo un singolo valore che è legato ai dati sottostanti. Il valore è composto da un riferimento al punto di inizio della slice e il numero di elementi nella slice.
Restituire una slice funzionerebbe anche per una funzione second_word
:
fn second_word(s: &String) -> &str {
Abbiamo ora un'API semplice che è molto più difficile da mettere male perché
il compilatore assicurerà che i riferimenti nella String
rimangano validi. Ricorda
il bug nel programma nell'Elenco 4-8, quando abbiamo ottenuto l'indice alla fine
della prima parola ma poi abbiamo svuotato la stringa, quindi il nostro indice non era più valido? Quel codice era logicamente scorretto ma non mostrava errori immediati. I problemi
si manifesterebbero più tardi se avessimo continuato a usare l'indice della prima parola
con una stringa vuota. Le slices rendono impossibile questo bug e ci fanno sapere
che abbiamo un problema con il nostro codice molto prima. Usare la versione con slice di first_word
genererà un errore di compilazione:
Nome file: src/main.rs
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {}", word);
}
Ecco l'errore del compilatore:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:18:5
|
16 | let word = first_word(&s);
| -- immutable borrow occurs here
17 |
18 | s.clear(); // error!
| ^^^^^^^^^ mutable borrow occurs here
19 |
20 | println!("the first word is: {}", word);
| ---- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error
Ricorda dalle regole del borrowing che se abbiamo un riferimento immutabile a
qualcosa, non possiamo anche prendere un riferimento mutable. Poiché clear
ha bisogno
di troncare il String
, ha bisogno di ottenere un riferimento mutable. Il println!
dopo la chiamata a clear
usa il riferimento in word
, quindi il riferimento
immutabile deve essere ancora attivo a quel punto. Rust non permette che esistano
allo stesso tempo il riferimento mutable in clear
e il riferimento immutabile in word
,
e la compilazione fallisce. Non solo Rust ha reso la nostra API più facile da usare,
ma ha anche eliminato un'intera classe di errori al momento della compilazione!
I Letterali di Stringa come Slices
Ricorda che abbiamo parlato di letterali di stringa memorizzati all'interno del binary. Ora che conosciamo le slices, possiamo capire correttamente i letterali di stringa:
#![allow(unused)] fn main() { let s = "Hello, world!"; }
Il tipo di s
qui è &str
: è una slice che punta a quel preciso punto del
binary. Questo è anche il motivo per cui i letterali di stringa sono immutabili; &str
è un
riferimento immutabile.
Le String Slices come Parametri
Sapere che puoi prendere slices di letterali e valori di String
ci porta a
un miglioramento su first_word
, e questa è la sua firma:
fn first_word(s: &String) -> &str {
Un Rustacean più esperto scriverebbe la firma mostrata nell'Elenco 4-9
invece perché ci permette di usare la stessa funzione sia sui valori &String
che sui valori &str
.
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// `first_word` works on slices of `String`s, whether partial or whole
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word` works on slices of string literals, whether partial or whole
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
Se abbiamo una string slice, possiamo passarla direttamente. Se abbiamo una String
, possiamo passare una slice della String
o un riferimento alla String
. Questa
flessibilità sfrutta le deref coercions, una caratteristica che tratteremo nella
sezione “Le Coercioni Deref Implicite con Funzioni e Metodi” del Capitolo 15.
Definire una funzione per prendere una string slice invece di un riferimento a una String
rende la nostra API più generale e utile senza perdere nessuna funzionalità:
Nome file: src/main.rs
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("hello world"); // `first_word` works on slices of `String`s, whether partial or whole let word = first_word(&my_string[0..6]); let word = first_word(&my_string[..]); // `first_word` also works on references to `String`s, which are equivalent // to whole slices of `String`s let word = first_word(&my_string); let my_string_literal = "hello world"; // `first_word` works on slices of string literals, whether partial or whole let word = first_word(&my_string_literal[0..6]); let word = first_word(&my_string_literal[..]); // Because string literals *are* string slices already, // this works too, without the slice syntax! let word = first_word(my_string_literal); }
Altre Slices
Le string slice, come potresti immaginare, sono specifiche per le stringhe. Ma c'è un tipo più generale di slice. Considera questo array:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; }
Proprio come potremmo voler fare riferimento a parte di una stringa, potremmo voler fare riferimento a parte di un array. Lo faremmo così:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]); }
Questa slice ha il tipo &[i32]
. Funziona allo stesso modo delle slices di stringa, memorizzando un riferimento al primo elemento e una lunghezza. Userai questo tipo di
slice per ogni sorta di altre collezioni. Discuteremo queste collezioni in
dettaglio quando parleremo dei vettori nel Capitolo 8.
Sommario
I concetti di ownership, borrowing, e slices assicurano la sicurezza della memoria nei programmi Rust al momento della compilazione. Il linguaggio Rust ti dà il controllo sull'uso della memoria nella stessa maniera di altri linguaggi di programmazione di sistema, ma avere il proprietario dei dati che elimina automaticamente quei dati quando il proprietario esce dallo scope significa che non devi scrivere e debugare codice extra per ottenere questo controllo.
L'ownership influenza come funzionano molte altre parti di Rust, quindi parleremo di
questi concetti ulteriormente nel resto del libro. Passiamo ora al
Capitolo 5 e guardiamo come raggruppare pezzi di dati insieme in uno Struct
.