Tutorial di Rust
Rust è un linguaggio di programmazione di Mozilla. Può essere usato per scrivere strumenti a riga di comando, applicazioni web e programmi di rete. Il linguaggio, che gode di grande popolarità tra i programmatori, è adatto anche per la programmazione vicina all’hardware.
In questo tutorial di Rust vi mostriamo le sue caratteristiche più importanti evidenziando analogie e differenze rispetto ad altri linguaggi di uso comune. Vi guideremo attraverso l’installazione di Rust e imparerete a scrivere e compilare il codice Rust sul vostro sistema.
Una panoramica del linguaggio di programmazione Rust
Rust è un linguaggio compilato. Questa caratteristica si traduce in elevate prestazioni; allo stesso tempo il linguaggio offre astrazioni sofisticate che rendono più facile il lavoro del programmatore. Un’attenzione particolare di Rust è rivolta alla sicurezza della memoria. Ciò gli conferisce un particolare vantaggio rispetto ai linguaggi più vecchi come C e C++.
Come usare Rust sul proprio sistema
Poiché Rust è un software gratuito e open source (FOSS), chiunque può scaricare la sua toolchain e utilizzarla sul proprio sistema. A differenza di Python e JavaScript, Rust non è un linguaggio interpretato. Al posto di un interprete viene utilizzato un compilatore, come in C, C++ e Java. In pratica, questo significa che esistono due fasi per l’esecuzione del codice:
- La compilazione del codice sorgente che produce un file binario eseguibile.
- L’esecuzione del file binario risultante.
Nel caso più semplice, entrambi i passaggi sono controllati dalla riga di comando.
In un altro articolo della nostra Digital Guide diamo un’occhiata più da vicino alla differenza tra compilatore e interprete.
Rust può essere usato per creare non solo file binari eseguibili ma anche librerie. Se il codice compilato è un programma direttamente eseguibile, nel codice sorgente deve essere definita una funzione main(). Come in C / C+++, questo serve come punto d’ingresso nell’esecuzione del codice.
Installare Rust sul sistema locale
Per utilizzare Rust, è necessario prima di tutto un’installazione locale. Su macOS si usa il gestore di pacchetti Homebrew. Homebrew funziona anche su Linux. Aprite la riga di comando (‘Terminal.App’ sul Mac), copiate la seguente riga di codice nel terminale ed eseguitela:
Per installare Rust su Windows o su un altro sistema senza l’ausilio di Homebrew, usate lo strumento ufficiale Rustup.
Per verificare che l’installazione di Rust sia andata a buon fine, aprite una nuova finestra sulla riga di comando ed eseguite il seguente codice:
Se Rust è installato correttamente sul vostro sistema, vi verrà mostrata la versione del compilatore. Se invece appare un messaggio di errore, riavviate l’installazione.
Compilare il codice Rust
Per compilare il codice Rust è necessario un file di codice sorgente Rust. Aprite la riga di comando ed eseguite i seguenti pezzi di codice. Per prima cosa creeremo una cartella per il tutorial di Rust sul desktop e la apriremo:
Successivamente creiamo il file del codice sorgente di Rust per un semplice esempio di “Hello World”:
I file del codice sorgente di Rust terminano con l’abbreviazione .rs.
Infine, compileremo il codice sorgente di Rust ed eseguiremo il file binario risultante:
Utilizzate il comando rustc rust-tutorial.rs && ./rust-tutorial per combinare i due passaggi. In questo modo è possibile ricompilare ed eseguire il programma sulla riga di comando premendo il tasto freccia in alto seguito da Invio.
Gestire i pacchetti Rust con Cargo
Oltre al linguaggio Rust vero e proprio esistono vari pacchetti esterni. Questi cosiddetti crates possono essere ottenuti dal Rust Package Registry. A questo scopo viene utilizzato lo strumento Cargo installato insieme a Rust. Il comando cargo viene usato sulla riga di comando e permette di installare pacchetti e crearne di nuovi. Prima di tutto, verificate che Cargo sia stato installato correttamente:
Imparare le basi di Rust
Per imparare a usare Rust vi consigliamo di provare direttamente esempi di codice. A tale proposito è possibile utilizzare il file rust-tutorial.rs già creato. Copiate un campione di codice nel file, compilatelo ed eseguite il file binario risultante. Per funzionare, il codice di esempio deve essere inserito all’interno della funzione main()!
Potete anche usare Rust Playground direttamente nel vostro browser per provare il codice Rust.
Istruzioni e blocchi
Le istruzioni sono blocchi di base del codice in Rust. Un’istruzione termina con un punto e virgola (;) e, a differenza di un’espressione, non restituisce un valore. Più istruzioni possono essere raggruppate in un blocco. I blocchi sono delimitati da parentesi graffe ‘{}’, come in C/C++ e Java.
Commenti
I commenti sono una caratteristica importante di qualsiasi linguaggio di programmazione. Vengono utilizzati sia per la documentazione del codice che per la sua pianificazione prima che il codice stesso venga scritto. Rust usa la stessa sintassi dei commenti di C, C++, Java e JavaScript, pertanto qualsiasi testo dopo una doppia barra viene interpretato come un commento e ignorato dal compilatore:
Variabili e costanti
In Rust usiamo la parola chiave ‘let’ per dichiarare una variabile. Una variabile esistente può essere dichiarata di nuovo, dopodiché “oscura” la variabile esistente. A differenza di molti altri linguaggi, il valore di una variabile non può essere cambiato facilmente:
Per contrassegnare il valore di una variabile come successivamente modificabile, Rust si serve della parola chiave ‘mut’. Il valore di una variabile dichiarata con ‘mut’ può essere modificato:
Con la parola chiave ‘const’ viene generata una costante. Il valore di una costante di Rust deve essere noto al momento della compilazione. Anche il tipo deve essere specificato esplicitamente:
Il valore di una costante non può essere modificato; una costante non può nemmeno essere dichiarata ‘mut’. Inoltre, una costante non può essere dichiarata nuovamente:
Concetto di proprietà
Una delle caratteristiche decisive di Rust è il concetto di proprietà (in inglese: “ownership”). La proprietà è strettamente legata al valore delle variabili, alla loro durata di vita e alla gestione della memoria degli oggetti in memoria dello heap. Quando una variabile si allontana dal campo di applicazione (in inglese: “scope”), il suo valore viene distrutto e la memoria rilasciata. Rust può quindi fare a meno di una “garbage collection”, di conseguenza può garantire prestazioni elevate.
Ogni valore appartiene a una variabile, cioè al proprietario. Ci può essere un solo proprietario per ciascun valore. Se il proprietario passa ad altri il valore, allora non è più il proprietario:
Occorre prestare particolare attenzione nella definizione delle funzioni: se una variabile viene passata a una funzione, il proprietario del valore cambia. La variabile non può essere riutilizzata dopo l’apertura della funzione. Qui, Rust usa un trucco: invece di passare il valore alla funzione stessa, viene dichiarato un riferimento con il simbolo della e commerciale (&). Ciò consente di “prendere in prestito” il valore di una variabile. Di seguito un esempio:
Strutture di controllo
Una proprietà fondamentale della programmazione è quella di rendere il flusso del programma non lineare. Un programma può diramarsi e i componenti del programma possono essere eseguiti più di una volta. È solo attraverso questa variabilità che un programma diventa veramente utile.
Rust ha le strutture di controllo disponibili nella maggior parte dei linguaggi di programmazione, tra cui i costrutti dei cicli ‘for’ e ‘while’, così come le ramificazioni tramite ‘if’ ed ‘else’. Oltre a ciò, Rust ha alcune caratteristiche speciali. Il costrutto ‘match’ permette l’assegnazione di pattern, mentre l’istruzione ‘loop’ crea un ciclo infinito. Per permettere quest’ultimo, viene utilizzata un’istruzione ‘break’.
Cicli
L’esecuzione ripetuta di un blocco di codice per mezzo di cicli (indicati in inglese come “loop”) è anche nota come “iterazione”. Spesso l’iterazione viene eseguita sugli elementi di un contenitore. Come Python, Rust conosce il concetto di “iteratore”. Un iteratore estrae l’accesso successivo agli elementi di un contenitore. Facciamo un esempio:
Ora, cosa fare se si vuole scrivere un ciclo ‘for’ nello stile C/C++ o Java? Volete dunque specificare un numero iniziale e un numero finale e passare in rassegna tutti i valori intermedi. In tal caso, esiste l’oggetto “range” in Rust, proprio come in Python. Questo a sua volta crea un iteratore su cui opera la parola chiave ‘for’:
Un ciclo ‘while’ funziona in Rust come nella maggior parte degli altri linguaggi. Viene impostata una condizione e il corpo del ciclo viene eseguito finché la condizione è vera:
Per tutti i linguaggi di programmazione è possibile creare un ciclo infinito con ‘while’. Normalmente si tratta di un errore, ma ci sono anche casi d’uso che lo richiedono. Rust ricorre all’istruzione “loop” per questi casi:
In entrambi i casi può essere utilizzata la parola chiave ‘break’ per uscire dal ciclo.
Ramificazioni
Anche la ramificazione con ‘if’ ed ‘else’ funziona in Rust esattamente come in altri linguaggi simili:
Particolarmente interessante è la parola chiave ‘match’, che ha una funzione simile all’istruzione ‘switch’ di altri linguaggi. Per capire come viene utilizzata, consultate la funzione simbolo_ carte () nella sezione “Tipi di dati compositi” (vedi sotto).
Funzioni, procedure e metodi
Nella maggior parte dei linguaggi di programmazione le funzioni sono l’elemento base della programmazione modulare. Le funzioni sono definite in Rust con la parola chiave ‘fn’. Non viene fatta una distinzione rigorosa tra i relativi concetti di funzione e procedura. Entrambi sono definiti in modi quasi identici.
Una funzione in senso stretto restituisce un valore. Come molti altri linguaggi di programmazione, anche Rust conosce le procedure, cioè le funzioni che non restituiscono alcun valore. L’unica restrizione fissa è che il tipo di ritorno di una funzione deve essere esplicitamente specificato. Se non viene specificato alcun tipo di ritorno, la funzione non può restituire un valore; allora viene definita come una procedura.
Oltre alle funzioni e alle procedure, Rust conosce anche i metodi noti dalla programmazione orientata agli oggetti. Un metodo è una funzione legata a una struttura di dati. Come in Python, i metodi in Rust sono definiti con il primo parametro ‘self’. Un metodo viene chiamato secondo il consueto schema oggetto.metodo(). Di seguito un esempio del metodo superficie(), legato a una struttura dati ‘struct’:
Tipi di dati e strutture di dati
Rust è un linguaggio tipizzato staticamente. A differenza dei linguaggi dinamici Python, Ruby, PHP e JavaScript, Rust richiede che il tipo di ogni variabile sia noto al momento della compilazione.
Tipi di dati elementari
Come la maggior parte dei linguaggi di programmazione superiori, Rust conosce alcuni tipi di dati elementari (“primitives”). Le istanze dei tipi di dati elementari sono assegnate sulla memoria dello stack, che è particolarmente performante. Inoltre, i valori dei tipi di dati elementari possono essere definiti con la sintassi “literal”. Ciò significa che i valori possono essere semplicemente scritti.
Tipo di dato | Spiegazione | Annotazione del tipo |
---|---|---|
Integer | Numero intero | i8, u8, ecc. |
Floating point | Numero in virgola mobile | f64, f32 |
Boolean | Valore di verità | bool |
Character | Singolo carattere Unicode | char |
String | Catena di caratteri Unicode | str |
Sebbene Rust sia un linguaggio tipizzato staticamente, non sempre il tipo di valore deve essere dichiarato esplicitamente. In molti casi, il tipo può essere derivato dal compilatore nel contesto (“type inferencer”). In alternativa, il tipo è esplicitamente specificato dall’annotazione del tipo. In alcuni casi, quest’ultimo è obbligatorio:
- Il tipo di ritorno di una funzione deve essere sempre specificato esplicitamente.
- Il tipo di costante deve essere sempre specificato esplicitamente.
- Le stringhe literal devono essere trattate in modo speciale in modo che la loro dimensione sia nota al momento della compilazione.
Di seguito alcuni esempi illustrativi di istanziazione di tipi di dati elementari con sintassi literal:
Tipi di dati compositi
I tipi di dati elementari mappano i singoli valori, mentre i tipi di dati compositi raggruppano diversi valori. Rust fornisce al programmatore una manciata di tipi di dati compositi.
Le istanze dei tipi di dati compositi sono assegnate sullo stack come le istanze dei tipi di dati elementari. Per poterlo fare, le istanze devono avere una dimensione fissa. Ciò significa anche che non possono essere modificati arbitrariamente dopo l’istanziazione. Di seguito un riepilogo dei più importanti tipi di dati compositi in Rust:
Tipo di dato | Spiegazione | Tipo di elemento | Sintassi literal |
---|---|---|---|
Array | Elenco di più valori | Stesso tipo | [a1, a2, a3] |
Tuple | Disposizione di più valori | Qualsiasi tipo | (t1, t2) |
Struct | Raggruppamento di più valori nominati | Qualsiasi tipo | – |
Enum | Enumerazione | Qualsiasi tipo | – |
Vediamo ora da vicino una struttura di dati ‘struct’. Definiamo una persona con tre campi nominati:
Per rappresentare una persona concreta, istanziamo ‘struct’:
Una ‘enum’ (abbreviazione di “enumeration”, enumerazione) rappresenta le possibili varianti di una proprietà. Lo illustriamo qui con un esempio dei quattro possibili semi di una carta da gioco:
Rust conosce la parola chiave ‘match’ per “pattern matching”. La funzionalità è paragonabile all’istruzione ‘switch’ di altri linguaggi. Di seguito un esempio:
Una tupla è una disposizione di diversi valori, i quali possono essere di tipo diverso. I singoli valori della tupla possono essere assegnati a diverse variabili attraverso la destrutturazione. Se uno dei valori non è necessario, l’underscore (_) viene utilizzato come segnaposto, come di consueto in Haskell, Python e JavaScript. Di seguito un esempio:
Poiché i valori delle tuple sono ordinati, è possibile accedervi anche tramite un indice numerico. L’indicizzazione non avviene tra parentesi quadre, ma utilizzando una sintassi a punti. Nella maggior parte dei casi, la destrutturazione dovrebbe portare a una maggiore leggibilità del codice:
Apprendere i costrutti di programmazione in Rust
Strutture dati dinamiche
I tipi di dati compositi già introdotti hanno in comune il fatto che le loro istanze sono assegnate sullo stack. La libreria standard di Rust contiene anche una serie di strutture di dati dinamici di uso comune. Le istanze di queste strutture dati sono assegnate sullo heap. Ciò significa che la dimensione delle istanze può essere modificata in seguito. Di seguito un breve riepilogo delle strutture dati dinamiche usate frequentemente:
Tipo di dati | Spiegazione |
---|---|
Vector | Elenco dinamico di più valori dello stesso tipo |
String | Successione dinamica di lettere Unicode |
HashMap | Assegnazione dinamica di coppie di valori chiave |
Di seguito un esempio di un vettore dinamico crescente:
Programmazione orientata agli oggetti (OOP) in Rust
A differenza di linguaggi come C++ e Java, Rust non conosce il concetto di classi. Tuttavia, è possibile programmare secondo la metodologia OOP. La base è costituita dai tipi di dati già presentati. Soprattutto il tipo ‘struct’ può essere usato per definire la struttura degli oggetti.
In più, in Rust esistono i ‘traits’, cioè le caratteristiche. Un trait raggruppa un insieme di metodi che possono poi essere implementati con qualsiasi tipo. Un trait comprende le dichiarazioni di metodo, ma può anche contenere delle implementazioni. Concettualmente, un trait si colloca a metà strada tra un’interfaccia Java e una classe base astratta.
Un trait esistente può essere implementato da diversi tipi. Inoltre, un tipo può implementare diversi traits. Rust permette quindi la composizione di funzionalità per diversi tipi senza dover ereditare da un’interfaccia comune.
Metaprogrammazione
Come molti altri linguaggi di programmazione, Rust permette di scrivere codice per la metaprogrammazione. Si tratta di codice che genera ulteriore codice. In Rust questo include da un lato le “macro” che potreste conoscere da C/C++. Le macro terminano con un punto esclamativo (!); la macro ‘println!’ per l’output di testo sulla riga di comando è già stata menzionata più volte in questo articolo.
D’altra parte, Rust conosce anche i “generics”, i quali permettono di scrivere codice astraibile in diversi tipi. I generics sono all’incirca paragonabili ai modelli in C++ o agli omonimi generics in Java. Un generic spesso usato in Rust è ‘Option<T>‘, che astrae la dualità ‘None’/’Some(T)’ per qualsiasi tipo ‘T’.
Rust ha il potenziale per sostituire i favoriti di lunga data C e C++ come linguaggio di programmazione di sistema.