Salta ai contenuti

Slice in Rust

Una slice ti permette di prendere in prestito una parte di un array o di un vettore (una «vista» su un pezzo di esso) senza copiare i dati e senza prenderne il possesso. Se hai già usato il prestito (borrowing) con un singolo valore, la slice è la stessa idea applicata a un intervallo di elementi.

fn main() {
let numbers = [10, 20, 30, 40, 50];
let middle = &numbers[1..4]; // gli elementi agli indici 1, 2, 3
println!("{:?}", middle);
}

Output:

[20, 30, 40]
▶ Provalo tu

1..4 è lo stesso tipo di range che hai visto nei cicli for: parte dall’indice 1 e si ferma prima dell’indice 4, quindi ottieni gli indici 1, 2 e 3.

Alcune scorciatoie che capitano spesso:

fn main() {
let numbers = [10, 20, 30, 40, 50];
println!("{:?}", &numbers[..3]); // dall'inizio fino all'indice 3 -> [10, 20, 30]
println!("{:?}", &numbers[2..]); // dall'indice 2 alla fine -> [30, 40, 50]
println!("{:?}", &numbers[..]); // tutto quanto -> [10, 20, 30, 40, 50]
}
▶ Provalo tu

In realtà stai già usando le slice: &str è una slice di una String (o di un’altra stringa)! Ecco perché i due tipi vanno così d’accordo:

fn main() {
let sentence = String::from("Una rapida volpe marrone");
let first_word = &sentence[0..3];
println!("{first_word}"); // "Una"
}
▶ Provalo tu

Gli indici di una slice di stringa contano i byte, non le lettere. Qui va tutto bene perché «Una» non ha lettere accentate; una lettera come «è» occupa 2 byte, quindi con gli accenti gli indici vanno scelti con più attenzione.

Potresti pensare di risolvere il problema copiando in un nuovo Vec o in una nuova String la parte che ti interessa. Le slice evitano del tutto la copia: puntano semplicemente a una parte dei dati che esistono già, come un riferimento (&) punta a un singolo valore. Quindi niente allocazioni in più, niente copie, e continuano a valere le regole di prestito che conosci dalla pagina sul Borrowing.

Le slice sono particolarmente utili come parametri di funzione, perché accettano sia array sia vettori:

fn sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for n in numbers {
total += n;
}
total
}
fn main() {
let array = [1, 2, 3];
let vector = vec![4, 5, 6];
println!("{}", sum(&array));
println!("{}", sum(&vector));
}
▶ Provalo tu

&[i32] accetta una slice presa sia da un array sia da un Vec: una sola funzione, e funzionano entrambe le chiamate.

Finora le nostre slice hanno solo letto i dati. Una slice mutabile, &mut [T], permette a una funzione di modificare gli elementi che ha ricevuto, ma solo quelli:

fn double_all(numbers: &mut [i32]) {
for i in 0..numbers.len() {
numbers[i] *= 2;
}
}
fn main() {
let mut scores = vec![1, 2, 3, 4, 5];
double_all(&mut scores[1..3]); // solo gli indici 1 e 2
println!("{:?}", scores);
double_all(&mut scores); // tutto il vettore
println!("{:?}", scores);
}

Output:

[1, 4, 6, 4, 5]
[2, 8, 12, 8, 10]
▶ Provalo tu

La prima chiamata passa &mut scores[1..3], quindi double_all vede solo gli indici 1 e 2. Tutto quello che sta fuori da quel range è al sicuro. Dentro la funzione la slice riparte dall’indice 0: per double_all è soltanto una lista di due numeri.

È sicuro grazie a una regola che conoscerai per bene nella pagina sul Borrowing: finché la funzione tiene una slice &mut, nient’altro può usare quegli elementi, quindi due parti del codice non possono mai modificarli nello stesso momento. Anche il vettore deve essere dichiarato mut, altrimenti Rust non ti lascia prenderlo in prestito in modo mutabile.

Crea il vettore vec![3, 6, 9, 12, 15, 18]. Stampa la prima metà e la seconda metà come due slice separate. Suggerimento: calcola let mid = numbers.len() / 2; e usa ..mid e mid...

Mostra la soluzione
fn main() {
let numbers = vec![3, 6, 9, 12, 15, 18];
let mid = numbers.len() / 2;
let first_half = &numbers[..mid];
let second_half = &numbers[mid..];
println!("Prima metà: {:?}", first_half);
println!("Seconda metà: {:?}", second_half);
}

Output:

Prima metà: [3, 6, 9]
Seconda metà: [12, 15, 18]
▶ Provalo tu

Visto che mid è calcolato a partire da len(), il codice continua a funzionare anche se aggiungi o togli numeri. Con una lunghezza dispari, l’elemento in più finisce nella seconda metà.

Scrivi una funzione largest(numbers: &[i32]) -> i32 che restituisce il numero più grande. Chiamala con un array, con un vettore e con una slice che contiene solo il primo elemento del vettore. Suggerimento: parti da numbers[0] e scorri il resto, come faresti con un array.

Mostra la soluzione
fn largest(numbers: &[i32]) -> i32 {
let mut biggest = numbers[0];
for n in numbers {
if *n > biggest {
biggest = *n;
}
}
biggest
}
fn main() {
let array = [7, 2, 9, 4];
let vector = vec![10, 35, 21];
println!("{}", largest(&array));
println!("{}", largest(&vector));
println!("{}", largest(&vector[..1]));
}

Output:

9
35
10
▶ Provalo tu

Scorrendo una slice ricevi un riferimento a ogni elemento (&i32), non il numero stesso. *n significa «il valore a cui punta n», quindi *n > biggest confronta due semplici numeri. Questa versione va in panic con una slice vuota, perché numbers[0] non esiste. Più avanti imparerai a restituire un Option per quel caso.

  • Una slice (&[T]) prende in prestito una parte di un array o di un vettore senza copiarla.
  • 1..4 seleziona gli indici da 1 a 3; ometti uno dei due lati di .. per partire dall’inizio o arrivare alla fine.
  • &str è a sua volta una slice di testo: per questo stringhe e slice si comportano in modo così simile.
  • Preferisci &[T] come tipo di parametro quando vuoi accettare sia array sia vettori.
  • Una slice mutabile (&mut [T]) permette a una funzione di modificare solo gli elementi che ha ricevuto.

Verifica veloce

1. Che cosa ti permette di fare una slice con una parte di un array o di un vettore?

2. Quali indici include &numbers[1..4]?

3. Perché &[i32] è una buona scelta come tipo di parametro di una funzione?

Punteggio: 0 / 3