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.
fn main() { let y = 6; }
main
contenente una dichiarazioneAnche 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.