Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 5
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:


Tipo  Lunghezza in bit   con segno  range
i8 8 bit si -128 + 127
u8 8 bit no 0  +255
i16 16 bit si -32768 +32767
u16 16 bit no 0 +65535
i32 (default) 32 bit si −2147483648  +2,147,483,647
u32 32 bit no 0  +4294967295
i64 64 bit si −9223372036854775808 +9223372036854775807 
u64 64 bit no 0 +18446744073709551615
i128 128 bit si -(2^127)  (2^127)-1
u128 128 bit no 0  2^128

Citiamo poi isize e usize rispettivamente con segno e senza segno che sono machine dipendent (quindi 32 o 64 bit) e che viene utilizzato in particolare quando si deve indicizzare sequenze, tipo gli array o contenere puntatori o offset in memoria.
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 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 numercihe 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 5.1
1
2
3
4
5
6
7
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 con 3 dà un (a mio avviso deludente) 2... abbiamo passato da un po' il 2020 e per avere il risultato esatto c'è ancora del lavoro da fare... boh. Alcuni linguaggi danno il risultato esatto e se proprio vuoi la parte intera te la puoi tirare fuori senza problemi. Si scherza, naturalmente, però è una cosa che un po' mi dà fastidio, anche se capisco che si tratta di un problema di poco conto dovuto ad una diversa impostazione del trattamento dei numeri. D'altronde per molti linguaggi è così ma, ad esempio, per quello più in voga al momento in cui scrivo, Python, 7 / 3 dà 2.333333 e se proprio voglio 2 basta scrivere 7 // 3. Ok sono linguaggi diversi, lo so, però questa impostazione la preferisco.
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 lettere minuscola, quindi ad esempio 0B non è accettato dal compilatore. Vediamo un esempio che presenta la notazione diretta ed inversa:

  Esempio 5.2
1
2
3
4
5
6
7
8
9
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 autoesplicativo:

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 per 4, 6 a seconda dello shift specificato).
  • ! - 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.
Questi operatori lavorano a livello di bit, vediamo un esempio:

  Esempio 5.3
1
2
3
4
5
6
7
8
fn main()
{
  let x01 = 3;
  let x02 = 5;
  println!("{}", x01 | x02);
  println!("{}", x02 << 2);
  println!("{:b}", !x01);
}

con il seguente output:

7
20
11111111111111111111111111111100

Dove si nota appunto la moltiplicazione per 4 dovuta al doppio shift a sinistra invocata alla riga 6 e si può apprezzare la trasformazione del 3, che in binario definito come i 32 è: 00000000000000000000000000000011 in un numero ben diverso. Potete sbizzarrirvi nel testare questi operatori, magari forzando l'output in firmato binario, come nell'esempio 2, 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 5.4
1
2
3
4
5
6
7
8
9
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 alla riga 6 la possibilità di effettuare l'elevamento a potenza, per gli interi, alla 5 l'estrazione del valore assoluto. La riga 8 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ù avanzato 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 5.5
1
2
3
4
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.