Slice in Rust
Le slice in Rust sono viste (riferimenti) a una sequenza contigua di elementi in una collezione. Non possiedono i dati: sono riferimenti a una porzione di dati esistenti. Le slice sono fondamentali in Rust perche permettono di lavorare con parti di stringhe, array e vettori in modo efficiente e sicuro, senza copiare i dati.
String slice (&str)
Una string slice &str è un riferimento a una porzione di una String (o di una stringa letterale). È il tipo più comune per passare stringhe a funzioni.
fn main() {
let saluto = String::from("Ciao, mondo!");
let ciao = &saluto[0..4]; // "Ciao"
let mondo = &saluto[6..11]; // "mondo"
println!("{ciao} {mondo}");
}
La sintassi [start..end] crea una slice dall’indice start (incluso) all’indice end (escluso). Si possono omettere gli estremi:
fn main() {
let s = String::from("Rust!");
let dal_inizio = &s[..4]; // "Rust" (da 0 a 4)
let alla_fine = &s[1..]; // "ust!" (da 1 alla fine)
let tutto = &s[..]; // "Rust!" (tutta la stringa)
println!("{dal_inizio}, {alla_fine}, {tutto}");
}
Le stringhe letterali sono già delle &str:
fn main() {
let s: &str = "Ciao Rust"; // Questa è già una slice
println!("{s}");
}
Preferisci &str a &String nei parametri delle funzioni:
// Buona pratica: accetta &str
fn conta_vocali(testo: &str) -> usize {
testo.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()
}
fn main() {
let stringa = String::from("Ciao Rust");
println!("Vocali: {}", conta_vocali(&stringa)); // &String -> &str automatico
println!("Vocali: {}", conta_vocali("hello")); // &str direttamente
}
Array slice (&[T])
Una slice di array &[T] è un riferimento a una porzione di un array o vettore.
fn main() {
let numeri = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let primi_tre = &numeri[..3]; // [1, 2, 3]
let centrali = &numeri[3..7]; // [4, 5, 6, 7]
let ultimi = &numeri[7..]; // [8, 9, 10]
println!("Primi tre: {:?}", primi_tre);
println!("Centrali: {:?}", centrali);
println!("Ultimi: {:?}", ultimi);
}
Anche i vettori si possono affettare:
fn somma(numeri: &[i32]) -> i32 {
numeri.iter().sum()
}
fn main() {
let vettore = vec![10, 20, 30, 40, 50];
println!("Somma totale: {}", somma(&vettore));
println!("Somma primi 3: {}", somma(&vettore[..3]));
println!("Somma ultimi 2: {}", somma(&vettore[3..]));
}
Slice mutabili (&mut [T])
Le slice mutabili permettono di modificare gli elementi della sequenza originale.
fn azzera(slice: &mut [i32]) {
for elem in slice.iter_mut() {
*elem = 0;
}
}
fn raddoppia(slice: &mut [i32]) {
for elem in slice.iter_mut() {
*elem *= 2;
}
}
fn main() {
let mut dati = [1, 2, 3, 4, 5, 6];
raddoppia(&mut dati[..3]); // Raddoppia solo i primi 3
println!("{:?}", dati); // [2, 4, 6, 4, 5, 6]
azzera(&mut dati[3..]); // Azzera gli ultimi 3
println!("{:?}", dati); // [2, 4, 6, 0, 0, 0]
}
Metodi utili delle slice
Le slice offrono numerosi metodi per lavorare con i dati.
len e is_empty
fn main() {
let dati = [1, 2, 3, 4, 5];
let slice = &dati[..];
println!("Lunghezza: {}", slice.len()); // 5
println!("È vuota: {}", slice.is_empty()); // false
let vuota: &[i32] = &[];
println!("Vuota: {}", vuota.is_empty()); // true
}
first, last e get
fn main() {
let numeri = [10, 20, 30, 40, 50];
println!("Primo: {:?}", numeri.first()); // Some(10)
println!("Ultimo: {:?}", numeri.last()); // Some(50)
println!("Indice 2: {:?}", numeri.get(2)); // Some(30)
println!("Indice 10: {:?}", numeri.get(10)); // None (sicuro, niente panic)
// Accesso diretto con indice (può causare panic se fuori range)
println!("Indice 2: {}", numeri[2]); // 30
}
get() è preferibile all’accesso diretto con [] quando l’indice potrebbe essere fuori range.
iter
fn main() {
let frutti = ["mela", "pera", "banana"];
for (i, frutto) in frutti.iter().enumerate() {
println!("{i}: {frutto}");
}
// Somma con iter
let numeri = [1, 2, 3, 4, 5];
let somma: i32 = numeri.iter().sum();
println!("Somma: {somma}");
}
windows
windows(n) restituisce un iteratore su sotto-slice consecutive di dimensione n.
fn main() {
let dati = [1, 2, 3, 4, 5];
println!("Finestre di 3 elementi:");
for finestra in dati.windows(3) {
println!(" {:?}", finestra);
}
// [1, 2, 3]
// [2, 3, 4]
// [3, 4, 5]
}
chunks
chunks(n) divide la slice in blocchi di dimensione n. L’ultimo blocco può essere più corto.
fn main() {
let dati = [1, 2, 3, 4, 5, 6, 7];
println!("Blocchi di 3:");
for blocco in dati.chunks(3) {
println!(" {:?}", blocco);
}
// [1, 2, 3]
// [4, 5, 6]
// [7]
}
contains e starts_with / ends_with
fn main() {
let numeri = [1, 2, 3, 4, 5];
println!("Contiene 3: {}", numeri.contains(&3)); // true
println!("Contiene 9: {}", numeri.contains(&9)); // false
println!("Inizia con [1, 2]: {}", numeri.starts_with(&[1, 2])); // true
println!("Finisce con [4, 5]: {}", numeri.ends_with(&[4, 5])); // true
}
sort e reverse (slice mutabili)
fn main() {
let mut numeri = [5, 3, 8, 1, 9, 2, 7];
numeri.sort();
println!("Ordinato: {:?}", numeri); // [1, 2, 3, 5, 7, 8, 9]
numeri.reverse();
println!("Invertito: {:?}", numeri); // [9, 8, 7, 5, 3, 2, 1]
}
Sintassi di slicing [start…end]
Riepilogo delle forme di slicing:
fn main() {
let v = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let _ = &v[2..5]; // [2, 3, 4] - da indice 2 a 5 (escluso)
let _ = &v[..4]; // [0, 1, 2, 3] - dall'inizio a 4 (escluso)
let _ = &v[6..]; // [6, 7, 8, 9] - da 6 alla fine
let _ = &v[..]; // [0, 1, ..., 9] - tutta la slice
let _ = &v[2..=5]; // [2, 3, 4, 5] - da 2 a 5 (incluso)
}
Conclusione
Le slice sono uno strumento essenziale in Rust per lavorare con porzioni di dati in modo sicuro ed efficiente. Permettono di passare riferimenti a parti di collezioni senza copiare dati e senza rischiare accessi fuori dai limiti (con get()). Preferisci sempre &str a &String e &[T] a &Vec<T> nelle firme delle funzioni per rendere il codice più flessibile e idiomatico.