Il Tipo Slice

Le Slices ti permettono di riferirti a una sequenza contigua di elementi in una collezione piuttosto che a tutta la collezione. Una slice è un tipo di riferimento, quindi non ha ownership.

Ecco un piccolo problema di programmazione: scrivi una funzione che prende una stringa di parole separate da spazi e restituisce la prima parola che trova in quella stringa. Se la funzione non trova uno spazio nella stringa, tutta la stringa deve essere una parola, quindi deve restituire l'intera stringa.

Vediamo come scrivere la firma di questa funzione senza usare slices, per comprendere il problema che le slices risolvono:

fn first_word(s: &String) -> ?

La funzione first_word ha un parametro &String. 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 nel Listing 4-7.

Filename: 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() {}

Listing 4-7: La funzione first_word che restituisce un valore di indice in byte nel parametro String

Poiché dobbiamo attraversare l'elemento String elemento per elemento e controllare se un valore è uno spazio, convertiremo la nostra 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 gli iteratori in maggior dettaglio nel Capitolo 13. Per ora, sappi che iter è un metodo che restituisce ogni 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 che calcolare l'indice da soli.

Poiché il metodo enumerate restituisce una tupla, possiamo usare i pattern per decomporre quella tupla. Discuteremo i pattern più dettagliatamente nel Capitolo 6. Nel ciclo for, 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 ciclo for, 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 trovare 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 di &String. In altre parole, poiché è un valore separato dalla String, non c'è garanzia che sarà ancora valido in futuro. Considera il programma nel Listing 4-8 che usa la funzione first_word dal Listing 4-7.

Filename: 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!
}

Listing 4-8: Memorizzare il risultato della chiamata alla funzione first_word e poi cambiare il contenuto della String

Questo programma compila senza errori e lo farebbe anche se usassimo word dopo aver chiamato s.clear(). Poiché word non è collegato affatto allo stato di s, word contiene ancora il valore 5. Potremmo usare quel valore 5 con la variabile s per cercare di estrarre la prima parola, ma questo sarebbe un bug perché il contenuto di s è cambiato da quando abbiamo salvato 5 in word.

Doversi preoccupare dell'indice in word che si disallinea con i dati in s è tedioso e soggetto a errori! La gestione di questi indici è ancora più fragile se scriviamo una funzione second_word. La sua firma dovrebbe assomigliare a questa:

fn second_word(s: &String) -> (usize, usize) {

Ora stiamo tracciando un indice di inizio e un indice di fine, e abbiamo ancora più valori che sono stati calcolati da dati in uno stato particolare ma non sono affatto legati a quello stato. Abbiamo tre variabili non correlate che devono essere mantenute sincronizzate.

Per fortuna, Rust ha una soluzione a questo problema: le slices di stringa.

Slices di Stringa

Una string slice è un riferimento a una 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'intera String, hello è un riferimento a una porzione della String, specificato nella parte aggiuntiva [0..5]. Creiamo le slices utilizzando un intervallo tra parentesi specificando [starting_index..ending_index], dove starting_index è la prima posizione nella slice e ending_index è uno in più rispetto all'ultima posizione nella slice. Internamente, la struttura dati della slice memorizza la posizione iniziale 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 un valore di lunghezza di 5.

Figura 4-6 mostra questo in un diagramma.

Tre tabelle: una tabella che rappresenta i dati nello stack di s, che punta
al byte all'indice 0 in una tabella dei dati della stringa "hello world" sul heap. La terza tabella
rappresenta i dati nello stack della slice world, che ha un valore di lunghezza di 5 e punta al byte 6
della tabella dei dati sul heap.

Figura 4-6: Slice di stringa che si riferisce a una parte di una String

Con la sintassi di intervallo .. di Rust, se vuoi iniziare dall'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 della String, puoi omettere il numero finale. Questo 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 slices devono trovarsi ai confini di caratteri validi UTF-8. Se tenti di creare una slice di stringa nel mezzo di un carattere multibyte, il tuo programma terminerà con un errore. Ai fini di introdurre le slices di stringa, stiamo assumendo solo ASCII in questa sezione; una discussione più approfondita della gestione di UTF-8 si trova nella sezione “Memorizzare Testo Codificato UTF-8 con le Stringhe” del Capitolo 8.

Con tutte queste informazioni in mente, riscriviamo first_word per restituire una slice. Il tipo che rappresenta “slice di stringa” è scritto come &str:

Filename: 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 nel Listing 4-7, cercando la prima occorrenza di uno spazio. Quando troviamo uno spazio, restituiamo una slice di stringa utilizzando l'inizio della stringa e l'indice dello spazio come indici iniziale e finale.

Ora, quando chiamiamo first_word, otteniamo un singolo valore che è legato ai dati sottostanti. Il valore è composto da un riferimento al punto di partenza della slice e dal numero di elementi nella slice.

Restituire una slice funzionerebbe anche per una funzione second_word:

fn second_word(s: &String) -> &str {

Ora abbiamo un'API semplice che è molto più difficile da sbagliare perché il compilatore garantirà che i riferimenti nella String rimangano validi. Ricorda il bug nel programma nel Listing 4-8, quando abbiamo ottenuto l'indice per la fine della prima parola ma poi abbiamo svuotato la stringa così il nostro indice era invalido? Quel codice era logicamente errato ma non mostrava errori immediati. I problemi si sarebbero presentati più tardi se avessimo continuato a usare l'indice della prima parola con una stringa svuotata. Le slices rendono impossibile questo bug e ci fanno sapere di avere un problema con il nostro codice molto prima. Utilizzare la versione a slice di first_word lancerà un errore in fase di compilazione:

Filename: 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 mutabile. Poiché clear deve truncare la String, ha bisogno di ottenere un riferimento mutabile. 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 il riferimento mutabile in clear e il riferimento immutabile in word esistano contemporaneamente, 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 in fase di compilazione!

String Literals come Slices

Ricorda che abbiamo parlato di stringhe letterali memorizzate all'interno del binario. Ora che sappiamo delle slices, possiamo comprendere correttamente le stringhe letterali:

#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

Il tipo di s qui è &str: è una slice che punta a quel punto specifico del binario. Questo è anche il motivo per il quale le stringhe letterali sono immutabili; &str è un riferimento immutabile.

Slices di Stringa come Parametri

Sapere che puoi prendere slices di letterali e valori String ci porta a un altro miglioramento su first_word, ed è la sua firma:

fn first_word(s: &String) -> &str {

Un Rustacean più esperto scriverebbe la firma mostrata nel Listing 4-9 invece perché ci permette di usare la stessa funzione sia su valori &String che &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);
}

Listing 4-9: Miglioramento della funzione first_word usando una slice di stringa per il tipo del parametro s

Se abbiamo una slice di stringa, 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 copriremo nella sezione “Coercizioni Deref Implicite con Funzioni e Metodi” del Capitolo 15.

Definire una funzione per prendere una slice di stringa invece di un riferimento a una String rende la nostra API più generale e utile senza perdere alcuna funzionalità:

Filename: 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 slices di stringa, come puoi immaginare, sono specifiche per le stringhe. Ma esiste anche un tipo di slice più generale. Considera questo array:

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}

Così come potremmo voler riferirci a una parte di una stringa, potremmo voler riferirci a una 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 tutti i tipi di altre collezioni. Discuteremo queste collezioni in dettaglio quando parleremo dei vettori nel Capitolo 8.

I concetti di ownership, borrowing e slices garantiscono la sicurezza della memoria nei programmi Rust in fase di compilazione. Il linguaggio Rust ti dà il controllo sull'uso della memoria allo stesso modo di altri linguaggi di programmazione di sistemi, ma avere il proprietario dei dati che automaticamente ripulisce quei dati quando il proprietario esce dallo Scope significa che non devi scrivere e debugare codice extra per ottenere questo controllo.

L'ownership influisce sul funzionamento di molte altre parti di Rust, quindi parleremo di questi concetti ulteriormente nel resto del libro. Passiamo al Capitolo 5 e vediamo come raggruppare pezzi di dati insieme in uno struct.