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
Questi operatori lavorano a livello di bit, vediamo un esempio:
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.