Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Funzioni

Le funzioni sono molto diffuse nel codice di Rust. Hai già visto una delle funzioni più importanti del linguaggio: la funzione main, che è il punto di ingresso di molti programmi. Hai anche visto la parola chiave fn, che ti permette di dichiarare nuove funzioni.

Il codice Rust utilizza lo snake case come stile convenzionale per i nomi di funzioni e variabili, in cui tutte le lettere sono minuscole e i trattini bassi separano le parole. Ecco un programma che contiene un esempio di definizione di funzione:

File: src/main.rs

fn main() {
    println!("Hello, world!");

    altra_funzione();
}

fn altra_funzione() {
    println!("Un'altra funzione.");
}

In Rust definiamo una funzione inserendo fn seguito dal nome della funzione e da una serie di parentesi tonde. Le parentesi graffe indicano al compilatore dove inizia e finisce il corpo della funzione.

Possiamo chiamare qualsiasi funzione che abbiamo definito inserendo il suo nome seguito da una serie di parentesi tonde. Poiché altra_funzione è definita nel programma, può essere chiamata dall’interno della funzione main. Nota che abbiamo definito altra_funzione dopo la funzione main nel codice sorgente; avremmo potuto definirla anche prima. A Rust non interessa dove definisci le tue funzioni, ma solo che siano definite in una parte del codice che sia “visibile”, in scope, al chiamante.

Cominciamo un nuovo progetto binario chiamato funzioni per esplorare ulteriormente le funzioni. Inserisci l’esempio altra_funzione in src/main.rs ed eseguilo. Dovresti vedere il seguente output:

$ cargo run
   Compiling funzioni v0.1.0 (file:///progetti/funzioni)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.37s
     Running `target/debug/funzioni`
Hello, world!
Un'altra funzione.

Le righe vengono eseguite nell’ordine in cui appaiono nella funzione main. Prima viene stampato il messaggio “Hello, world!”, poi viene chiamata altra_funzione e viene stampato il suo messaggio.

Parametri

Possiamo definire le funzioni in modo che abbiano dei parametri, ovvero delle variabili speciali che fanno parte della firma di una funzione. Quando una funzione ha dei parametri, puoi fornirle dei valori concreti per questi parametri. Tecnicamente, i valori concreti sono chiamati argomenti, ma in una conversazione informale si tende a usare le parole parametro e argomento in modo intercambiabile, sia per le variabili nella definizione di una funzione che per i valori concreti passati quando si chiama una funzione.

In questa versione di altra_funzione aggiungiamo un parametro:

File: src/main.rs

fn main() {
    altra_funzione(5);
}

fn altra_funzione(x: i32) {
    println!("Il valore di x è: {x}");
}

Prova a eseguire questo programma; dovresti ottenere il seguente risultato:

$ cargo run
   Compiling funzioni v0.1.0 (file:///progetti/funzioni)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.36s
     Running `target/debug/funzioni`
Il valore di x è: 5

La dichiarazione di altra_funzione ha un parametro chiamato x. Il type di x è specificato come i32. Quando passiamo 5 ad altra_funzione, la macro println! mette 5 nel punto in cui si trovava la coppia di parentesi graffe contenente x nella stringa di formato.

Nelle firme delle funzioni è obbligatorio dichiarare il type di ogni parametro. Si tratta di una decisione deliberata nel design di Rust: richiedere le annotazioni sul type nelle definizioni delle funzioni significa che il compilatore non ha quasi mai bisogno che tu le usi in altre parti del codice per capire a quale type ti riferisci. In questo modo il compilatore potrà anche dare messaggi di errore più utili se sa quali type si aspetta la funzione.

Quando definisci più parametri, separa le dichiarazioni dei parametri con delle virgole, in questo modo:

File: src/main.rs

fn main() {
    stampa_unita_misura(5, 'h');
}

fn stampa_unita_misura(valore: i32, unita_misura: char) {
    println!("La misura è : {valore}{unita_misura}");
}

Questo esempio crea una funzione chiamata stampa_unita_misura con due parametri. Il primo parametro si chiama valore ed è un i32. Il secondo si chiama unita_misura ed è di type char. La funzione stampa quindi un testo contenente sia il valore che unita_misura.

Eseguiamo il codice. Sostituisci il codice attualmente presente nel file src/main.rs_ del tuo progetto funzioni con l’esempio precedente ed eseguilo con cargo run:

$ cargo run
   Compiling funzioni v0.1.0 (file:///progetti/funzioni)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/funzioni`
La misura è : 5h

Poiché abbiamo chiamato la funzione con 5 come valore per valore e 'h' come valore per unita_misura, l’output del programma contiene questi valori.

Dichiarazioni ed espressioni

I corpi delle funzioni sono costituiti da una serie di dichiarazioni che possono eventualmete terminare con un’espressione. Finora le funzioni che abbiamo trattato non hanno incluso un’espressione finale, ma hai visto un’espressione come parte di una dichiarazione. Poiché Rust è un linguaggio basato sulle espressioni, questa è una distinzione importante da capire. Altri linguaggi non hanno le stesse distinzioni, quindi vediamo cosa sono le dichiarazioni e le espressioni e come le loro differenze influenzano il corpo delle funzioni.

  • Le dichiarazioni sono istruzioni che eseguono un’azione e non restituiscono un valore.
  • Le espressioni vengono valutate e restituiscono un valore risultante.

Vediamo alcuni esempi.

In realtà abbiamo già usato le dichiarazioni e le espressioni. Creare una variabile e assegnarle un valore con la parola chiave let è una dichiarazione. Nel Listato 3-1, let y = 6; è una dichiarazione.

File: src/main.rs
fn main() {
    let y = 6;
}
Listato 3-1: La funzione main contenente una dichiarazione

Anche la definizione di una funzione è una dichiarazione; l’intero esempio precedente è, di per sé, una dichiarazione. (Come vedremo più avanti, però, chiamare una funzione non è una dichiarazione)

Le dichiarazioni non restituiscono valori. Pertanto, non puoi assegnare una dichiarazione let a un’altra variabile, come cerca di fare il codice seguente; otterrai un errore:

File: src/main.rs

fn main() {
    let x = (let y = 6);
}

Quando esegui questo programma, l’errore che otterrai è simile a questo:

$ cargo run
   Compiling funzioni v0.1.0 (file:///progetti/funzioni)
error: expected expression, found `let` statement
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^
  |
  = note: only supported directly in conditions of `if` and `while` expressions

warning: unnecessary parentheses around assigned value
 --> src/main.rs:2:13
  |
2 |     let x = (let y = 6);
  |             ^         ^
  |
  = note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
  |
2 -     let x = (let y = 6);
2 +     let x = let y = 6;
  |

warning: `funzioni` (bin "funzioni") generated 1 warning
error: could not compile `funzioni` (bin "funzioni") due to 1 previous error; 1 warning emitted

La dichiarazione let y = 6 non restituisce un valore, quindi non c’è nulla a cui x possa legarsi. Questo è diverso da ciò che accade in altri linguaggi, come C e Ruby, dove l’assegnazione restituisce il valore dell’assegnazione. In questi linguaggi, puoi scrivere x = y = 6 e far sì che sia x che y abbiano il valore 6; questo non è il caso di Rust.

Le espressioni che valutate restituiscono un valore costituiscono la maggior parte del resto del codice che scriverai in Rust. Considera un’operazione matematica, come 5 + 6, che è un’espressione che restituisce il valore 11. Le espressioni possono far parte di dichiarazioni: nel Listato 3-1, il 6 nella dichiarazione let y = 6; è un’espressione che valuta il valore 6. Chiamare una funzione è un’espressione. Chiamare una macro è un’espressione. Pure definire tramite parentesi graffe un nuovo scope ad esempio:

File: src/main.rs

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("Il valore di y è: {y}");
}

Questa espressione:

{
    let x = 3;
    x + 1
}

è un blocco che, in questo caso, valuta 4. Questo valore viene legato a y come parte dell’istruzione let. Nota che la riga x + 1 non ha un punto e virgola alla fine, il che è diverso dalla maggior parte delle righe che hai visto finora. Le espressioni non includono il punto e virgola finale. Se aggiungi un punto e virgola alla fine di un’espressione, la trasformi in una dichiarazione e quindi non restituirà un valore. Tienilo a mente mentre leggi il prossimo paragrafo sui volori di ritorno delle funzioni e le espressioni.

Funzioni con valori di ritorno

Le funzioni possono restituire dei valori al codice che le chiama. Non assegnamo un nome ai valori di ritorno, ma dobbiamo esplicitarne il type dopo una freccia (->). In Rust, il valore di ritorno della funzione è sinonimo del valore dell’espressione finale nel blocco del corpo della funzione. Puoi far ritornare un valore anche in anticipo alla funzione usando la parola chiave return e specificando un valore, ma la maggior parte delle funzioni restituisce l’ultima espressione in modo implicito. Ecco un esempio di funzione che restituisce un valore:

File: src/main.rs

fn cinque() -> i32 {
    5
}

fn main() {
    let x = cinque();

    println!("Il valore di x è: {x}");
}

Non ci sono chiamate di funzione, macro o dichiarazioni let nella funzione cinque, ma solo il numero 5 da solo. Si tratta di una funzione perfettamente valida in Rust. Nota che anche il type di ritorno della funzione è specificato come -> i32. Prova a eseguire questo codice; l’output dovrebbe essere simile a questo:

$ cargo run
   Compiling funzioni v0.1.0 (file:///progetti/funzioni)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s
     Running `target/debug/funzioni`
Il valore di x è: 5

Il 5 in cinque è il valore di ritorno della funzione, motivo per cui il type di ritorno è i32. Esaminiamo il tutto più in dettaglio. Ci sono due elementi importanti: innanzitutto, la riga let x = cinque(); mostra che stiamo utilizzando il valore di ritorno di una funzione per inizializzare una variabile. Poiché la funzione cinque restituisce un 5, questa riga è uguale alla seguente:

#![allow(unused)]
fn main() {
let x = 5;
}

In secondo luogo, la funzione cinque non ha parametri e definisce il type del valore di ritorno, ma il corpo della funzione è un solitario 5 senza punto e virgola perché è un’espressione il cui valore vogliamo restituire.

Vediamo un altro esempio:

File: src/main.rs

fn main() {
    let x = piu_uno(5);

    println!("Il valore di x è: {x}");
}

fn piu_uno(x: i32) -> i32 {
    x + 1
}

Eseguendo questo codice verrà stampato Il valore di x è: 6. Ma se inseriamo un punto e virgola alla fine della riga contenente x + 1, trasformandola da espressione a dichiarazione, otterremo un errore:

File: src/main.rs

fn main() {
    let x = piu_uno(5);

    println!("Il valore di x è: {x}");
}

fn piu_uno(x: i32) -> i32 {
    x + 1;
}

La compilazione di questo codice produce un errore, come segue:

$ cargo run
   Compiling funzioni v0.1.0 (file:///progetti/funzioni)
error[E0308]: mismatched types
 --> src/main.rs:7:23
  |
7 | fn piu_uno(x: i32) -> i32 {
  |    -------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: remove this semicolon to return this value

For more information about this error, try `rustc --explain E0308`.
error: could not compile `funzioni` (bin "funzioni") due to 1 previous error

Il messaggio di errore principale, mismatched types (type incompatibili), rivela il problema principale di questo codice. La definizione della funzione piu_uno dice che restituirà un i32, ma le dichiarazioni non risultano in un valore, restituendo un (), il type unit. Pertanto, non viene restituito nulla, il che contraddice la definizione della funzione e provoca un errore. In questo output, Rust fornisce un messaggio che può aiutare a correggere questo problema: suggerisce di rimuovere il punto e virgola, che risolverebbe l’errore.