|
In Rust abbiamo due tipi di numeri con virgola che differiscono per la precisione a cui possono arrivare:
let f1 = 2.0; let f2: f32 = 3.1; let f3 = 2f64; let f4 = 2.0_f32; quindi lasciando all'inferenza (che di default sceglie giustamente f64) oppure forzando noi il tipo o anche ponendo dei suffissi nelle due forme note. Per la magnitudine dei due tipi abbiamo anche in questo caso MIN e MAX. Nel seguente programma vedremo la differenza di precisione tra i due tipi ed eseguendolo potremo apprezzare la enorme capacità del tipo f64.
che presenta il seguente output per quanto riguarda la divisione (i limiti degli f32 e degli f64 li lascio a voi che diversamente gli ultimi mi sfondano la tabella).
per le altre operazioni di base non ci sono differenze sostanziali ovviamente rispetto a quanto visto nel capitolo precedente. Per quanto riguarda l'occupazione in memoria dei due tipi vediamo il seguente semplice programma che vi potrà essere utile anche in altri casi: use std::mem; fn main() { let size_of_f64 = mem::size_of::<f64>(); let size_of_f32 = mem::size_of::<f32>(); println!("Dimensione di f64: {} byte", size_of_f64); println!("Dimensione di f32: {} byte", size_of_f32); } da cui risulterà che f64 occupa 8 byte ed f32 solo 4. Un'altra rappresentazione abbastanza nota è quella esponenziale. fn main() { // Numeri molto grandi let large_number = 2.5e6; // equivalente a 2.5 * 10^6 println!("2.5e6 = {}", large_number); // Numeri molto piccoli let small_number = 3.2e-6; // equivalente a 3.2 * 10^-6 println!("3.2e-6 = {}", small_number); // Notazione esponenziale in Rust let number = 1e3; // equivalente a 1 * 10^3 println!("1e3 = {}", number); let negative_power = 4.12E-9; // equivalente a 4.12 * 10^-9 println!("4.12E-9 = {}", negative_power); } Veniamo adesso ad un capitolo interessante, ovvero la convivenza tra numeri interi e numeri con virgola, delle cui peculiarità parleremo comunque più avanti in questo paragrafo. Le (piccole) difficoltà di questa convivenza sono prevedibili sulla base di quanto visto nel precedente paragrafo ma d'altronde la "safety" pretesa da Rust vuole anche qualche piccolo sacrificio, lo ribadiamo. Infatti, il seguente codice: let x = 7.0; let y = 3; let z = x + y; dà origine ad un errore: no implementation for `{float} + {integer}` e lo stesso vale se provate con altre operazioni, ovvero non possiamo lavorare con due tipi diversi. Quindi, armiamoci di pazienza e prepariamoci ad adottare qualche escamotage per risolvere la situazione. Una prima strada è quella di ricorrere all'operatore as, che già conosciamo: let x = 7.0; let y = 3; let z = x + y as f64; questo funziona e, in particolare, risulta utile in caso di divisioni quando volete trovare (eureka!) il risultato corretto. Si può anche fare il contrario, convertendo un float in intero, perdendo la parte decimale ovviamente: let x = 7.0123; let y = 5; let z = x as i32 + y; in dettaglio:
Come potrete facilmente intuire non è possibile mischiare gli f32 con gli f64. Quindi: let x = 65f64; let y = 2_f32; println!("{}", x/y); Questo codice origina un errore nell'ambito del quale compare questa segnalazione: ^ no implementation for `f64 / f32` che è abbastanza chiaro e ribadisce quanto detto prima. Come detto possiamo ricorrere ad as ma con una possibile perdita di precisione se scegliamo di passare da f64 a 32 mentre passare fa f32 a f64 ha pure le sue peculiarità. Vedremo un paragrafo apposta devoluto alle conversioni numeriche. Apro una breve parentesi, che riguarda anche la convivenza tra numeri interi e con virgola riguardante gli elevamenti a potenza, un tipo di computazione che può essere molto utile e comune. Riassumendo: 1) base intera, esponente intero visto nel capitolo precedente, la funzione ad hoc è pow: let x = 7; let y = 3; let z = i32::pow(x, y); 2) base con virgola esponente intero: si usa powi let x = 7.17; let y = 3; let z = f64::powi(x, y); // si può usare anche f32::powi che ha un range meno ampio di valori ammissibili 3) base con virgola, esponente con virgola: si usa powf let x = 7.17; let y = 3.29; let z = f32::powf(x, y); 4) base intera esponente con virgola: si usa ancora powf con as per convertire il numero intero: let x = 7; let y = 3.29; let z = f32::powf(x as f32, y); Come gli interi, anche i numeri con virgola possono essere manipolati tramite numerose funzioni che trovate sul sito ufficiale. In questo paragrafo vedremo qualche esempio per impostarne l'utilizzo. Ne approfittiamo anche per vedere qualche aspetto interessante dell'inferenza e di come essa lavora. fn main() { let x = 2.34; println!("{}", x.trunc()); } questo codice non compila e il messaggio può essere abbastanza sorprendente: error[E0689]: can't call method `trunc` on ambiguous numeric type `{float}` --> r0098.rs:4:22 | 4 | println!("{}", x.trunc()); | ^^^^^ | help: you must specify a type for this binding, like `f32` | 3 | let x: f32 = 2.34; | ~~~~~~ Certo può sembrare e forse è, curioso che l'inferenza sia in grado di attribuire il tipo i32 alla variabile x e poi il metodo trunc (che come dice il suo nome tronca alla sola parte intera il numero con virgola) mi dice che il tipo è ambiguo. La spiegazione non è semplice, tanto che questo fatto, comune a tutte le funzioni, è oggetto di discussioni sui vari forum da parte di utenti un po' confusi (mi ci metto anche io) specie se abituati con altri linguaggi. Detto in breve, se è vero che il compilatore inferisce il tipo in fase di compilazione a garanzia della safety sui dati, tale meccanismo non interviene quando viene chiamato un metodo che si può in realtà applicare su più tipi. Per il metodo insomma la variabile è sconosciuta pertanto deve essere specificata. L'errore si presenta anche in altri casi, vediamo ad esempio per gli interi:
println!("{}", x.abs());
Qui è il metodo abs che va in crisi in quanto non sa esattamente con quale tipo di variabile numerica abbia a che fare. In pratica il compilatore garantisce l'integrità dei dati in termini di tipologia ad essa assegnata così che l'uso che ne viene fatto sia coerente ma le singole funzioni necessitano diciamo di chiarimenti qualora possano lavorare su più tipi. Non ho approfondito il perchè di questa scelta ma esiste un capitolo molto interessante sul sito ufficiale legato a come lavora l'inferenza "under the hood". Una possibile soluzione per il codice precedente è riscrivere la riga incriminata come segue:
println!("{}", (x as f64).trunc());
Ad ogni modo trunc è solo delle tantissime funzioni che avete a disposizione. Nel seguente esempio vediamo alcune funzioni, anche qui, si tratta di un campione non esaustivo ma sul sito ufficiale le trovate tutte.
Le righe 4, 6, 8, 10 ci fanno capire come funziona l'arrotondamento (round), che tende al numero più basso fino al valore.49999 e poi attonda verso l'alto. riga 11 - floor porta all'intero basso più vicino riga 12 - ceil porta all'intero alto più vicino riga 13 - trunc, tronca, come detto riga 14 - fract espone la parte frazionale del numero riga 15 - sqrt, la classica radice quadrata. Questa agisce sui numeri con virgola, non lavora sugli interi. Il programma successivo invece mostra l'uso di alcune funzioni built-in, qualcuna l'abbiamo già vista
Infine passiamo in rassegna un gruppo di interessanti costanti legate ai numeri con virgola: DIGITS Numero delle cifre significative in base 10. EPSILON Valore di confronto minimo - Vedremo nel prossimo capitolo. INFINITY Infinito (∞). MANTISSA_DIGITS Numero di cifre significative in base 2. MAX Valore float32 (o float64) massimo. MIN Valore float32 (o float64) minimo MIN_POSITIVE Più piccolo valore positivo. NAN Not a Number (NaN). NEG_INFINITY Infinito negativo (−∞). println!("{}", f32::DIGITS); println!("{}", f64::DIGITS); println!("{}", f32::EPSILON); mentre sono presenti alcune altre costanti che rappresentano valori di un certo uso comune: E Numero di Eulero e FRAC_1_PI 1/π FRAC_1_SQRT_2 1/sqrt(2) FRAC_2_PI 2/π FRAC_2_SQRT_PI 2/sqrt(π) FRAC_PI_2 π/2 FRAC_PI_3 π/3 FRAC_PI_4 π/4 FRAC_PI_6 π/6 FRAC_PI_8 π/8 LN_2 ln(2) LN_10 ln(10) LOG2_10 log2(10) LOG2_E log2(e) LOG10_2 log10(2) LOG10_E log10(e) PI il famoso pigreco SQRT_2 sqrt(2) TAU costante equivalente a 2 volte pigreco (τ) per richiamare queste ultime occorre appunto riferirsi al modulo consts: println!("{}", std::f32::consts::PI); Si tratta evidentemente di una raccolta molto utile in tante situazioni. Argomento interessante è quello del confronto tra le entità numeriche. Se per gli interi non ci sono particolari problemi la cosa cambia un po' quando si tratta di manipolare i numeri con virgola. Per riassumere un po' le cose ho dedicato ai confronti il paragrafo successivo a questo dove si parla anche delle conversioni. Per il momento ricordate: as è utile e comodo ma anche pericoloso. |