Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 24
I range

Come abbiamo accennato nel capitolo precedente, Rust, come altri linguaggi, prevede i range. Questi sono particolari sequenze delimitate di elementi e normalmente sono create sulla base di interi o caratteri. E' possibile anche creare range su tipi personalizzati purchè questi implementino obbligatoriamente il trait PartialOrd per permettere il confronto di elementi e preferibilmente, ma non obbligatoriamente, il trait sized per questioni di ottimizzazione e performance. In questo paragrafo ci occuperemo comunque solo dei tipi standard.
La definizione formale interna ci dice già tutto dei range:

pub struct Range<Idx> {
  pub start: Idx,
  pub end: Idx,
}
 
in pratica siamo di fronte ad una struct in cui è presente:
- un parametro generico che ci indica il tipo di inizio e fine dei delimitatori ed è quel <Idx>
- un valore di partenza - start
- un valore finale - end

Il formato (almeno quello più consueto ma come vedremo a fine paragrafo ve ne sono anche un po' diversi), come abbiamo visto nel precedente paragrafo può presentarsi in due modalità che ripetiamo pari pari qui:

start..end

che equivale, supponendo di ispezionare tale range con la variabile x a:

start <= x < end

quindi escluso il valore più alto e si parla di range esclusivo. Se vogliamo che anche questo sia incluso (quindi qui abbiamo il range inclusivo) la sintassi diventa:

start ..= end

quindi

0..5  comprende tutti gli elementi da 0 a 4
0..=5 comprende tutti gli elementi da 0 a 5.

I campi start ed end possono essere modificati se il range è dichiarato mutabile:

let mut r1 = 0..5;    
r1.start = 2;

I range, come abbiamo visto, sono estremamente utili nelle operazioni di iterazione, potete rivedere gli esempi che abbiamo dato nei passati paragrafi. Abbiamo tuttavia evidenziato nel paragrafo dedicato all'iterazione while non è possibile usare i range direttamente con esso. In pratica questo codice:

let mut x = 0;    
while x in (0..=5) {

non compila, in fondo while è legato ad una valutazione booleana stretta. Possiamo aggirare il problema come segue, nel caso vi fosse necessario usare per forza un range con un metodo molto scolastico:

  Esempio 24.1
1
2
3
4
5
6
7
8
9
10
11
fn main()
{
    let r1 = 0..5;
    println!("{}", r1.start);
    println!("{}", r1.end);
    let mut x = r1.start;
    while x < r1.end {
        println!("ciao");
        x = x + 1;
    }
}

Abbiamo fatto uso dei campi start e end propri della definizione di un range per delimitare l'ambito di applicazione del while. Un'alternativa forse più "rusticeana" è la seguente:

let mut x = 0;
while (0..=5).contains(&x) {
println!("x nel range {}", x);
x += 1;
}


Come detto i range possono lavorare anche su altre tipologie di dati, come i caratteri:


for x in 'a'..'f' {        
print!("{}", x)

questo codice espone a video i caratteri da 'a' a 'e'

Un caso diverso è se si ha a che fare con i float. ovvero è lecito dichiarare un range come segue

let r1 = 1.1..=1.9;

ma i float non implementano il trait step necessario per le iterazioni. Quindi lo create ma, sostanzialmente non ve ne fate nulla. Se volete stampare una iterazione sui float dovrete gestire manualmente la cosa, come nel seguente esempio, che in realtà con c'entra con i range ma che presento a titolo didattico per fornire una soluzione al problema:

fn main() {
    let start = 0.0;          // valore iniziale
    let end   = 1.0;          // valore finale
    let step  = 0.1;          // passo di incremento
    let mut current = start;
    while current < end {
        println!("{:.1}", current);
        current += step;
    }
}

I range hanno solo due campi come detto, start e end ma dispongono anche di alcune proprietà utili per la loro manipolazione:

  • is_empty() ci dice se un range è vuoto
    println!("{}", (0..5).is_empty());  // false
    la parentesi che racchiude il range è necessaria altrimenti is_empty si legherebbe al numero 5 il che non va bene.
  • clone() abbiamo già visto effettua una copia del range, copia indipendente dall'originale
    let mut r1 = 0..5;
    let r2 = r1.clone();
    r1.start = 2;
    println!("{}", r2.start);
    alla fine il punto di inizio di r2 è ancora 0
  • len() restituisce il numero di elementi che compongono il range:
    println!("{}", (0..5).len);
    da notare però che len() funziona con i range numerici, es provate su un range tipo 'a'..'z' il compilatore vi restituisce un errore (stavolta meno immediato di altri casi). Al posto di len() si può usare count() che funziona sempre.
  • contains(&elemento) che abbiamo già visto in precedenza ci dice se il range contiene un dato valore, che deve essere passato per riferimento (per maggiore flassibilità)
    println!("{}", r1.contains(&6));
    restituisce false perchè il valore 6 non è compreso. Da notare che si lavora per riferimento.

Oltre al range standard, oggetto finora di questo capitolo, che abbiamo visto nelle due forme esclusivo ed inclusivo, ne esistono altri formati che ci permettono ulteriori espressività. Possiamo quindi presentare la seguente tabella che riassume quello che abbiamo a disposizione in Rust:

  • Range(start..end): Definisce un range che include il valore iniziale start e esclude il valore finale end.
  • RangeFrom(start..): Crea un range che parte da start e continua all'infinito (o fino al massimo possibile per il tipo di dati).
  • RangeTo(..end): Crea un range che inizia da un limite inferiore non specificato (solitamente zero o un'altro valore predefinito dipendente dal tipo) fino a end, escludendo end.
  • RangeInclusive(start..=end): Simile a Range, ma include anche il valore end.
  • RangeToInclusive(..=end): Crea un range da un limite inferiore non specificato fino a end, includendo end.

Bisogna fare attenzione che la manipolabilità non è uguale per tutti. Ad esempio i range del secondo e del terzo o dell'ultimo punto non implementano il trait Iterator, per cui non sono attraversabili con un semplice for. Una soluzione è ricorrere a take come nell'esempio seguente:

  Esempio 24.2
1
2
3
4
5
6
7
8
9
fn main() {
    for i in (0..).take(5) {
        println!("{}", i); // Stampa: 0, 1, 2, 3, 4
    }
    println!();
    for i in (5..).take(5) {
        println!("{}", i); // Stampa: 5, 6, 7, 8, 9
    }
}

I range non sono indicizzati, se è necessario ricavare un elemento ad un certa posizione dobbiamo convertirlo in un tipo indicizzato oppure usare un iteratore, vedremo nel capitolo dedicato a questo argomento l'uso di nth(). Per quanto riguarda la conversione verso un tipo indicizzato un metodo semplice è passare da range a vettore tramite il metodo collect e abbiamo visto un esempio proprio nel capitolo dedicato ai vettori. A quel punto potremo usare tutti i metodi disponibili per i vettori. Più difficile lavorare con un array in quanto il numero di elementi del range deve essere noto a compile time e non sempre è vero.