Rust - Gli interi
Non occorre certo che sia io a sottolineare l'importanza di questo
argomento. La gestione dei numeri, siano essi interi o con virgola è uno
dei pilastri della programmazione. Banalissimo dirlo, lo so. Dando ciò per
scontato, partiamo subito con i concetti di base ma tenete presente che
anche in Rust come in tutti i linguaggi anche un argomento banale in
apparenza come questo, nasconde qualche insidia.
Rust ci offre i
seguenti tipi nei quali sono racchiusi gli interi: creiamo quindi
due tabella una per gli interi con segno e l'altra
per quelli senza segno
| Tipo | Bit | Intervallo | Descrizione |
|---|---|---|---|
i8 |
8 | da −128 a 127 | Con segno 8‑bit |
i16 |
16 | da −32 768 a 32 767 | Con segno 16‑bit |
i32 |
32 | da −2 147 483 648 a 2 147 483 647 | Con segno 32‑bit (default numerico) |
i64 |
64 | da −9.22e18 a 9.22e18 | Con segno 64‑bit |
i128 |
128 | range enorme (~3.4e38) | Con segno 128‑bit |
isize |
architettura | 32 o 64 bit | Con segno pointer‑sized |
| Tipo | Bit | Intervallo | Descrizione |
|---|---|---|---|
u8 |
8 | 0–255 | Senza segno 8‑bit |
u16 |
16 | 0–65 535 | Senza segno 16‑bit |
u32 |
32 | 0–4 294 967 295 | Senza segno 32‑bit |
u64 |
64 | 0–1.84e19 | Senza segno 64‑bit |
u128 |
128 | range enorme (~1e38) | Senza segno 128‑bit |
usize |
architettura | 32 o 64 bit | Senza segno pointer‑sized (per indici, slice, allocazioni) |
Il tipo di default, è i32 mentre isize e usize dipendono dalla architettura sottostante, come indicato.
La dichiarazione delle variabili numeriche è ovviamente quella già vista, per esempio:
let x = 9;
let y: i8 = 10;
quindi con uso della inferenza o senza di essa. Si può anche ricorrere all'uso di suffissi senza o con il carattere _:
let y = 34i32;
let x = 23_i16;Per una maggior leggibilità è possibile scrivere anche i numeri aggiungendo il separatore _ ad esempio 1000000 può essere scritto 1_000_000 o se volete 1_00_00_00, potete decidere liberamente dove piazzare il carattere underscore. Potete anche inserire lunghe sequenze di underscore, 2____0___0 equivarrà a 200 per il compilatore.
Una prima ovvia norma comportamentale viene dalla considerazione che quando si specifica un tipo, il valore di inizializzazione deve essere compreso nel suo range. Ad esempio se scriviamo:
let y: i8 = 1000;il compilatore ci avvisa a modo suo :
error: literal out of range for `i8`
Diversamente, il tipo di default, se non specificato diversamente e il valore resta entro il limite previsto, è i32. Questo dà origine, in conseguenza di quanto detto prima, ad un altro problema:
let y = 1000000000000;non va bene:
error: literal out of range for `i32`
--> r051.rs:2:10
|
2 | let y = 1000000000000;
| ^^^^^^^^^^^^^
|
= note: the literal `1000000000000` does not fit into the type `i32` whose range is `-2147483648..=2147483647`
= help: consider using the type `i64` instead
= note: `#[deny(overflowing_literals)]` on by default
Questo messaggio ci indica che il valore di default non è adatto per il valore di inizializzazione in quanto questo supera il limite degli i32 e ci suggerisce di forzare l'uso di un i64. Infatti:
let y:
i64 = 1000000000000;compila. Rust non permette conversioni numeriche implicite per cui non può esservi una promozione silente da un tipo di numero ad un altro più capiente. Quello degli overflow numerici è un problema che affligge tutti i linguaggi di programmazione e ha procurato anche disastri nel passato, vedi il caso Ariane 5 del 1996, per dirne uno dei tanti. Ci torneremo sopra in questo stesso paragrafo, per ora rilassiamoci con cose un po' più semplici. Ad esempio, vediamo le operazioni base:
Esempio 6.1
fn main() {
println!("{}", 7 + 3); // addizione
println!("{}", 7 - 3); // sottrazione
println!("{}", 7 * 3); // moltiplicazione
println!("{}", 7 / 3); // divisione
println!("{}", 7 % 3); // resto della divisione
}
Il tutto è abbastanza esplicativo e non c'è bisogno di ulteriori
spiegazioni. La divisione di 7 per 3 dà come risultato 2 (intero)
conservando la tipologia dei numeri come nella maggior parte dei linguaggi.
Per avere il risultato esatto (2.3333...) dovremo attendere di studiare i
numeri con virgola, argomento del prossimo paragrafo.
Rust permette
anche la notazione esadecimale, ottale e binaria, notazione basata
su prefissi:
0x per la notazione esadecimale
0o per la notazione ottale
0b per la
notazione binaria.
tali prefissi vogliono la notazione con la
lettera minuscola, quindi ad esempio 0B non è accettato dal
compilatore. Vediamo un esempio che presenta la notazione diretta ed
inversa:
Esempio 6.2
fn main()
{
let x01 = 0b11;
let x02 = 0o25;
let x03 = 0x15b;
println!("{} {} {}", x01, x02, x03);
let x04 = 30;
println!("{:b} {:o} {:x}", x04, x04, x04);
}
con il suo output abbastanza esplicativo:
| 3 21 347 11110 36 1e |
Oltre alle operazioni standard sugli interi possiamo applicare le operazioni bitwise, attraverso gli operatori
- | - or - combina i bit di due numeri in modo che il bit nel risultato sia 1 se almeno uno dei bit corrispondenti nei due numeri di input è 1
- & - and - combina i bit di due numeri in modo che il bit nel risultato sia 1 solo se entrambi i bit corrispondenti nei due numeri di input sono 1.
- ^ - xor - combina i bit di due numeri in modo che il bit nel risultato sia 1 se solo uno dei bit corrispondenti nei due numeri di input è 1 (esclusivo).
- >> - shift destro - sposta tutti i bit di un numero verso destra di un numero specificato di posizioni. Per i numeri con segno (i32, i64, ecc.), Rust esegue uno "shift aritmetico", dove il bit più significativo (il bit di segno) è replicato. Per i numeri senza segno (u32, u64, ecc.), è uno "shift logico", e gli spazi vuoti vengono riempiti con zeri.
- << - shift sinistro - sposta tutti i
bit di un numero verso sinistra di un numero specificato di posizioni,
riempiendo gli spazi vuoti con zeri. Questo effettivamente moltiplica il
numero per 2 per ogni bit di shift (o meglio, in generale
x << n=x * 2ⁿ). - ! - not - inverte tutti i bit di un numero; i bit 1 diventano 0 e viceversa. Questo operatore è unario, il che significa che opera su un singolo operando, ma lavora su tutta la lunghezza del tipo che è in uso.
Esempio 6.3
fn main() {
let x01 = 3;
let x02 = 5;
println!("{}", x01 | x02);
println!("{}", x02 << 2);
println!("{:b}", !x01);
}
da cui otteniamo
|
7 20 11111111111111111111111111111100 |
Dove si nota appunto la moltiplicazione per 4 dovuta al doppio shift a sinistra invocato con l'operatore << e si può apprezzare la trasformazione del 3, che in binario definito come i32 è: 00000000000000000000000000000011 in un numero ben diverso. Potete sbizzarrirvi nel testare questi operatori, magari forzando l'output in firmato binario il che faciliterà la comprensione dei risultati.
Vi potreste chiedere a questo punto come si fa per effettuare calcoli un po' più complessi, trigonometrici, logaritmi, radici e altre funzioni. E' un argomento che affronteremo nel prossimo paragrafo essendo metodi strettamenti collegati ai numeri con virgola.
Anche gli interi hanno comunque dei metodi e proprietà strettamente ad essi collegati. Innanzitutto parliamo di due costanti, che ci permettono di ricavare i limiti per ciascun tipo, ovvero MIN e MAX (ovviamente in maiuscolo per quanto detto relativamente alla costanti:
println!("{}", i32::MAX);
println!("{}", i32::MIN);
ci restituisce:
| 2147483647 -2147483648 |
e così si può fare per i8, u16 ecc... tanto per toglierci lo sfizio vediamo questo:
println!("{}", u128::MAX);da cui risulta:
340282366920938463463374607431768211455
non male....
tra i metodi legati agli interi, anche qui di qualsiasi magnitudine e valido sia per quelli con segno che per quelli senza segno, vediamone qualcuno all'opera a titolo di esempio, rimandando alla pagina ufficiale per una panoramica completa:
Esempio 6.4
fn main() {
let x = 5;
println!("{}", i32::count_zeros(x));
println!("{}", i32::count_ones(x));
println!("{}", i32::abs(-4);
println!("{}", i32::pow(4,3));
let x: i32 = 256;
println!("{}", x.ilog(4));
}
Segnalo la funzione pow che realizza la
possibilità di effettuare l'elevamento a potenza, per gli interi, e anche
abs l'estrazione del valore assoluto.
L'utlima riga ci propone la possibilità di trovare il logaritmo di un numero
in una certa base(scelta tra parentesi), funzione che lavora solo se viene
specificato esplicitamente il tipo, cosa che avviene con moltissime altre
funzioni e vedremo nel prossimo paragrafo perchè.
Avrete senza dubbio
notato l'uso dell'operatore ::. Esso è
estremamente importante nell'ambito di questo linguaggio e può essere
definito come risolutore di percorso. Viene utilizzato per diversi scopi che
vedremo man mano durante il nostro studio ma che intanto anticipiamo qui di
seguito:
-- Accesso a Moduli e Namespace
--
Utilizzo di Enum e le loro Varianti
-- Chiamate a Funzioni Associate e
Metodi Statici
-- Accesso a Costanti e Tipi Associati
-- Richiamo dei traits
da utilizzare
CONVERSIONI
Come abbiamo detto in precedenza, Rust non permette conversioni numeriche implicite. Questo porta ad un altro problema. Se ad esempio proviamo a sommare un i32 con i16 il compilatore ci avvisa:
let x: i32 = 6;
let y: i16 = 8;
let z = x + y;
error[E0308]: mismatched types
--> r069.rs:4:15
|
4 | let z = x
+ y;
| ^ expected `i32`, found `i16`
Questo è molto chiaro, come sempre, e ci dice che non è possibile effettuare un'operazione, non solo una somma, che coinvolga tipi diversi. Questo intoppo è il prezzo da pagare (una parte...) alla type safety di Rust. Per far funzionare le cose un metodo semplice è quello di effettuare un cast, una sorta di promozione, verso il tipo a magnitudine più alta. E' possibile anche farla in senso inverso ma è ovviamente più rischioso e direi che non è consigliabile. Per effettuare il cast si usa la keyword as. Se modifichiamo la terza riga come segue:
let z = x + y as i32;
allora tutto torna a posto. Questo non è l'unico sistema, ci sono altri sistemi po' più avanzati che richiedono qualche conoscenza in più. Ci torneremo sopra ma già con as potete fare praticamente tutto quello che vi serve. Tenete presente però che l'uso di tale keyword non è privo di insidie:
Esempio 6.5
fn main() {
let x: i32 = 400;
let y = x as i8;
println!("{}", y);
}
Il risultato che si presenta ai vostri occhi sarà -112, un po' diverso da
400. Quindi attenzione, se non volete far precipitare qualche altro razzo.
Per ora è tutto ma torneremo sugli interi parlando della gestione degli
overflow e ancora delle conversioni, discorsi, come detto anche prima,
importanti e critici. Nel prossimo paragrafo invece, principalmente dedicato
ai numeri con virgola, avremo modo di vedere altri aspetti degli interi.