Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 14
Slice

Proseguendo nel nostro viaggio tra i tipi proposti da Rust è arrivato il momento di parlare degli slice, che avevamo sfiorato parlando delle sequenze letterali,  in maniera approfondita. Argomento fondamentale per questo linguaggio, si tratta in breve di un riferimento ad una sequenza contigua di elementi in memoria. Gli slice sono particolarmente utili perché offrono un modo per accedere a una porzione di un array o di un altro slice senza prendere possesso di esso, permettendo così operazioni sicure e efficienti su sottosequenze di una collezione di dati. Inoltre uno dei fatti essenzialie delle slice è che rappresentano una astrazione a costo zero in Rust. Quando lavoriamo con una slice, Rust non introduce overhead aggiuntivo a runtime rispetto a lavorare direttamente con gli array. Ciò è garantito dal fatto che il compilatore genera codice ottimizzato come se stessimo lavorando direttamente sugli array. In pratica le slice in Rust sono progettate per essere sicure e performanti. Il compilatore garantisce che  siano sempre valide e non causino errori di accesso alla memoria. 
Semanticamente abbiamo due tipi di slice:


&[T]
&mut [T] - slice mutabili

dove T ovviamente rappresenta il tipo. Come troverete scritto nella letteratura dedicata, i vantaggi degli slice si possono rissumere in:

  • Sicurezza di memoria: Rust assicura che l'accesso tramite slice sia sicuro durante la compilazione, prevenendo errori comuni come l'accesso fuori dai limiti dell'array.
  • Flessibilità: Gli slice possono essere utilizzati con diversi tipi di collezioni, aumentando la flessibilità del codice.
  • Prestazioni: Accedere ai dati tramite slice può essere molto efficiente, poiché non c'è la necessità di copiare dati o prendere possesso della collezione originale.
detto questo vediamo un esempio:

  Esempio 14.1
1
2
3
4
5
6
7
8
9
fn main() {
    let arr = [1, 2, 3, 4, 5];
    // Un slice che contiene tutti gli elementi dell'array
    let intero = &arr[..];
    // Un slice che contiene gli elementi da 1 a 3 dell'array
    let parziale = &arr[1..4];
    println!("Intero array: {:?}", intero);
    println!("Porzione di array: {:?}", parziale);
}

Come avevamo già visto, componente fondamentale degli slice è l'operatore & che in questo contesto rappresenta come detto un puntatore, un riferimento, alla sequenza indicata ciò che permette la realizzazione del meccanismo di borrowing. Nel caso delle sequenze letterali avevamo una string slice ma adesso scopriamo che l'applicazione è molto più ampia. Questo è un altro dei punti di forza di questo costrutto.
Prima di addentrarci in esempi applicativi vediamo come lavorare con i nsotri slice.

Iiniziamo con una paio di metodi di base molto uitili, specialmente il primo::

len - che indica il numero di elementi dello slice
is_empty - che riporta se lo slice è vuoto oppure no

let ar01 = [1, 2, 3, 4, 5];
let sl01 = &ar01[1..4];
println!("Elementi in slice: {}", sl01.len());
println!("Slice vuoto? {}", sl01.is_empty());
Il risultato è:

Elementi in slice: 3
Slice vuoto? false

Il risultato della prima istruzione è 3 in quanto il range non è inclusivo (per un range inclusivo dovremmo scrivere
1..=4
ma ne parleremo nel paragrafo dedicato ai range) e quindi lo slice prenderà gli elementi con indice 1,2,3 costruendo la sequenza costituita dai numeri
2 3 4 che avranno indice
0 1 2.

Per accedere ad un singolo elemento si può usare l'operatore []
Riprendendo l'esempio precedente:

let ar01 = [1, 2, 3, 4, 5];
let sl01 = &ar01[1..4];
println!("{}", sl01[2]);

ci restituirà il numero 4.

Per iterare attraverso gli elementi di uno slice abbiamo iter

  Esempio 14.2
1
2
3
4
5
6
7
fn main() {
    let sl01 = &[1, 2, 3, 4, 5]; // Definiamo uno slice
    // Iteriamo attraverso gli elementi dello slice
    for element in sl01.iter() {
        println!("Elemento: {}", element);
    }
}

Usando iter non possiamo modificare nulla dello slice, è un'operazione di sola lettura.

Un altro metodo utile è contains(&T)  che permette di verificare l'esistenza di un dato elemento all'interno dello slice dando come risultato true o false a seconda l'elemento cercato sia oppure no nello slice:

let sl01 = &[1, 2, 3, 4, 5]; // Definiamo uno slice
if sl01.contains(&3) {
     println!("Lo slice contiene l'elemento {}!", 3);
    } else {
     println!("L'elemento {} non è presente nello slice.", 3);
    }

Invece sort e reverse possono aiutare ad ordinare come preferite i vostri dati:

  Esempio 14.3
1
2
3
4
5
6
fn main() {
    let sl01 = &mut[7,5,9,1,0];
    sl01.sort();
    sl01.reverse();
    println!("{:?}", sl01);
}

e abbiamo:

[9, 7, 5, 1, 0]

Scopriamo ora come lavorare per cambiare gli elementi di uno slice:

  Esempio 14.4
1
2
3
4
5
fn main() {
    let sl01 = &mut[7,5,9,1,0];
    sl01[0] = 77;
    println!("{:?}", sl01);
}

che ci dà:

[77, 5, 9, 1, 0]

fin qui tutto normale, abbiamo dichiarato uno slice mutabile e lo abbiamo di fatto mutato.

let ar01 = [7,5,9,1,0];    
let sl01 = &ar01;    
println!("{:?}", sl01);

Questo primo step costruisce uno slice che punta ad un array. Ovviamente niente può essere cambiato.

let ar01 = [7,5,9,1,0];    
let sl01 = &mut ar01;    
println!("{:?}", sl01);    
sl01[0] = 77;

Questo step non funziona. Il motivo è che dichiariamo di poter cambiare un elemento di qualcosa che, nella prima riga non è dicharato mutabile. Il compilatore ce lo dice senza mezzi termini:

error[E0596]: cannot borrow `ar01` as mutable, as it is not declared as mutable
--> r199.rs:3:16
|
3 | let sl01 = &mut ar01;
| ^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut ar01 = [7,5,9,1,0];
| +++

quindi:

let mut ar01 = [7,5,9,1,0];    
let sl01 = &mut ar01;    
sl01[0] = 77;    
println!("{:?}", sl01);

ed otteniamo lo stesso risultato dell'esempio precedente.
Come per gli array avremo un panic in caso di indice fuori range. La soluzione è la stessa già vista per quelle sequenze, ovvero get_mut(indice) che restituirà un Option. Vi lascio il semplice adattamento dell'esempio., riprendendo quello del capitolo precedente.