Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Il Type Slice

Le slice (sezioni, fette, porzioni in italiano) ti permettono di fare riferimento (un reference) a una sequenza contigua di elementi in una collezione. Una slice è una tipologia di reference, 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 che trova in quella stringa. Se la funzione non trova uno spazio nella stringa, l’intera stringa deve essere considerata come una sola parola, quindi deve essere restituita l’intera stringa.

Nota: Ai fini dell’introduzione alle slice di stringhe, stiamo assumendo solo caratteri ASCII in questa sezione; una discussione più approfondita sulla gestione di UTF-8 si trova nella sezione “Memorizzare testo codificato UTF-8 con le stringhe” del Capitolo 8.

Lavoriamo su come scrivere la firma di questa funzione senza usare slice per ora, per comprendere il problema che le slice risolveranno:

fn prima_parola(s: &String) -> ?

La funzione prima_parola ha un parametro di tipo &String. Non abbiamo bisogno di ownership, quindi va bene. (In Rust idiomatico, le funzioni non prendono la ownership dei loro argomenti se non strettamente necessario, e i motivi per questo diventeranno chiari man mano che andremo avanti.) Ma cosa dovremmo ritornare? Non abbiamo davvero un modo per descrivere una parte di una stringa. Tuttavia, potremmo restituire l’indice della fine della parola, indicato da uno spazio. Proviamo a farlo, come mostrato nel Listato 4-7.

File: src/main.rs
fn prima_parola(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &lettera) in bytes.iter().enumerate() {
        if lettera == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}
Listato 4-7: La funzione prima_parola che restituisce un valore di indice byte nella variabile String

Poiché dobbiamo esaminare la 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 prima_parola(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &lettera) in bytes.iter().enumerate() {
        if lettera == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Successivamente, creiamo un iteratore sull’array di byte usando il metodo iter:

fn prima_parola(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &lettera) in bytes.iter().enumerate() {
        if lettera == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Discuteremo gli iteratori in modo più dettagliato nel Capitolo 13. Per ora, sappi che iter è un metodo che restituisce ogni elemento in una collezione e che enumerate prende il risultato di iter e restituisce ogni elemento come parte di una tupla. 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 calcolarci l’indice da soli.

Poiché il metodo enumerate restituisce una tupla, possiamo usare i pattern per destrutturare quella tupla. Discuteremo meglio i pattern 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é da .iter().enumerate() otteniamo un reference all’elemento, usiamo & nel pattern.

All’interno del ciclo for, cerchiamo il byte che rappresenta lo spazio usando la sintassi del letterale byte. Se troviamo uno spazio, restituiamo la posizione. Altrimenti, restituiamo la lunghezza della stringa usando s.len().

fn prima_parola(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &lettera) in bytes.iter().enumerate() {
        if lettera == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Ora abbiamo un metodo per scoprire l’indice della fine della prima parola nella stringa, ma c’è un problema. Stiamo ritornando un usize da solo, che è un numero significativo solo se usato in contesto con &String. In altre parole, poiché è un valore separato dalla String, non c’è garanzia che rimarrà valido in futuro. Considera il programma nel Listing 4-8 che utilizza la funzione prima_parola dal Listing 4-7.

File: src/main.rs
fn prima_parola(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &lettera) in bytes.iter().enumerate() {
        if lettera == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {
    let mut s = String::from("hello world");

    let parola = prima_parola(&s); // `parola` riceverà il valore 5

    s.clear(); // questo svuota la String, rendendola uguale a ""

    // `parola` mantiene ancora il valore di 5, ma `s` non contiene più quello a cui
    // quel 5 si riferisce, quindi `parola` è adesso considerabile come non valida!
}
Listato 4-8: Memorizzare il risultato della chiamata alla funzione prima_parola e poi modificare il contenuto della String

Questo programma si compila senza errori e lo farebbe anche se usassimo parola dopo aver chiamato s.clear(). Poiché parola non è collegato allo stato di s, parola 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 parola.

Doversi preoccupare dell’indice in parola che si disallinea con i dati in s è noioso e soggetto a errori! Gestire questi indici è ancora più fragile se scriviamo una funzione seconda_parola. La sua firma dovrebbe apparire così:

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

Ora stiamo tracciando un indice di partenza e un indice di fine, e abbiamo ancora altri valori che sono stati calcolati da dati in un determinato stato, ma che non sono per nulla legati a quello stato in qualche modo. Abbiamo tre variabili non correlate e indipendenti che noi dobbiamo mantenere in sincronia.

Fortunatamente, Rust ha una soluzione a questo problema: le slice di stringhe.

Slice di Stringa

Una slice di stringa (string slice) è un reference a una sequenza contigua degli elementi di una String, e appare così:

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];
}

Invece di un reference all’intera String, hello è un reference a una porzione della String, specificata con l’aggiunta di [0..5]. Creiamo le slice usando un intervallo all’interno delle parentesi quadre specificando [indice_inizio..indice_fine], dove indice_inizio è la prima posizione nella slice e indice_fine è l’ultima posizione nella slice più uno. Internamente, la struttura dati della slice memorizza la posizione iniziale e la lunghezza della slice, che corrisponde a indice_fine meno indice_inizio. Quindi, nel caso di let world = &s[6..11];, world sarebbe una slice che contiene un puntatore al byte all’indice 6 di w con un valore di lunghezza di 5.

La Figura 4-7 mostra questo in un diagramma.

Tre
tabelle: una tabella che rappresenta i dati dello stack di s, che punta al byte
all’indice 0 in una tabella dei dati della stringa “hello world” nell’heap. La
terza tabella rappresenta i dati sullo stack dello slice world, che ha un valore
di lunghezza di 5 e punta al byte 6 della tabella dei dati nell’heap.

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

Con la sintassi d’intervallo .. di Rust, se vuoi iniziare dall’indice 0, puoi omettere il valore prima dei due punti. In altre parole, questi sono equivalenti:

#![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. Ciò significa che questi sono equivalenti:

#![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 equivalenti:

#![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 slice di stringa devono trovarsi in posizioni valide tenendo conto anche dei caratteri UTF-8. Se tenti di creare una slice nel mezzo di un carattere multi-byte, il tuo programma terminerà con un errore.

Tenendo presente tutte queste informazioni, riscriviamo prima_parola per restituire una slice. Il type che indica la slice di stringa è scritto come &str:

File: src/main.rs
fn prima_parola(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &lettera) in bytes.iter().enumerate() {
        if lettera == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {}

Otteniamo l’indice per la fine della parola nello stesso modo in cui lo abbiamo fatto nel Listato 4-7, cercando la prima occorrenza di uno spazio. Quando troviamo uno spazio, restituiamo una slice di stringa usando l’inizio della stringa e l’indice dello spazio come indici di inizio e fine.

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

Restituire una slice funzionerebbe anche per una funzione seconda_parola:

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

Ora abbiamo una funzione più semplice in cui è molto più difficile succedano cose strane perché il compilatore garantirà che i reference alla String rimangano validi. Ricordi il bug nel programma nel Listato 4-8, quando abbiamo ottenuto l’indice per la fine della prima parola ma poi abbiamo svuotato la stringa, rendendo il nostro indice non valido? Quel codice era logicamente errato ma non mostrava immediatamente errori. I problemi si sarebbero manifestati più tardi se avessimo continuato a cercare di usare l’indice della prima parola con una stringa svuotata. Le slice rendono questo bug impossibile e ci fanno sapere che abbiamo un problema con il nostro codice molto prima. Usare la versione slice di prima_parola genererà un errore di compilazione:

File: src/main.rs
fn prima_parola(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &lettera) in bytes.iter().enumerate() {
        if lettera == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let mut s = String::from("hello world");

    let parola = prima_parola(&s);

    s.clear(); // errore!

    println!("la prima parola è: {parola}");
}

Ecco l’errore del compilatore:

$ cargo run
   Compiling ownership v0.1.0 (file:///progetti/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:19:5
   |
17 |     let parola = prima_parola(&s);
   |                               -- immutable borrow occurs here
18 |
19 |     s.clear(); // errore!
   |     ^^^^^^^^^ mutable borrow occurs here
20 |
21 |     println!("la prima parola è: {parola}");
   |                                   ------ immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Ricorda le regole di borrowing: se abbiamo un reference immutabile a qualcosa, non possiamo anche prendere un reference mutabile. Poiché clear deve troncare la String, ha bisogno di ottenere un reference mutabile. Il println! dopo la chiamata a clear utilizza il reference a parola, quindi il reference immutabile deve essere ancora attivo a quel punto. Rust vieta che il reference mutabile in clear e il reference immutabile a parola esistano contemporaneamente, e la compilazione fallisce. Non solo Rust ha reso la nostra funzione più facile da usare, ma ha anche eliminato un’intera classe di errori durante la compilazione!

Letterali Stringa come Slice

Ricordi che abbiamo parlato dei letterali stringa memorizzati all’interno del binario? Ora che abbiamo scoperto le slice, possiamo comprendere correttamente i letterali stringa:

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

Il type di s qui è &str: è una slice che punta a quel punto specifico nel binario. Questo è anche il motivo per cui i letterali stringa sono immutabili; &str è un reference immutabile.

Slice di Stringa come Parametri

Sapendo che puoi avere slice di letterali e di valori String, arriviamo a un ulteriore miglioramento per prima_parola, e cioè la sua firma:

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

Un Rustacean più esperto scriverebbe invece la firma come mostrata nel Listato 4-9, perché ci permette di usare la stessa funzione sia su valori &String che su valori &str.

fn prima_parola(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &lettera) in bytes.iter().enumerate() {
        if lettera == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let mia_stringa = String::from("hello world");

    // `prima_parola` funziona con slice di `String`, parziali o intere.
    let parola = prima_parola(&mia_stringa[0..6]);
    let parola = prima_parola(&mia_stringa[..]);
    // `prima_parola` funziona anche con reference a `String`, che corrisponde
    // a una slice intera di `String`.
    let parola = prima_parola(&mia_stringa);

    let mia_stringa_letterale = "hello world";

    // `prima_parola` funziona con slice di letterali di stringa,
    // parziali o intere.
    let parola = prima_parola(&mia_stringa_letterale[0..6]);
    let parola = prima_parola(&mia_stringa_letterale[..]);

    // E siccome i letterali di stringa *sono* già delle slice,
    // funziona pure così, senza usare la sintassi delle slice!
    let parola = prima_parola(mia_stringa_letterale);
}
Listato 4-9: Migliorare la funzione prima_parola utilizzando una slice come type del parametro s

Se abbiamo una slice di stringa, possiamo passarlo direttamente. Se abbiamo una String, possiamo passare una slice della String o un reference alla String. Questa flessibilità sfrutta la deref coercions (de-referenziazione forzata), una funzionalità che tratteremo nella sezione “Usare la De-Referenziazione Forzata in Funzioni e Metodi” del Capitolo 15.

Definire una funzione che come parametro prende una slice di stringa invece di un reference a una String rende la nostra funzione più generica e utile senza perdere alcuna funzionalità:

File: src/main.rs
fn prima_parola(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &lettera) in bytes.iter().enumerate() {
        if lettera == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let mia_stringa = String::from("hello world");

    // `prima_parola` funziona con slice di `String`, parziali o intere.
    let parola = prima_parola(&mia_stringa[0..6]);
    let parola = prima_parola(&mia_stringa[..]);
    // `prima_parola` funziona anche con reference a `String`, che corrisponde
    // a una slice intera di `String`.
    let parola = prima_parola(&mia_stringa);

    let mia_stringa_letterale = "hello world";

    // `prima_parola` funziona con slice di letterali di stringa,
    // parziali o intere.
    let parola = prima_parola(&mia_stringa_letterale[0..6]);
    let parola = prima_parola(&mia_stringa_letterale[..]);

    // E siccome i letterali di stringa *sono* già delle slice,
    // funziona pure così, senza usare la sintassi delle slice!
    let parola = prima_parola(mia_stringa_letterale);
}

Altre Slice

Le slice di stringa, come puoi immaginare, sono specifiche per le stringhe. Ma c’è anche un tipo di slice più generale. 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 in questo modo:

#![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 type &[i32]. Funziona allo stesso modo delle string slice, memorizzando un reference al primo elemento e una lunghezza. Utilizzerai questo tipo di slice per tutti i type di altre collezioni. Discuteremo di queste collezioni in dettaglio quando parleremo dei vettori nel Capitolo 8.

I concetti di ownership, borrowing e slice garantiscono la sicurezza della memoria nei programmi Rust già in fase d compilazione. Il linguaggio Rust ti offre il controllo sul tuo utilizzo della memoria nello stesso modo in cui fanno altri linguaggi di programmazione di sistema, ma avere un proprietario (ownership) per ogni dato e che questo pulisca automaticamente i propri dati quando se ne va (non più in scope), significa non dover scrivere e debuggare codice extra per ottenere questo controllo.

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