Rust è un linguaggio di programmazione compilato, moderno e pensato per scrivere software veloce, affidabile e sicuro. Nato come progetto sostenuto da Mozilla e oggi sviluppato da una comunità open source, ha una promessa precisa: darti prestazioni vicine a quelle di C e C++ senza i classici errori di gestione della memoria. In questa guida per principianti vediamo le basi di Rust passo per passo, con esempi di codice da provare subito: dall'installazione alla gestione degli errori, passando per ownership, struct, enum e Cargo.
Cos'è Rust e perché imparare questo linguaggio
- Rust è un linguaggio compilato: il codice viene trasformato in un programma nativo, senza interprete e senza garbage collector. Questo lo rende adatto a software che richiedono velocità e controllo delle risorse. I motivi principali per cui sempre più sviluppatori lo studiano sono questi:
- Prestazioni elevate: il codice compilato gira direttamente sulla macchina, con un uso della memoria molto prevedibile.
- Sicurezza della memoria: il compilatore blocca già in fase di compilazione intere categorie di bug, come accessi a memoria non valida o riferimenti "appesi".
- Concorrenza più sicura: le regole del linguaggio aiutano a evitare le data race nei programmi che usano più thread.
- Tipi forti e messaggi d'errore chiari: il compilatore di Rust spiega spesso che cosa non va e come correggerlo.
- Strumenti integrati: Cargo (build e dipendenze), rustfmt (formattazione), Clippy (suggerimenti sul codice) e un sistema di test già pronti.
- Ecosistema in crescita: migliaia di librerie, chiamate crate, sono disponibili su crates.io.
Il prezzo da pagare è una curva di apprendimento più ripida all'inizio, soprattutto per il concetto di ownership che vedremo più avanti. Il vantaggio è che, una volta compreso, ti porta a scrivere codice più solido.
Dove si usa Rust
Rust si usa in molti ambiti:
- strumenti da riga di comando veloci e portabili;
- servizi backend e API ad alte prestazioni;
- WebAssembly, per eseguire codice veloce nel browser;
- sistemi embedded e componenti di basso livello;
- applicazioni desktop, ad esempio con il framework Tauri;
- strumenti per sviluppatori e infrastrutture.
Non è però la scelta migliore per tutto: per un sito vetrina o per uno script rapido esistono strumenti più semplici. Rust dà il meglio quando contano affidabilità e prestazioni.
Come installare Rust con rustup
Il modo ufficiale per installare Rust è rustup, che gestisce compilatore, versioni e componenti. Su Linux e macOS basta aprire il terminale ed eseguire:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Su Windows scarica il programma di installazione dalla pagina ufficiale: serve anche l'ambiente di compilazione C++ di Microsoft (Build Tools di Visual Studio), che l'installer ti guida a configurare. Al termine controlla che tutto funzioni:
rustc --version
cargo --version
Per aggiornare Rust in futuro usa rustup update. Come editor puoi usare Visual Studio Code con l'estensione rust-analyzer, che offre completamento, errori in tempo reale e navigazione del codice.
Il primo programma: Hello World con Cargo
Cargo è lo strumento che crea progetti, compila il codice e scarica le dipendenze. Creiamo il primo progetto:
cargo new ciao_rust
cd ciao_rust
cargo run
Cargo genera questa struttura:
ciao_rust/
├── Cargo.toml
└── src/
└── main.rs
Il file Cargo.toml contiene nome, versione e dipendenze del progetto, mentre src/main.rs contiene il codice. Il programma di partenza è questo:
fn main() {
println!("Ciao, Rust!");
}
fn main() è il punto di ingresso del programma. println! è una macro (lo riconosci dal punto esclamativo) che stampa una riga di testo. I comandi di Cargo che userai più spesso sono:
- cargo run: compila ed esegue il programma;
- cargo build: compila senza eseguire;
- cargo check: controlla gli errori senza produrre l'eseguibile, ed è molto più veloce;
- cargo build --release: compila con le ottimizzazioni per la versione finale;
- cargo test: esegue i test.
Variabili e mutabilità in Rust
In Rust le variabili sono immutabili per impostazione predefinita. Per poterle modificare devi dichiararle con mut:
fn main() {
let x = 5; // immutabile
let mut y = 10; // mutabile
y += 1;
const MAX_PUNTI: u32 = 100_000; // costante, il tipo è obbligatorio
let x = x + 1; // shadowing: nuova variabile con lo stesso nome
println!("{x} {y} {MAX_PUNTI}");
}
Lo shadowing permette di ridefinire una variabile (anche cambiandone il tipo) senza renderla mutabile. Le costanti si dichiarano con const, richiedono sempre il tipo e valgono per tutto il programma.
Tipi di dato: numeri, testo, tuple e array
Rust è un linguaggio a tipizzazione statica: ogni valore ha un tipo noto in fase di compilazione. Nella maggior parte dei casi il compilatore lo deduce da solo, ma puoi indicarlo:
let intero: i32 = -42; // intero con segno a 32 bit
let senza_segno: u8 = 255; // intero positivo a 8 bit
let decimale: f64 = 3.14; // numero in virgola mobile
let vero: bool = true;
let lettera: char = 'R'; // carattere Unicode
let tupla: (i32, f64, char) = (500, 6.4, 'z');
let (a, b, c) = tupla; // destrutturazione
let array: [i32; 5] = [1, 2, 3, 4, 5];
let primo = array[0];
I tipi interi hanno nomi come i8, i16, i32, i64 (con segno) e u8, u16, u32, u64 (senza segno). Le tuple raggruppano valori di tipi diversi, mentre gli array hanno lunghezza fissa e un solo tipo.
Stringhe: &str e String
Il testo è un punto in cui i principianti inciampano spesso, perché Rust ha due tipi principali:
let statica: &str = "ciao"; // porzione di testo, non modificabile
let mut dinamica: String = String::from("ciao"); // testo di proprietà, modificabile
dinamica.push_str(", mondo");
&str è un riferimento a del testo già esistente, come i letterali tra virgolette. String possiede il proprio testo e può crescere o cambiare.
Funzioni ed espressioni
Le funzioni si dichiarano con fn, con tipi espliciti per i parametri e per il valore restituito:
fn somma(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let risultato = somma(2, 3);
println!("{risultato}");
}
Nota che a + b non ha il punto e virgola: in Rust l'ultima espressione di un blocco è il valore restituito. Aggiungendo ; trasformeresti l'espressione in un'istruzione e la funzione non restituirebbe più il numero.
Controllo del flusso: if, cicli e match
Condizioni con if
if è un'espressione, quindi può produrre un valore:
let numero = 7;
if numero % 2 == 0 {
println!("pari");
} else {
println!("dispari");
}
let etichetta = if numero > 5 { "grande" } else { "piccolo" };
La condizione deve essere un bool: Rust non converte automaticamente numeri o testo in vero/falso.
Cicli: for, while e loop
for i in 1..=3 { // da 1 a 3 compreso
println!("{i}");
}
let mut n = 3;
while n > 0 {
n -= 1;
}
let mut contatore = 0;
let risultato = loop { // ciclo infinito con valore di uscita
contatore += 1;
if contatore == 10 {
break contatore * 2;
}
};
Il ciclo for è il più usato e il più sicuro, perché scorre intervalli e collezioni senza gestire indici a mano.
Pattern matching con match
match confronta un valore con una serie di schemi e deve coprire tutti i casi possibili:
let voto = 8;
match voto {
1..=5 => println!("insufficiente"),
6 | 7 => println!("sufficiente"),
8..=10 => println!("ottimo"),
_ => println!("voto non valido"),
}
Il simbolo _ rappresenta "tutto il resto". Se dimentichi un caso, il compilatore ti avvisa.
Ownership, borrowing e lifetime: il cuore di Rust
L'ownership (proprietà) è il concetto che rende Rust diverso dagli altri linguaggi: permette di gestire la memoria in sicurezza senza garbage collector. Si basa su tre regole:
ogni valore ha un unico proprietario;
può esserci un solo proprietario alla volta;
quando il proprietario esce dal suo ambito (scope), il valore viene rilasciato automaticamente.
Move e clone
let s1 = String::from("ciao");
let s2 = s1; // la proprietà passa a s2: s1 non è più valida
// println!("{s1}"); // errore di compilazione
let s3 = s2.clone(); // copia esplicita del contenuto
println!("{s2} {s3}");
Quando assegni un valore come String a un'altra variabile, la proprietà viene spostata (move). Se ti serve una copia vera, usi clone(). I tipi semplici come i32 o bool vengono invece copiati automaticamente.
Borrowing: prestare un valore con i riferimenti
Per usare un valore senza prenderne la proprietà lo si prende in prestito con un riferimento (&):
fn lunghezza(testo: &str) -> usize {
testo.len()
}
fn aggiungi_punto(testo: &mut String) {
testo.push('.');
}
fn main() {
let mut frase = String::from("Ciao");
println!("{}", lunghezza(&frase)); // riferimento immutabile
aggiungi_punto(&mut frase); // riferimento mutabile
println!("{frase}");
}
Le regole del borrow checker sono semplici da enunciare:
puoi avere più riferimenti immutabili oppure un solo riferimento mutabile, mai entrambi nello stesso momento;
un riferimento deve sempre puntare a un valore ancora valido.
Con queste regole il compilatore elimina in anticipo problemi come le data race e i riferimenti a memoria già liberata.
Lifetime in breve
I lifetime indicano al compilatore per quanto tempo un riferimento resta valido. Nella maggior parte dei casi sono dedotti automaticamente; li scrivi esplicitamente solo quando il compilatore non può capirlo da solo:
fn piu_lunga<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
L'annotazione 'a dice che il riferimento restituito vive al massimo quanto i due riferimenti ricevuti. All'inizio non devi preoccupartene: arriveranno quando ne avrai davvero bisogno.
Struct e metodi: creare i propri tipi
Le struct raggruppano dati correlati, mentre il blocco impl aggiunge funzioni e metodi:
struct Utente {
nome: String,
email: String,
attivo: bool,
}
impl Utente {
fn new(nome: &str, email: &str) -> Self {
Self {
nome: nome.to_string(),
email: email.to_string(),
attivo: true,
}
}
fn saluta(&self) -> String {
format!("Ciao, {}!", self.nome)
}
fn disattiva(&mut self) {
self.attivo = false;
}
}
fn main() {
let mut utente = Utente::new("Marco", "marco@example.com");
println!("{}", utente.saluta());
utente.disattiva();
}
I metodi ricevono &self (lettura) o &mut self (modifica). La funzione new non è una parola riservata: è solo una convenzione per i costruttori.
Enum, Option e pattern matching
Gli enum di Rust sono molto più potenti di quelli di altri linguaggi, perché ogni variante può contenere dati:
enum Stato {
Attivo,
Sospeso { motivo: String },
Chiuso(u32),
}
fn descrivi(stato: &Stato) -> String {
match stato {
Stato::Attivo => String::from("attivo"),
Stato::Sospeso { motivo } => format!("sospeso: {motivo}"),
Stato::Chiuso(giorni) => format!("chiuso da {giorni} giorni"),
}
}
Option: addio ai valori null
Rust non ha il valore null. Quando un valore può mancare si usa l'enum Option, che ha due varianti: Some(valore) e None.
fn trova_indice(lista: &[i32], cercato: i32) -> Option<usize> {
for (i, &valore) in lista.iter().enumerate() {
if valore == cercato {
return Some(i);
}
}
None
}
fn main() {
match trova_indice(&[10, 20, 30], 20) {
Some(i) => println!("trovato in posizione {i}"),
None => println!("non trovato"),
}
if let Some(i) = trova_indice(&[1, 2, 3], 3) {
println!("trovato in posizione {i}");
}
}
Il compilatore ti obbliga a gestire il caso None, eliminando una delle fonti di errori più comuni in molti altri linguaggi.
Gestione degli errori in Rust: Result e panic
Rust non usa le eccezioni. Distingue due situazioni: gli errori irrecuperabili, che fanno terminare il programma con un panic!, e quelli recuperabili, rappresentati dall'enum Result<T, E>, che ha le varianti Ok(valore) ed Err(errore).
use std::num::ParseIntError;
fn raddoppia(testo: &str) -> Result<i32, ParseIntError> {
let numero: i32 = testo.trim().parse()?;
Ok(numero * 2)
}
fn main() {
match raddoppia("21") {
Ok(valore) => println!("Risultato: {valore}"),
Err(errore) => println!("Errore: {errore}"),
}
match raddoppia("abc") {
Ok(valore) => println!("Risultato: {valore}"),
Err(errore) => println!("Errore: {errore}"),
}
}
L'operatore ? è una scorciatoia molto usata: se il risultato è Ok, estrae il valore; se è Err, esce subito dalla funzione restituendo l'errore. Esistono anche unwrap() ed expect(), che estraggono il valore ma vanno in panic in caso di errore:
let n: i32 = "42".parse().unwrap();
let m: i32 = "42".parse().expect("il valore deve essere un numero");
let p: i32 = "abc".parse().unwrap_or(0); // valore di riserva in caso di errore
Sono comodi per prove e piccoli esempi, ma in un programma vero conviene gestire gli errori con match, ? o valori di riserva.
Collezioni, iteratori e closure
Vec: la lista dinamica
let mut numeri = vec![1, 2, 3];
numeri.push(4);
for n in &numeri {
println!("{n}");
}
let somma: i32 = numeri.iter().sum();
let quadrati: Vec<i32> = numeri.iter().map(|n| n * n).collect();
Vec<T> è un vettore che può crescere. Scorrendolo con &numeri lo prendi in prestito senza consumarlo. I metodi map, filter e sum appartengono agli iteratori e si combinano in catene molto leggibili. La sintassi |n| n * n è una closure, cioè una piccola funzione anonima.
HashMap: coppie chiave-valore
use std::collections::HashMap;
let mut punteggi = HashMap::new();
punteggi.insert(String::from("Anna"), 10);
punteggi.insert(String::from("Luca"), 8);
if let Some(punti) = punteggi.get("Anna") {
println!("Anna ha {punti} punti");
}
Il metodo get restituisce un Option, perché la chiave potrebbe non esistere: ancora una volta il compilatore ti obbliga a gestire il caso mancante.
Trait e generics: scrivere codice riutilizzabile
I trait definiscono un comportamento comune, in modo simile alle interfacce di altri linguaggi. I generics permettono di scrivere funzioni che lavorano con tipi diversi:
trait Descrivibile {
fn descrizione(&self) -> String;
}
impl Descrivibile for Utente {
fn descrizione(&self) -> String {
format!("Utente {}", self.nome)
}
}
fn stampa<T: Descrivibile>(elemento: &T) {
println!("{}", elemento.descrizione());
}
fn il_piu_grande<T: PartialOrd + Copy>(lista: &[T]) -> T {
let mut massimo = lista[0];
for &elemento in lista {
if elemento > massimo {
massimo = elemento;
}
}
massimo
}
Nella funzione stampa, T: Descrivibile significa "accetto qualunque tipo che implementi il trait Descrivibile". Questi vincoli (trait bound) sono verificati dal compilatore, quindi non rischi errori a runtime per un metodo mancante. Molti trait sono già nella libreria standard, come Display, Debug, Clone e PartialOrd.
Moduli, crate e dipendenze con Cargo
Per organizzare il codice si usano i moduli. Tutto è privato per impostazione predefinita: con pub rendi visibile ciò che vuoi esporre.
mod geometria {
pub fn area_rettangolo(larghezza: f64, altezza: f64) -> f64 {
larghezza * altezza
}
}
use geometria::area_rettangolo;
fn main() {
println!("{}", area_rettangolo(3.0, 4.5));
}
Un crate è un pacchetto di codice Rust: può essere un programma (binary crate) o una libreria (library crate). Per aggiungere una dipendenza da crates.io basta un comando:
cargo add serde
Cargo aggiorna da solo il file Cargo.toml, nella sezione [dependencies], e scarica la libreria alla prima compilazione:
[package]
name = "ciao_rust"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = "1"
L'edition indica l'edizione del linguaggio usata dal progetto: il valore generato dipende dalla versione di Rust che hai installato.
Scrivere test in Rust
Il supporto ai test è parte del linguaggio. Basta scrivere funzioni con l'attributo #[test], di solito in un modulo tests nello stesso file:
fn somma(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn somma_funziona() {
assert_eq!(somma(2, 3), 5);
}
}
Esegui tutti i test con cargo test. Le macro assert!, assert_eq! e assert_ne! verificano le condizioni e mostrano un messaggio chiaro quando falliscono.
Strumenti utili: rustfmt, Clippy e documentazione
Oltre a Cargo, l'installazione di Rust include strumenti che ti fanno risparmiare tempo:
- cargo fmt formatta il codice secondo lo stile standard della comunità;
- cargo clippy analizza il codice e suggerisce miglioramenti e correzioni;
- cargo doc --open genera e apre la documentazione del tuo progetto e delle sue dipendenze;
- rustup update aggiorna il compilatore all'ultima versione stabile.
Errori comuni dei principianti
Quando si inizia con Rust è normale scontrarsi con il compilatore. Questi sono gli inciampi più frequenti:
- Usare clone() ovunque per far sparire gli errori: funziona, ma nasconde il problema e spreca risorse. Prima chiediti se basta un riferimento.
- Riempire il codice di unwrap(): va bene per provare, ma in un programma reale porta a panic inattesi.
- Confondere &str e String: in generale, nei parametri delle funzioni preferisci &str.
- Lottare contro il borrow checker invece di ripensare la struttura dei dati: spesso basta restituire un valore invece di tenere un riferimento.
- Ignorare i messaggi del compilatore: sono lunghi ma quasi sempre contengono il suggerimento giusto.
Rust per chi viene da PHP, JavaScript o Python
Se conosci già un linguaggio dinamico, questo confronto ti aiuta a orientarti:
| Concetto | PHP / JavaScript | Rust |
|---|---|---|
| Esecuzione | Interpretato o JIT | Compilato in codice nativo |
| Tipi | Dinamici | Statici, con deduzione automatica |
| Memoria | Garbage collector o conteggio dei riferimenti | Ownership e borrowing |
| Valori mancanti | null / undefined | Option |
| Errori | Eccezioni | Result e panic! |
| Pacchetti | Composer / npm | Cargo e crates.io |
Il cambio di mentalità principale è che in Rust molti controlli avvengono prima che il programma giri: più errori in compilazione, meno sorprese in produzione.
Come continuare a studiare Rust
Un percorso di studio efficace, dalle basi in avanti:
- installa Rust e familiarizza con Cargo scrivendo piccoli programmi;
- ripassa variabili, tipi, funzioni e controllo del flusso;
- dedica tempo extra a ownership e borrowing, esercitandoti con Rustlings;
- realizza un piccolo progetto da riga di comando, ad esempio un contatore di parole o una lista di cose da fare;
- leggi con calma il libro ufficiale e passa a struct, trait, generics e test;
- in seguito esplora le librerie per il web e per la programmazione asincrona.
Risorse gratuite e affidabili:
-
The Rust Programming Language, il libro ufficiale;
-
Rust by Example, per imparare dagli esempi;
-
Rustlings, esercizi guidati da svolgere sul proprio computer;
-
la documentazione della libreria standard;
-
crates.io, il registro delle librerie.
Domande frequenti su Rust
Rust è difficile da imparare?
È più impegnativo di linguaggi come Python o JavaScript, soprattutto per ownership e borrowing. La difficoltà è concentrata all'inizio: dopo aver interiorizzato questi concetti, il resto diventa molto più lineare.
Rust è meglio di C++?
Sono strumenti diversi. Rust offre la sicurezza della memoria come impostazione predefinita e un sistema di build moderno, mentre C++ ha un ecosistema enorme e una lunga storia in molti settori. La scelta dipende dal progetto e dal team.
Serve conoscere il C prima di imparare Rust?
No. Conoscere un altro linguaggio di programmazione aiuta, ma Rust si può imparare anche partendo da zero con una buona guida.
Rust si può usare per il web?
Sì: per lo sviluppo di backend e API e, tramite WebAssembly, per codice eseguito nel browser. Per siti vetrina o applicazioni semplici linguaggi come PHP o JavaScript restano spesso più rapidi da usare.
Quanto tempo serve per imparare le basi di Rust?
Dipende dall'esperienza di partenza e dalla costanza. Con esercizi regolari le basi si apprendono in alcune settimane, mentre servirà più pratica per sentirsi davvero a proprio agio con ownership e trait.
Conclusione
Rust unisce prestazioni elevate e sicurezza della memoria grazie a un sistema di regole, ownership e borrowing, che il compilatore verifica per te. Ora conosci gli strumenti principali: Cargo, variabili e tipi, controllo del flusso, struct ed enum, Option e Result, collezioni, trait, moduli e test. Il passo successivo è la pratica: scrivi piccoli programmi, sbaglia, leggi gli errori del compilatore e riprova.
Hai dubbi o vuoi un percorso guidato per imparare Rust? Scrivimi: ti aiuto a partire dal progetto giusto.
Azione bloccata
Per utilizzare i pulsanti di condivisione, contattarci su WhatsApp o accedere ai canali social, è necessario abilitare i cookie di Marketing. Vuoi abilitarli ora per continuare?