Home/ Academy/ programmazione
programmazione 10 min lettura

Rust: guida completa al linguaggio di programmazione per principianti

Scopri Rust, il linguaggio veloce e sicuro: installazione, variabili, ownership, struct, enum, errori e Cargo spiegati passo passo per principianti.

D
Domenico / MarketCode

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:

  1. installa Rust e familiarizza con Cargo scrivendo piccoli programmi;
  2. ripassa variabili, tipi, funzioni e controllo del flusso;
  3. dedica tempo extra a ownership e borrowing, esercitandoti con Rustlings;
  4. realizza un piccolo progetto da riga di comando, ad esempio un contatore di parole o una lista di cose da fare;
  5. leggi con calma il libro ufficiale e passa a struct, trait, generics e test;
  6. in seguito esplora le librerie per il web e per la programmazione asincrona.

Risorse gratuite e affidabili:

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.

Condividi l'articolo

D
Domenico Sviluppatore & Consulente — MarketCode

Sviluppo software nativo ad alte prestazioni e strategie SEO/GEO mirate per portare risultati tangibili alle imprese. Parliamo del tuo progetto →

Tutti gli articoli
Continua a leggere

Articoli correlati

E-commerce B2B ad Alte Prestazioni: Guida all'Architettura PHP senza Framework

Come Costruire un Funnel di Acquisizione Clienti per una PMI Locale

GDPR: Guida Completa alla Conformità per l'Era Digitale

Il Valore Economico della Qualità del Codice: Perché Scelte Tecniche come TypeScript Cambiano il ROI Aziendale

Guida Completa a TypeScript: Architettura, Funzionalità e Best Practice per lo Sviluppo Moderno

Campagne social che convertono: come pianificare, lanciare e misurare il ROI

Next Step

Portiamo il tuo progetto
al livello successivo?

Non lasciare questi concetti sulla carta. Analizziamo insieme la tua presenza online o lo sviluppo del tuo software dedicato.

Cerca nell'articolo