|
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:
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:
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:
con il suo output abbastanza autoesplicativo:
Oltre alle operazioni standard sugli interi possiamo applicare le operazioni bitwise, attraverso gli operatori
con il seguente output:
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);
2147483647 -2147483648 e così si può fare per i8, u16 ecc... tanto per toglierci lo sfizio vediamo questo:
println!("{}", u128::MAX);
340282366920938463463374607431768211455da cui risulta: 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:
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:
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 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:
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. |