Tipi di Dati in Rust
Rust è un linguaggio staticamente tipizzato: il tipo di ogni variabile deve essere noto a tempo di compilazione. Grazie all’inferenza dei tipi, il compilatore è spesso in grado di determinare automaticamente il tipo, ma in alcuni casi è necessario specificarlo esplicitamente. In questa guida esploreremo i principali tipi di dati disponibili in Rust.
Tipi Scalari
I tipi scalari rappresentano un singolo valore. Rust ha quattro categorie di tipi scalari.
Interi
I tipi interi rappresentano numeri senza parte decimale. Rust offre interi con e senza segno di diverse dimensioni:
fn main() {
let intero_con_segno: i32 = -42; // Da -2^31 a 2^31 - 1
let intero_senza_segno: u32 = 42; // Da 0 a 2^32 - 1
let byte: u8 = 255; // Da 0 a 255
let grande: i64 = 1_000_000_000; // Intero a 64 bit
println!("{}, {}, {}, {}", intero_con_segno, intero_senza_segno, byte, grande);
}
I tipi disponibili sono: i8, i16, i32, i64, i128, isize (con segno) e u8, u16, u32, u64, u128, usize (senza segno). Il tipo predefinito per i letterali interi è i32.
Numeri in Virgola Mobile
Rust ha due tipi per i numeri decimali: f32 e f64. Il tipo predefinito è f64:
fn main() {
let x = 2.0; // f64 (predefinito)
let y: f32 = 3.14; // f32
println!("x = {}, y = {}", x, y);
}
Booleani
Il tipo bool può avere solo due valori: true e false. Occupa 1 byte in memoria:
fn main() {
let vero: bool = true;
let falso = false; // Tipo inferito come bool
println!("vero: {}, falso: {}", vero, falso);
}
Caratteri
Il tipo char in Rust rappresenta un valore scalare Unicode e occupa 4 byte. Può rappresentare molto più dei soli caratteri ASCII:
fn main() {
let lettera = 'a';
let emoji = '🦀';
let ideogramma = 'æ¼¢';
let accento: char = 'è';
println!("{} {} {} {}", lettera, emoji, ideogramma, accento);
}
Tipi Composti
I tipi composti raggruppano più valori in un singolo tipo. Rust ha due tipi composti primitivi: tuple e array.
Tuple
Una tupla raggruppa valori di tipi diversi in un unico tipo composto. Ha una lunghezza fissa:
fn main() {
let persona: (&str, i32, bool) = ("Alice", 30, true);
// Accesso tramite destructuring
let (nome, eta, attiva) = persona;
println!("Nome: {}, Età : {}, Attiva: {}", nome, eta, attiva);
// Accesso tramite indice (con punto e numero)
println!("Nome: {}", persona.0);
println!("Età : {}", persona.1);
println!("Attiva: {}", persona.2);
}
La tupla vuota () è chiamata unit type ed è il valore restituito dalle funzioni che non restituiscono nulla esplicitamente:
fn non_restituisce_nulla() {
println!("Questa funzione restituisce ()");
}
fn main() {
let risultato: () = non_restituisce_nulla();
println!("Risultato: {:?}", risultato); // Stampa: ()
}
Array
Un array contiene una collezione di valori dello stesso tipo con lunghezza fissa, nota a tempo di compilazione:
fn main() {
let numeri: [i32; 5] = [1, 2, 3, 4, 5];
let primo = numeri[0];
let ultimo = numeri[4];
println!("Primo: {}, Ultimo: {}", primo, ultimo);
println!("Lunghezza: {}", numeri.len());
// Array inizializzato con lo stesso valore
let zeri = [0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
println!("Zeri: {:?}", zeri);
}
L’accesso a un indice fuori dai limiti causa un panic a runtime, non un comportamento indefinito come in C:
fn main() {
let arr = [1, 2, 3];
// arr[5]; // Panic! index out of bounds: the len is 3 but the index is 5
}
Inferenza dei Tipi
Rust ha un potente sistema di inferenza dei tipi che nella maggior parte dei casi deduce il tipo corretto:
fn main() {
let x = 5; // i32 (inferito)
let y = 3.14; // f64 (inferito)
let attivo = true; // bool (inferito)
let nome = "Rust"; // &str (inferito)
let numeri = vec![1, 2, 3]; // Vec<i32> (inferito)
println!("x: {}, y: {}, attivo: {}, nome: {}", x, y, attivo, nome);
println!("numeri: {:?}", numeri);
}
Talvolta l’inferenza necessita di aiuto, specialmente con metodi generici:
fn main() {
// Il compilatore non sa quale tipo numerico parsare
// let numero = "42".parse().unwrap(); // Errore!
let numero: i32 = "42".parse().unwrap(); // OK: tipo annotato
let numero2 = "42".parse::<f64>().unwrap(); // OK: turbofish syntax
println!("i32: {}, f64: {}", numero, numero2);
}
Annotazione dei Tipi
Quando l’inferenza non è sufficiente o si vuole essere espliciti, si annotano i tipi:
fn main() {
let temperatura: f64 = 36.6;
let contatore: u64 = 0;
let messaggio: &str = "Ciao mondo";
let coordinate: (f64, f64) = (45.46, 9.19);
let punteggi: [u32; 3] = [100, 95, 88];
println!("Temperatura: {}°C", temperatura);
println!("Coordinate: {:?}", coordinate);
println!("Punteggi: {:?}", punteggi);
}
Alias di Tipo con type
La parola chiave type crea un alias per un tipo esistente. Non crea un nuovo tipo, ma un nome alternativo:
type Chilometri = f64;
type Risultato = Result<String, std::io::Error>;
type Punto2D = (f64, f64);
fn distanza(p1: Punto2D, p2: Punto2D) -> Chilometri {
let dx = p2.0 - p1.0;
let dy = p2.1 - p1.1;
(dx * dx + dy * dy).sqrt()
}
fn main() {
let roma: Punto2D = (41.9, 12.5);
let milano: Punto2D = (45.5, 9.2);
let dist: Chilometri = distanza(roma, milano);
println!("Distanza approssimativa: {:.2}", dist);
}
Gli alias sono utili per rendere il codice più leggibile, specialmente con tipi complessi:
type CallbackFn = Box<dyn Fn(i32) -> i32>;
fn applica(callback: &CallbackFn, valore: i32) -> i32 {
callback(valore)
}
fn main() {
let raddoppia: CallbackFn = Box::new(|x| x * 2);
let risultato = applica(&raddoppia, 21);
println!("Risultato: {}", risultato); // Stampa: 42
}
Conclusione
Il sistema di tipi di Rust è uno dei suoi punti di forza principali. La combinazione di tipi scalari e composti copre le esigenze fondamentali della programmazione, mentre l’inferenza dei tipi mantiene il codice conciso senza sacrificare la sicurezza. Gli alias di tipo permettono di rendere il codice più leggibile e manutenibile. Comprendere i tipi di dati di Rust è essenziale per sfruttare appieno il sistema di ownership e le garanzie di sicurezza del linguaggio.