Rust - I range


I range sono centrali in Rust: rappresentano intervalli (Range, RangeInclusive, RangeFrom, ecc.), possono essere limitati o illimitati, e sono legati a iteratori, slice, for e pattern matching. Sono normalmente costruiti su interi o char. Per usarli con tipi personalizzati, il trait PartialOrd serve per poter usare metodi come contains, mentre per iterarli serve il trait Step, attualmente (vers. 1.99) disponibile solo su nightly.

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

La forma più comune in cui incontreremo i nostri range è la seguente:

inizio..fine

che va dall'elemento inizio all'elemento fine-1, quindi siamo davanti ad un range cosiddetto esclusivo, ad esempio

0..5 è in realtà costituito dai numeri:

0 1 2 3 4

se vogliamo avere un range inclusivo la sintassi diventa:

inizio..= fine

e quindi:

0..5  -> 0 1 2 3 4
0..=5 -> 0 1 2 3 4 5

Questo non è diverso da altri linguaggi. Rust però ci mette a disposizione vari tipi di Range e li riassumiamo nella seguente tabella che troverete anche in giro sulla rete più o meno identica:

  Sintassi Tipo Significato
1 a..b Range da a incluso a b escluso
2 a..=b RangeInclusive da a incluso a b incluso
3 a.. RangeFrom da a in poi
4 ..b RangeTo dall'inizio fino a b escluso
5 ..=b RangeToInclusive dall'inizio fino a b incluso
6 .. RangeFull intervallo completo

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

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

Vediamo un piccolo esempio riassuntivo di quanto visto finora nel quale ritorniamo anche sulla relazione tra while e i range:

Esempio 24.1

fn main()
{
    let mut r1 = 0..5;
    println!("{:?}", r1);
    println!("{}", r1.start);
    println!("{}", r1.end);
    r1.start = 2;
    println!("{:?}", r1);
    let mut x = r1.start;
    while x < r1.end {
        println!("ciao");
        x = x + 1;
    }
}

ecco l'output:

0..5
0
5
2..5
ciao
ciao
ciao

Questo conferma quanto finora illustrato e torna su quella considerazione fatta quando abbiamo presentato l'istruzione while legando direttamente stavolta i margini del range all'esecuzione del ciclo. Un trucco da nulla ovviamente. Più nello spirito di Rust forse è la seguente soluzione che usa contains(&elemento):

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' fino a 'e' essendo esclusivo. Tra l'altro in questo spezzone di codice vediamo il modo diciamo "standard" di attraversare un range ovvero tramite il ciclo for.

E per i float? Non funziona in quanto step anche nelle nightly non è supportato dai numeri con virgola. Se volete un'idea di come poter fare per iterare su di essi, tra le tante possibili, vi lascio il seguente codice:

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:

Abbiamo presentato una tabella iniziale che ci presentava tutti i tipi di range possibili. Il comportamento però non è uguale per tutti, la cosa è anche intuibile stante la diversa natura egli intervalli possibili. Non è difficile capire che per i range di tipo dal 3 al 6 non ha senso parlare di len() o count() non avendo uno start o un end definiti. L'attraversamento può essere fatto comunque tramite for su RangeFrom (numero 3 nella tabella) pur con le dovute accortezze. In effetti:

for i in 1.. {
  println!("{i}");
}

si può fare ma vedrete solo una gran scorere di numeri a video. Meglio mettere un break da qualche parte. 
Per gli altri 3, i numeri 4,5 e 6 della tabella invece non si può usare for.

I range non sono indicizzati e quindi per conoscere l'emento ad una certa posizione o per conoscere la poszione di un elemento bosogna percorrere strane diverse rispetto ad un array o un vettore. Ci sono vari modi, ne suggerisco due:

Altrimenti si può ricorrere alla conversione verso vettore (convertire verso array è difficile perchè la dinensione deve essere nota a compile time cosa che non è per i range. Si ricorre, come abbiamo già detto, a collect(). Rivediamo un brevissimo esempio:

let v: Vec = (10..20).collect();

e poi si lavora con quello che i vettori ci mettono a disposizione.

A titolo di cultura, quando scriviamo

let r1 = 1..8;

stiamo usando una sorta di "zucchero sintattico" con annessa comprensione da parte del compilatore invece di istanziare la struct che, come detto all'inizio, è la reale rappresentazione di un range. Si veda il seguente codice:

use std::ops::Range;
fn main() {
  let r1 = Range { start: 1, end: 8 };
  println!("{}", r1.len())
}

dove è necessario specifica la clausola use perchè il compilatore deve sapere dove andare a prendere la struct, con la sintassi "classica" costruisce da solo l'elemento. Tutti i tipi di range si possono costuire allo stesso modo:

RangeFrom { start }
RangeTo { end }
RangeToInclusive { end }
RangeFull
quest'ultima è chiaramente una unit struct senza campi.

RangeInclusive è un'eccezione: 1..=8 non si può costruire con la sintassi a campi, si costruisce con:
RangeInclusive::new(1, 8)