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:- 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() 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. A questo proposito, len() e count() danno origine allo stesso risultato ma con una diffierenza sostanziale: len() prende &self e non consuma nulla, count() prende self per valore e consuma l'iteratore:
fn main() {
let r = 0..10;
println!("{}", r.len()); // 10
println!("{}", r.len()); // utilizzabile
let r1 = 0..10;
println!("{}", r1.count()); // 10, ma ora mosso
println!("{}", r1.count()); // errore
}
Inoltre len() non è garantito nemmeno per Range e RangeInclusive: ExactSizeIterator (che è un trait della libreria standard che indica che un iteratore conosce con esattezza quanti elementi gli restano, senza bisogno di scorrerli) è implementato solo per alcuni interi (u8, u16, u32, usize, i8, i16, i32, isize) e solo per Range, non per RangeInclusive su tipi più larghi o per u64/i64. Quindi
(0..10u64).len() non compila, mentre
(0..10u64).count() sì.
Laddove sia disponibile è comunque preferibile len() operativamente più leggero - contains(&elemento) che abbiamo
già visto in precedenza ci dice se il range contiene un dato valore, che
deve essere passato per riferimento
println!("{}", r1.contains(&6));
restituisce false perchè il valore 6 non è compreso.
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:
- trovare la posizione di un elemento:
let pos = (10..20).position(|x| x == 15);
println!("{pos:?}"); // Some(5) - data una posizione trovare l'elemento in quella posizione:
let elem = (10..20).nth(5); // Some(15)
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)