Numeri in Rust
Rust offre un sistema numerico ricco e preciso, con molteplici tipi interi e in virgola mobile progettati per coprire ogni esigenza. A differenza di molti linguaggi, Rust non effettua conversioni implicite tra tipi numerici, garantendo massima sicurezza e prevedibilità . In questa guida esamineremo in dettaglio tutti i tipi numerici disponibili.
Tipi Interi con Segno
I tipi interi con segno possono rappresentare numeri positivi e negativi. Usano la rappresentazione in complemento a due:
| Tipo | Dimensione | Range |
|---|---|---|
i8 |
8 bit | da -128 a 127 |
i16 |
16 bit | da -32.768 a 32.767 |
i32 |
32 bit | da -2.147.483.648 a 2.147.483.647 |
i64 |
64 bit | da -9.223.372.036.854.775.808 a 9.223.372.036.854.775.807 |
i128 |
128 bit | da -2^127 a 2^127 - 1 |
isize |
arch | Dipende dall’architettura (32 o 64 bit) |
fn main() {
let piccolo: i8 = -100;
let medio: i16 = 30_000;
let standard: i32 = 2_000_000;
let grande: i64 = 9_000_000_000;
let enorme: i128 = 170_141_183_460_469_231_731_687_303_715_884;
let indice: isize = -1; // Dipende dalla piattaforma
println!("i8: {}, i16: {}, i32: {}", piccolo, medio, standard);
println!("i64: {}, i128: {}, isize: {}", grande, enorme, indice);
}
Tipi Interi senza Segno
I tipi interi senza segno rappresentano solo valori positivi (incluso zero):
| Tipo | Dimensione | Range |
|---|---|---|
u8 |
8 bit | da 0 a 255 |
u16 |
16 bit | da 0 a 65.535 |
u32 |
32 bit | da 0 a 4.294.967.295 |
u64 |
64 bit | da 0 a 18.446.744.073.709.551.615 |
u128 |
128 bit | da 0 a 2^128 - 1 |
usize |
arch | Dipende dall’architettura |
fn main() {
let byte: u8 = 255;
let porta: u16 = 8080;
let contatore: u32 = 1_000_000;
let file_size: u64 = 4_294_967_296;
let lunghezza: usize = 42; // Usato per indici e lunghezze
println!("byte: {}, porta: {}", byte, porta);
println!("contatore: {}, file_size: {}", contatore, file_size);
println!("lunghezza: {}", lunghezza);
}
Il tipo usize è particolarmente importante: viene usato per indicizzare le collezioni e rappresenta la dimensione dell’architettura del processore.
Tipi in Virgola Mobile
Rust ha due tipi floating-point conformi allo standard IEEE 754:
fn main() {
let singola: f32 = 3.14; // Precisione singola (32 bit)
let doppia: f64 = 3.141592653589793; // Precisione doppia (64 bit, predefinito)
println!("f32: {}", singola);
println!("f64: {}", doppia);
// Valori speciali
let infinito = f64::INFINITY;
let neg_infinito = f64::NEG_INFINITY;
let non_numero = f64::NAN;
println!("Infinito: {}", infinito);
println!("-Infinito: {}", neg_infinito);
println!("NaN: {}", non_numero);
println!("NaN == NaN? {}", non_numero == non_numero); // false!
}
Il tipo predefinito per i letterali in virgola mobile è f64, che offre maggiore precisione con prestazioni simili a f32 sulle CPU moderne a 64 bit.
Letterali Numerici
Rust supporta diverse notazioni per i letterali numerici:
fn main() {
// Decimale
let decimale = 98_222;
// Esadecimale (prefisso 0x)
let esadecimale = 0xFF;
// Ottale (prefisso 0o)
let ottale = 0o77;
// Binario (prefisso 0b)
let binario = 0b1111_0000;
// Byte (solo u8, prefisso b)
let byte_letterale = b'A'; // Valore ASCII di 'A' = 65
println!("Decimale: {}", decimale);
println!("Esadecimale: {} (0xFF)", esadecimale);
println!("Ottale: {} (0o77)", ottale);
println!("Binario: {} (0b1111_0000)", binario);
println!("Byte: {} (b'A')", byte_letterale);
}
Underscore nei Numeri
Gli underscore _ possono essere inseriti ovunque nei letterali numerici per migliorarne la leggibilita, senza influenzare il valore:
fn main() {
let milione = 1_000_000;
let pi = 3.141_592_653;
let esadecimale = 0xFF_EC_DE;
let binario = 0b1100_1010_0011;
println!("Milione: {}", milione);
println!("Pi: {}", pi);
println!("Hex: {:#X}", esadecimale);
println!("Bin: {:#b}", binario);
}
Suffissi di Tipo
È possibile specificare il tipo di un letterale numerico tramite un suffisso:
fn main() {
let a = 42u8; // u8
let b = 1_000i64; // i64
let c = 3.14f32; // f32
let d = 100_usize; // usize
println!("a: {}, b: {}, c: {}, d: {}", a, b, c, d);
}
Integer Overflow
In Rust, l’overflow degli interi viene gestito in modo diverso a seconda della modalità di compilazione:
- Debug: L’overflow causa un panic (il programma si interrompe).
- Release: L’overflow esegue il wrapping (complemento a due).
fn main() {
let massimo: u8 = 255;
// let overflow = massimo + 1; // Panic in debug!
// Metodi espliciti per gestire l'overflow:
// Checked: restituisce Option<T>
let checked = massimo.checked_add(1);
println!("Checked: {:?}", checked); // None
// Wrapping: esegue il wrapping
let wrapped = massimo.wrapping_add(1);
println!("Wrapped: {}", wrapped); // 0
// Saturating: si ferma al valore massimo/minimo
let saturated = massimo.saturating_add(10);
println!("Saturated: {}", saturated); // 255
// Overflowing: restituisce (valore, bool_overflow)
let (valore, overflow) = massimo.overflowing_add(1);
println!("Overflowing: {} (overflow: {})", valore, overflow); // 0, true
}
Operazioni Numeriche
Rust supporta tutte le operazioni aritmetiche standard:
fn main() {
// Addizione
let somma = 5 + 10;
// Sottrazione
let differenza = 95.5 - 4.3;
// Moltiplicazione
let prodotto = 4 * 30;
// Divisione
let quoziente = 56.7 / 32.2;
let divisione_intera = 5 / 3; // Risultato: 1 (troncato)
// Resto (modulo)
let resto = 43 % 5;
println!("Somma: {}", somma);
println!("Differenza: {}", differenza);
println!("Prodotto: {}", prodotto);
println!("Quoziente: {:.4}", quoziente);
println!("Divisione intera: {}", divisione_intera);
println!("Resto: {}", resto);
}
Conversione con as
Rust non esegue conversioni implicite tra tipi numerici. Per convertire, si usa la parola chiave as:
fn main() {
let intero: i32 = 42;
let decimale: f64 = intero as f64;
println!("i32 -> f64: {}", decimale);
let grande: f64 = 3.99;
let troncato: i32 = grande as i32; // Tronca la parte decimale
println!("f64 -> i32: {}", troncato); // 3 (non arrotonda)
let valore: i32 = 300;
let piccolo: u8 = valore as u8; // Overflow: 300 % 256 = 44
println!("i32(300) -> u8: {}", piccolo); // 44
let negativo: i32 = -1;
let senza_segno: u32 = negativo as u32;
println!("i32(-1) -> u32: {}", senza_segno); // 4294967295
}
Per conversioni sicure, è preferibile usare i metodi try_from e try_into:
fn main() {
let grande: i64 = 1_000_000;
match i32::try_from(grande) {
Ok(valore) => println!("Conversione riuscita: {}", valore),
Err(e) => println!("Errore di conversione: {}", e),
}
let troppo_grande: i64 = 10_000_000_000;
match i32::try_from(troppo_grande) {
Ok(valore) => println!("Conversione riuscita: {}", valore),
Err(e) => println!("Errore di conversione: {}", e),
}
}
Conclusione
Rust offre un sistema numerico completo e sicuro, con tipi che coprono ogni esigenza di precisione e dimensione. L’assenza di conversioni implicite e la gestione esplicita dell’overflow rendono il codice numerico in Rust prevedibile e privo di bug sottili. La scelta del tipo numerico appropriato e l’uso consapevole delle conversioni sono competenze fondamentali per ogni programmatore Rust.