Il linguaggio di sistemi più amato: memoria sicura senza garbage collector, performance paragonabile a C/C++. Impara ownership, borrowing, struct, enum e le principali strutture dati. Con esempi pratici ed esercizi.
Introduzione a Rust
Rust è un linguaggio di programmazione di sistemi progettato per garantire sicurezza della memoria, alta performance e concorrenza sicura — senza garbage collector. È stato votato il linguaggio più amato dagli sviluppatori per otto anni consecutivi sul sondaggio di Stack Overflow.
Caratteristica
Descrizione
Sicurezza memoria
Nessun null pointer, nessun dangling pointer, garantiti a compile-time
Performance
Veloce come C/C++, zero overhead astrazioni
Ownership
Sistema unico per la gestione della memoria senza GC
Concorrenza
Thread sicuri garantiti dal compilatore
Ecosistema
Cargo (gestore pacchetti), crates.io (repository)
Rust è usato da Mozilla (Firefox), Microsoft (Windows), Amazon (AWS), Google, Meta e Linux Kernel per le parti critiche in termini di performance e sicurezza.
💡 Il punto di forza di Rust è che il compilatore ti impedisce di scrivere codice non sicuro prima ancora che il programma giri. Questo elimina intere categorie di bug comuni in C/C++.
# Crea un nuovo progetto con Cargo
cargo new mio_progetto
cd mio_progetto
# Struttura creata:
# mio_progetto/
# Cargo.toml ← metadati e dipendenze
# src/
# main.rs ← codice sorgente
# Compila ed esegui
cargo run
# Solo compilazione
cargo build
# Build ottimizzata per produzione
cargo build --release
💡 println! con il punto esclamativo è una macro, non una funzione. Le macro in Rust si riconoscono dal ! finale.
Sintassi base
Rust ha una sintassi familiare per chi conosce C, C++ o Java, con alcune caratteristiche proprie.
Struttura base di un programma
// Questo è un commento
/* Commento
su più righe */
fn main() {
// Il punto di ingresso è sempre main()
let x = 5; // immutabile per default
let mut y = 10; // mut = modificabile
y = 20;
println!("x = {x}");
println!("y = {y}");
// Blocchi: l'ultima espressione senza ; è il valore
let z = {
let a = 3;
a * 2 // nessun ; → valore del blocco = 6
};
println!("z = {z}");
}
Output formattato
fn main() {
let nome = "Alice";
let eta = 30;
let pi = 3.14159;
println!("Nome: {nome}");
println!("Età: {eta}");
println!("Pi greco: {pi:.2}"); // 2 decimali
println!("{:>10}", "destra"); // allineato a destra
println!("{:0>5}", 42); // "00042"
println!("{:b}", 255); // binario: 11111111
println!("{:x}", 255); // esadecimale: ff
}
⚠️ In Rust le variabili sono immutabili per default. Devi aggiungere mut esplicitamente se vuoi modificarle. Questo ti protegge da modifiche accidentali.
Variabili e tipi di dato
Rust è tipizzato staticamente: ogni variabile ha un tipo noto a compile-time. Spesso il compilatore lo inferisce automaticamente.
Categoria
Tipo
Esempio
Interi con segno
i8, i16, i32, i64, i128, isize
let x: i32 = -5;
Interi senza segno
u8, u16, u32, u64, u128, usize
let n: u8 = 255;
Virgola mobile
f32, f64
let pi: f64 = 3.14;
Booleano
bool
let ok: bool = true;
Carattere
char
let c: char = 'A';
Dichiarazione di variabili
fn main() {
// Inferenza di tipo
let x = 42; // i32 (default)
let y = 3.14; // f64 (default)
let attivo = true; // bool
let lettera = 'R'; // char
// Tipo esplicito
let grande: i64 = 1_000_000_000; // _ per leggibilità
let piccolo: u8 = 255;
// Costanti (MAIUSCOLO per convenzione)
const MAX_PUNTI: u32 = 100_000;
println!("{x} {y} {attivo} {lettera}");
println!("{grande} {piccolo} {MAX_PUNTI}");
}
Shadowing: ridichiarare una variabile
fn main() {
let x = 5;
let x = x + 1; // shadowing: nuova variabile x
let x = x * 2;
println!("{x}"); // 12
// Lo shadowing permette anche di cambiare tipo
let spazio = " ";
let spazio = spazio.len(); // ora è usize, non &str
println!("{spazio}"); // 3
}
🎯 Il shadowing è diverso dal mut: con shadowing crei una nuova variabile (puoi anche cambiare tipo), con mut modifichi la stessa variabile (il tipo rimane fisso).
Operatori
Operatori aritmetici e di confronto
fn main() {
let a = 10;
let b = 3;
// Aritmetici
println!("{}", a + b); // 13
println!("{}", a - b); // 7
println!("{}", a * b); // 30
println!("{}", a / b); // 3 (divisione intera tra interi)
println!("{}", a % b); // 1 (modulo)
let x: f64 = 10.0;
let y: f64 = 3.0;
println!("{:.2}", x / y); // 3.33
// Confronto (restituiscono bool)
println!("{}", a > b); // true
println!("{}", a == b); // false
println!("{}", a != b); // true
println!("{}", a >= 10); // true
}
Operatori logici e bit a bit
fn main() {
let vero = true;
let falso = false;
println!("{}", vero && falso); // false (AND)
println!("{}", vero || falso); // true (OR)
println!("{}", !vero); // false (NOT)
// Operatori bit a bit
let a: u8 = 0b1100; // 12
let b: u8 = 0b1010; // 10
println!("{:b}", a & b); // 1000 (8) AND
println!("{:b}", a | b); // 1110 (14) OR
println!("{:b}", a ^ b); // 0110 (6) XOR
println!("{:b}", a << 1); // 11000 (24) shift sinistro
}
if / else / match
if e else
fn main() {
let numero = 7;
if numero < 5 {
println!("piccolo");
} else if numero < 10 {
println!("medio"); // stampato
} else {
println!("grande");
}
// if come espressione (restituisce un valore)
let descrizione = if numero % 2 == 0 { "pari" } else { "dispari" };
println!("{numero} è {descrizione}");
}
match — la switch di Rust
fn main() {
let voto = 8;
let giudizio = match voto {
10 => "Eccellente",
8 | 9 => "Ottimo", // più pattern con |
6 | 7 => "Sufficiente",
1..=5 => "Insufficiente", // range inclusivo
_ => "Voto non valido" // default (wildcard)
};
println!("{giudizio}");
// match su tuples
let punto = (0, 1);
match punto {
(0, 0) => println!("Origine"),
(x, 0) => println!("Sull'asse X: {x}"),
(0, y) => println!("Sull'asse Y: {y}"),
(x, y) => println!("({x}, {y})"),
}
}
💡 Il match di Rust è esaustivo: il compilatore ti obbliga a coprire tutti i casi. Questo elimina i bug da casi non gestiti.
Cicli: loop, while, for
loop — ciclo infinito con break
fn main() {
let mut contatore = 0;
let risultato = loop {
contatore += 1;
if contatore == 10 {
break contatore * 2; // break restituisce un valore
}
};
println!("Risultato: {risultato}"); // 20
}
while e for
fn main() {
// while
let mut n = 3;
while n > 0 {
println!("{n}...");
n -= 1;
}
println!("Via!");
// for su un range
for i in 1..=5 {
print!("{i} "); // 1 2 3 4 5
}
println!();
// for su una collezione
let frutti = ["mela", "pera", "kiwi"];
for frutto in &frutti {
println!("{frutto}");
}
// con indice
for (i, frutto) in frutti.iter().enumerate() {
println!("{i}: {frutto}");
}
}
Funzioni
In Rust le funzioni si dichiarano con fn. Il tipo dei parametri e del valore di ritorno è obbligatorio. L'ultima espressione senza punto e virgola è il valore di ritorno.
Funzioni: parametri e ritorno
fn somma(a: i32, b: i32) -> i32 {
a + b // nessun "return" né ";" → valore di ritorno
}
fn saluta(nome: &str) -> String {
format!("Ciao, {nome}!")
}
fn minmax(v: &[i32]) -> (i32, i32) {
let mut min = v[0];
let mut max = v[0];
for &n in v {
if n < min { min = n; }
if n > max { max = n; }
}
(min, max) // restituisce una tupla
}
fn main() {
println!("{}", somma(3, 7)); // 10
println!("{}", saluta("Alice")); // Ciao, Alice!
let numeri = vec![4, 1, 9, 2, 7];
let (min, max) = minmax(&numeri);
println!("min={min} max={max}"); // min=1 max=9
}
Rust ha due tipi di stringa: &str (slice di stringa, immutabile, borrowed) e String (stringa heap-allocated, modificabile, owned).
&str vs String
fn main() {
// &str: letterale stringa, immutabile
let s1: &str = "Ciao, mondo!";
// String: allocata sull'heap, modificabile
let mut s2 = String::from("Ciao");
s2.push_str(", Rust!"); // appendi
s2.push('!'); // appendi un char
println!("{s1}");
println!("{s2}");
// Conversioni
let s3 = s1.to_string(); // &str → String
let s4: &str = &s2; // String → &str (slicing)
// Concatenazione
let s5 = String::from("Hello, ");
let s6 = String::from("world!");
let s7 = s5 + &s6; // s5 viene spostato (moved)
println!("{s7}");
}
Metodi utili sulle stringhe
fn main() {
let s = String::from(" Ciao, Rust! ");
println!("{}", s.trim()); // "Ciao, Rust!"
println!("{}", s.to_uppercase());
println!("{}", s.to_lowercase());
println!("{}", s.contains("Rust")); // true
println!("{}", s.replace("Rust", "mondo"));
println!("{}", s.len()); // lunghezza in byte
// Spezzare una stringa
let csv = "alfa,beta,gamma";
let parti: Vec<&str> = csv.split(',').collect();
println!("{:?}", parti); // ["alfa", "beta", "gamma"]
// Unire un vettore in stringa
let unita = parti.join(" - ");
println!("{unita}"); // alfa - beta - gamma
}
Ownership (Proprietà)
L'ownership è il meccanismo centrale di Rust per gestire la memoria senza garbage collector. Ogni valore ha un unico proprietario; quando il proprietario esce dallo scope, la memoria viene liberata automaticamente.
Tre regole fondamentali
Ogni valore ha un solo proprietario
Quando il proprietario esce dallo scope, il valore viene eliminato
Può esserci solo un proprietario alla volta
Move: trasferimento di proprietà
fn main() {
let s1 = String::from("ciao");
let s2 = s1; // s1 viene SPOSTATO in s2
// println!("{s1}"); // ERRORE: s1 non è più valido!
println!("{s2}"); // OK
// Clone: copia profonda esplicita
let s3 = String::from("mondo");
let s4 = s3.clone(); // copia indipendente
println!("{s3} {s4}"); // entrambi validi
// I tipi scalari (i32, bool, f64...) si COPIANO automaticamente
let x = 5;
let y = x; // copia, non move
println!("{x} {y}"); // entrambi validi
}
Ownership nelle funzioni
fn prendi(s: String) { // s diventa proprietario
println!("{s}");
} // s viene eliminato qui
fn restituisce(s: String) -> String {
s // ritorna la proprietà al chiamante
}
fn main() {
let s1 = String::from("ciao");
prendi(s1);
// println!("{s1}"); // ERRORE: s1 è stato spostato nella funzione
let s2 = String::from("mondo");
let s3 = restituisce(s2);
println!("{s3}"); // OK: la proprietà è tornata a s3
}
💡 Spostare la proprietà ad ogni chiamata di funzione è scomodo. Per questo esiste il borrowing: puoi prestare un valore senza cederne la proprietà.
Borrowing e riferimenti
Il borrowing permette di usare un valore senza prenderne la proprietà, tramite i riferimenti (&). I riferimenti possono essere immutabili (molti contemporaneamente) o mutabili (uno solo alla volta).
Riferimenti immutabili
fn lunghezza(s: &String) -> usize {
s.len() // prendiamo in prestito s, non lo possediamo
}
fn main() {
let s = String::from("ciao mondo");
// Passiamo un riferimento: s rimane valida dopo la chiamata
let len = lunghezza(&s);
println!("'{s}' ha {len} caratteri");
// Possiamo avere molti riferimenti immutabili contemporaneamente
let r1 = &s;
let r2 = &s;
println!("{r1} e {r2}"); // OK
}
Riferimenti mutabili
fn aggiungi(s: &mut String) {
s.push_str(", mondo");
}
fn main() {
let mut s = String::from("ciao");
aggiungi(&mut s);
println!("{s}"); // ciao, mondo
// REGOLA: solo UN riferimento mutabile alla volta
let r1 = &mut s;
// let r2 = &mut s; // ERRORE: due riferimenti mutabili!
println!("{r1}");
// Non si possono mescolare immutabile e mutabile
let s2 = String::from("hello");
let r_imm = &s2;
// let r_mut = &mut s2; // ERRORE: già in prestito come immutabile
println!("{r_imm}");
}
Tipo di riferimento
Quanti contemporaneamente
Modifica
&T (immutabile)
Illimitati
No
&mut T (mutabile)
Uno solo
Sì
🎯 Queste regole sui riferimenti, applicate a compile-time dal borrow checker, eliminano le race condition e i dangling pointer prima ancora che il programma venga eseguito.
Array e Vettori
Array — dimensione fissa
Array (lunghezza fissa, stack)
fn main() {
// Array: tipo [T; N], dimensione fissa nota a compile-time
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeri = [0; 10]; // 10 zeri
println!("{}", arr[0]); // 1
println!("{}", arr.len()); // 5
println!("{:?}", arr); // [1, 2, 3, 4, 5]
// Iterazione
for n in arr {
print!("{n} ");
}
println!();
}
Vec — vettore dinamico
Vec<T> (lunghezza dinamica, heap)
fn main() {
// Vettore: cresce e si riduce dinamicamente
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
println!("{:?}", v); // [1, 2, 3]
// Macro vec! per creare con valori iniziali
let mut nomi = vec!["Alice", "Bob", "Charlie"];
nomi.push("Diana");
println!("{}", v[0]); // 1
println!("{}", v.len()); // 3
v.pop(); // rimuove l'ultimo
println!("{:?}", v); // [1, 2]
// Slicing
let slice = &v[0..2];
println!("{:?}", slice);
// Iterazione
for n in &v {
print!("{n} ");
}
}
💡 Usa array quando la dimensione è fissa e nota a compile-time (più efficiente, allocato in stack). Usa Vec quando la dimensione può variare a runtime.
Tuple
Le tuple raggruppano valori di tipi diversi. Hanno dimensione fissa e si accede agli elementi tramite indice puntato (.0, .1, ...).
Tuple: utilizzo e destructuring
fn main() {
let persona: (&str, i32, f64) = ("Alice", 30, 1.68);
// Accesso per indice
println!("Nome: {}", persona.0); // Alice
println!("Età: {}", persona.1); // 30
println!("Alt: {}", persona.2); // 1.68
// Destructuring (scomposizione)
let (nome, eta, altezza) = persona;
println!("{nome} ha {eta} anni ed è alta {altezza}m");
// Funzione che restituisce più valori con tuple
fn dividi(a: i32, b: i32) -> (i32, i32) {
(a / b, a % b)
}
let (quoziente, resto) = dividi(17, 5);
println!("{quoziente} resto {resto}"); // 3 resto 2
// Tupla vuota (unit type)
let vuota: () = ();
println!("{:?}", vuota); // ()
}
HashMap
La HashMap<K, V> memorizza coppie chiave-valore con accesso medio O(1). Va importata da std::collections.
HashMap: operazioni di base
use std::collections::HashMap;
fn main() {
let mut voti: HashMap<String, i32> = HashMap::new();
// Inserimento
voti.insert(String::from("Alice"), 9);
voti.insert(String::from("Luca"), 7);
voti.insert(String::from("Marco"), 8);
// Lettura
if let Some(v) = voti.get("Alice") {
println!("Voto di Alice: {v}"); // 9
}
// Contiene la chiave?
println!("{}", voti.contains_key("Luca")); // true
// Iterazione
for (nome, voto) in &voti {
println!("{nome}: {voto}");
}
// Rimuovi
voti.remove("Marco");
println!("{}", voti.len()); // 2
}
entry API — inserisci se non esiste
use std::collections::HashMap;
fn main() {
let testo = "ciao mondo ciao rust ciao";
let mut conteggi: HashMap<&str, u32> = HashMap::new();
for parola in testo.split_whitespace() {
// entry: se la chiave esiste aggiorna, altrimenti inserisce 0
let contatore = conteggi.entry(parola).or_insert(0);
*contatore += 1;
}
println!("{:?}", conteggi);
// {"ciao": 3, "mondo": 1, "rust": 1}
}
Struct
Le struct sono tipi di dato custom che raggruppano campi correlati. Puoi implementare metodi tramite blocchi impl.
Gli enum di Rust sono molto più potenti di quelli di altri linguaggi: ogni variante può contenere dati. Combinati con match, gestiscono situazioni complesse in modo sicuro.
Enum con dati e match
#[derive(Debug)]
enum Forma {
Cerchio(f64), // contiene il raggio
Rettangolo(f64, f64), // larghezza, altezza
Triangolo { base: f64, h: f64 }, // campi nominati
}
impl Forma {
fn area(&self) -> f64 {
match self {
Forma::Cerchio(r) => std::f64::consts::PI * r * r,
Forma::Rettangolo(l, a) => l * a,
Forma::Triangolo { base, h } => 0.5 * base * h,
}
}
}
fn main() {
let forme = vec![
Forma::Cerchio(3.0),
Forma::Rettangolo(4.0, 5.0),
Forma::Triangolo { base: 6.0, h: 4.0 },
];
for f in &forme {
println!("{:?} → area {:.2}", f, f.area());
}
}
Option e Result: enum della libreria standard
fn divisione(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
fn main() {
// Option<T>: Some(valore) oppure None
match divisione(10.0, 2.0) {
Some(r) => println!("Risultato: {r}"),
None => println!("Divisione per zero!"),
}
// Metodi comodi su Option
let r = divisione(10.0, 0.0).unwrap_or(0.0);
println!("{r}"); // 0.0
// Result<T, E>: Ok(valore) oppure Err(errore)
let s = "42";
match s.parse::<i32>() {
Ok(n) => println!("Numero: {n}"),
Err(e) => println!("Errore: {e}"),
}
}
💡 In Rust non esiste null. Invece di null pointer, si usa Option<T>: sei obbligato a gestire esplicitamente il caso "nessun valore", eliminando i NullPointerException a runtime.
Esercizi pratici
🏋️ Esercizio 1 — Calcolatore di media
Scrivi una funzione che prenda un Vec<f64> e restituisca Option<f64> con la media, oppure None se il vettore è vuoto.
Soluzione Esercizio 1
fn media(numeri: &[f64]) -> Option<f64> {
if numeri.is_empty() {
return None;
}
Some(numeri.iter().sum::<f64>() / numeri.len() as f64)
}
fn main() {
let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
match media(&v) {
Some(m) => println!("Media: {m}"), // 3.0
None => println!("Vettore vuoto"),
}
println!("{:?}", media(&vec![])); // None
}
🏋️ Esercizio 2 — Contatore parole
Usa una HashMap per contare quante volte ogni parola appare in una stringa. Stampa le parole in ordine di frequenza decrescente.
Soluzione Esercizio 2
use std::collections::HashMap;
fn conta_parole(testo: &str) -> Vec<(&str, usize)> {
let mut mappa: HashMap<&str, usize> = HashMap::new();
for parola in testo.split_whitespace() {
*mappa.entry(parola).or_insert(0) += 1;
}
let mut v: Vec<_> = mappa.into_iter().collect();
v.sort_by(|a, b| b.1.cmp(&a.1)); // decrescente
v
}
fn main() {
let t = "ciao mondo ciao rust ciao mondo";
for (parola, n) in conta_parole(t) {
println!("{parola}: {n}");
}
}
🏋️ Esercizio 3 — Struct Studente
Crea una struct Studente con nome, età e voti (Vec<f64>). Implementa un metodo media_voti e un metodo promosso che restituisce true se la media è ≥ 6.
Complimenti! Hai completato il corso Rust. Ora conosci le basi del linguaggio più sicuro e performante dell'ecosistema moderno: sintassi, tipi, controllo di flusso, ownership, borrowing e le principali strutture dati.
Cosa hai imparato:
Variabili immutabili per default, shadowing, tipi scalari
if/else, match esaustivo, cicli loop/while/for
Funzioni, closures e riferimenti
Ownership e borrow checker: memoria sicura senza GC
Stringhe (&str vs String), array, Vec, tuple, HashMap
Struct con metodi, enum con dati, Option e Result
I prossimi passi in Rust: closures e iteratori, trait, generici, error handling avanzato, lifetimes e la programmazione asincrona con Tokio.