Operatori Aritmetici in Rust
Gli operatori aritmetici in Rust sono utilizzati per eseguire operazioni matematiche sui valori numerici. A differenza di molti altri linguaggi, Rust non esegue conversioni implicite tra tipi numerici e offre strumenti avanzati per gestire l’overflow in modo sicuro. In questa guida esploreremo tutti gli operatori aritmetici e le loro particolarita.
Operatori Aritmetici di Base
Rust supporta cinque operatori aritmetici fondamentali:
| Operatore | Operazione | Esempio |
|---|---|---|
+ |
Addizione | 5 + 3 = 8 |
- |
Sottrazione | 10 - 4 = 6 |
* |
Moltiplicazione | 3 * 7 = 21 |
/ |
Divisione | 20 / 4 = 5 |
% |
Resto (modulo) | 17 % 5 = 2 |
fn main() {
let a = 15;
let b = 4;
println!("{} + {} = {}", a, b, a + b); // 19
println!("{} - {} = {}", a, b, a - b); // 11
println!("{} * {} = {}", a, b, a * b); // 60
println!("{} / {} = {}", a, b, a / b); // 3 (divisione intera)
println!("{} % {} = {}", a, b, a % b); // 3
}
Nessuna Conversione Implicita
Rust non converte automaticamente tra tipi numerici diversi. Tutti gli operandi devono essere dello stesso tipo:
fn main() {
let intero: i32 = 10;
let decimale: f64 = 3.5;
// let risultato = intero + decimale; // Errore! Tipi diversi
// Conversione esplicita necessaria
let risultato = intero as f64 + decimale;
println!("Risultato: {}", risultato); // 13.5
let a: i32 = 100;
let b: i64 = 200;
// let somma = a + b; // Errore! i32 e i64 sono tipi diversi
let somma = a as i64 + b;
println!("Somma: {}", somma); // 300
}
Divisione Intera e in Virgola Mobile
La divisione tra interi tronca la parte decimale (divisione intera), mentre la divisione tra float produce un risultato decimale:
fn main() {
// Divisione intera: tronca verso zero
let a: i32 = 7;
let b: i32 = 2;
println!("7 / 2 = {} (intero)", a / b); // 3
let c: i32 = -7;
let d: i32 = 2;
println!("-7 / 2 = {} (intero)", c / d); // -3 (tronca verso zero)
// Divisione float: risultato decimale
let e: f64 = 7.0;
let f: f64 = 2.0;
println!("7.0 / 2.0 = {} (float)", e / f); // 3.5
// Divisione per zero
// let errore = 10 / 0; // Panic! Divisione per zero con interi
let inf = 10.0_f64 / 0.0; // Infinity (IEEE 754)
println!("10.0 / 0.0 = {}", inf);
}
L’Operatore Modulo (%)
L’operatore resto % restituisce il resto della divisione intera. In Rust, il segno del risultato segue il segno del dividendo:
fn main() {
println!("17 % 5 = {}", 17 % 5); // 2
println!("20 % 4 = {}", 20 % 4); // 0
println!("-17 % 5 = {}", -17 % 5); // -2 (segno del dividendo)
println!("17 % -5 = {}", 17 % -5); // 2 (segno del dividendo)
// Funziona anche con i float
println!("7.5 % 2.0 = {}", 7.5_f64 % 2.0); // 1.5
// Uso pratico: verificare se un numero è pari
let numero = 42;
if numero % 2 == 0 {
println!("{} è pari", numero);
}
}
Operatore Unario di Negazione
L’operatore - unario nega un valore numerico:
fn main() {
let positivo = 42;
let negativo = -positivo;
println!("{}", negativo); // -42
let x: f64 = 3.14;
let y = -x;
println!("{}", y); // -3.14
// Non funziona con i tipi senza segno
// let u: u32 = 5;
// let neg = -u; // Errore! Cannot apply unary operator `-` to type `u32`
}
Checked Operations
Le operazioni checked restituiscono None in caso di overflow, invece di andare in panic:
fn main() {
let a: i32 = i32::MAX;
let checked_add = a.checked_add(1);
println!("MAX + 1 (checked): {:?}", checked_add); // None
let checked_mul = 100_i32.checked_mul(200);
println!("100 * 200 (checked): {:?}", checked_mul); // Some(20000)
let checked_div = 10_i32.checked_div(0);
println!("10 / 0 (checked): {:?}", checked_div); // None
// Uso pratico con match
match a.checked_add(1) {
Some(risultato) => println!("Risultato: {}", risultato),
None => println!("Overflow rilevato!"),
}
}
Wrapping Operations
Le operazioni wrapping eseguono il wrapping aritmetico (complemento a due) in caso di overflow:
fn main() {
let a: u8 = 255;
let wrapped = a.wrapping_add(1);
println!("255 + 1 (wrapping u8): {}", wrapped); // 0
let wrapped2 = a.wrapping_add(10);
println!("255 + 10 (wrapping u8): {}", wrapped2); // 9
let b: i8 = 127;
let wrapped3 = b.wrapping_add(1);
println!("127 + 1 (wrapping i8): {}", wrapped3); // -128
}
Saturating Operations
Le operazioni saturating si fermano al valore massimo o minimo del tipo:
fn main() {
let a: u8 = 250;
let saturated = a.saturating_add(10);
println!("250 + 10 (saturating u8): {}", saturated); // 255
let b: u8 = 5;
let saturated2 = b.saturating_sub(10);
println!("5 - 10 (saturating u8): {}", saturated2); // 0
let c: i8 = 120;
let saturated3 = c.saturating_add(20);
println!("120 + 20 (saturating i8): {}", saturated3); // 127
// Utile per barre di progresso, volumi, ecc.
let mut volume: u8 = 240;
volume = volume.saturating_add(30); // Non supera 255
println!("Volume: {}", volume); // 255
}
Overflowing Operations
Le operazioni overflowing restituiscono una tupla con il risultato e un booleano che indica se c’e stato overflow:
fn main() {
let a: u8 = 200;
let (risultato, overflow) = a.overflowing_add(100);
println!("200 + 100 (u8): {} (overflow: {})", risultato, overflow);
// Output: 44 (overflow: true)
let (risultato2, overflow2) = a.overflowing_add(10);
println!("200 + 10 (u8): {} (overflow: {})", risultato2, overflow2);
// Output: 210 (overflow: false)
}
Operator Overloading con i Trait
In Rust, gli operatori aritmetici sono implementati tramite i trait del modulo std::ops. È possibile implementarli per i propri tipi:
use std::ops::Add;
#[derive(Debug)]
struct Vettore2D {
x: f64,
y: f64,
}
impl Add for Vettore2D {
type Output = Vettore2D;
fn add(self, altro: Vettore2D) -> Vettore2D {
Vettore2D {
x: self.x + altro.x,
y: self.y + altro.y,
}
}
}
fn main() {
let v1 = Vettore2D { x: 1.0, y: 2.0 };
let v2 = Vettore2D { x: 3.0, y: 4.0 };
let v3 = v1 + v2;
println!("Risultato: {:?}", v3); // Vettore2D { x: 4.0, y: 6.0 }
}
I trait per gli operatori aritmetici sono: Add (+), Sub (-), Mul (*), Div (/) e Rem (%).
Conclusione
Gli operatori aritmetici in Rust sono familiari nella sintassi ma unici nel loro rigore. L’assenza di conversioni implicite e la gestione esplicita dell’overflow attraverso le varianti checked, wrapping, saturating e overflowing rendono il codice numerico in Rust estremamente sicuro e prevedibile. Questa attenzione ai dettagli, combinata con la possibilita di implementare operator overloading tramite i trait, rende Rust un linguaggio potente e affidabile per il calcolo numerico.