Funzioni e Chiusure Avanzate
Questa sezione esplora alcune funzionalità avanzate legate a funzioni e chiusure, inclusi i puntatori a funzione e il ritorno di chiusure.
Puntatori a Funzione
Abbiamo parlato di come passare chiusure alle funzioni; puoi anche passare
funzioni normali a funzioni! Questa tecnica è utile quando vuoi passare una
funzione che hai già definito piuttosto che definire una nuova chiusura. Le
funzioni si convertono automaticamente al type fn
(con la f minuscola), da
non confondere con il trait Fn
delle chiusure. Il type fn
è chiamato
puntatore a funzione. Passare funzioni con puntatori a funzione ti permette di
utilizzare funzioni come argomenti per altre funzioni.
La sintassi per specificare che un parametro è un puntatore a funzione è simile
a quella delle chiusure, come mostrato nel Listato 20-28, dove abbiamo definito
una funzione più_uno
che aggiunge 1 al suo parametro. La funzione due_volte
prende due parametri: un puntatore a funzione verso qualsiasi funzione che
prende un parametro i32
e ritorna un i32
, e un valore i32
. La funzione
due_volte
chiama la funzione f
due volte, passando il valore arg
, poi
somma i risultati delle due chiamate. La funzione main
chiama due_volte
con
gli argomenti più_uno
e 5
.
fn più_uno(x: i32) -> i32 { x + 1 } fn due_volte(f: fn(i32) -> i32, arg: i32) -> i32 { f(arg) + f(arg) } fn main() { let risposta = due_volte(più_uno, 5); println!("La risposta è: {risposta}"); }
fn
per accettare un puntatore a funzione come argomentoQuesto codice stampa La risposta è: 12
. Specifichiamo che il parametro f
in
due_volte
è un fn
che prende un parametro di type i32
e ritorna un
i32
. Possiamo quindi chiamare f
nel corpo di due_volte
. In main
,
possiamo passare il nome della funzione più_uno
come primo argomento a
due_volte
.
A differenza delle chiusure, fn
è un type, non un trait, quindi
specifichiamo fn
come type del parametro direttamente piuttosto che
dichiarare un type generico con uno dei trait Fn
come vincolo.
I puntatori a funzione implementano tutti e tre i trait delle chiusure (Fn
,
FnMut
e FnOnce
), il che significa che puoi sempre passare un puntatore a
funzione come argomento a una funzione che si aspetta una chiusura. È meglio
scrivere funzioni usando un type generico e uno dei trait delle chiusure
così che le tue funzioni possano accettare sia funzioni che chiusure.
Detto questo, un esempio in cui potresti voler accettare solo fn
e non
chiusure è quando ti interfacci con codice esterno che non ha chiusure: le
funzioni in C possono accettare funzioni come argomenti, ma C non ha chiusure.
Come esempio di dove potresti usare sia una chiusura definita che una funzione
nominata, diamo un’occhiata all’uso del metodo map
fornito dal trait
Iterator
nella libreria standard. Per usare il metodo map
per trasformare un
vettore di numeri in un vettore di stringhe, potremmo usare una chiusura, come
nel Listato 20-29.
fn main() { let lista_di_numeri = vec![1, 2, 3]; let lista_di_stringhe: Vec<String> = lista_di_numeri.iter().map(|i| i.to_string()).collect(); }
map
per convertire numeri in stringheOppure potremmo nominare una funzione come argomento di map
al posto della
chiusura. Il Listato 20-30 mostra come sarebbe.
fn main() { let lista_di_numeri = vec![1, 2, 3]; let lista_di_stringhe: Vec<String> = lista_di_numeri.iter().map(ToString::to_string).collect(); }
String::to_string
con il metodo map
per convertire numeri in stringheNota che dobbiamo usare la sintassi completamente qualificata di cui abbiamo
parlato in “Trait Avanzati” perché ci sono
più funzioni con nome to_string
.
Qui usiamo la funzione to_string
definita nel trait ToString
, che la
libreria standard ha implementato per ogni type che implementa Display
.
Ricorda da “Valori di Enum” nel Capitolo 6 che il nome di ogni variante enum diventa anche una funzione inizializzatrice. Possiamo usare queste funzioni inizializzatrici come puntatori a funzione che implementano i trait delle chiusure, il che significa che possiamo specificare le funzioni inizializzatrici come argomenti per metodi che accettano chiusure, come nel Listato 20-31.
fn main() { enum Stato { Valore(u32), Stop, } let lista_stati: Vec<Stato> = (0u32..20).map(Stato::Valore).collect(); }
map
per creare un’istanza Stato
dai numeriQui creiamo istanze di Stato::Valore
usando ogni valore u32
nell’intervallo
su cui è chiamato map
, usando la funzione inizializzatrice di Stato::Valore
.
Alcuni preferiscono questo stile, altri preferiscono usare chiusure. Entrambi i
metodi si compilano allo stesso modo, quindi usa quello che trovi più chiaro.
Restituire Chiusure
Le chiusure sono rappresentate da trait, il che significa che non puoi
restituire direttamente una chiusura. Nella maggior parte dei casi in cui
potresti voler ritornare un trait, puoi invece usare il type concreto che
implementa il trait come type di ritorno della funzione. Tuttavia, di solito
non puoi fare questo con le chiusure perché non hanno un type concreto
restituibile; per esempio, non puoi usare il puntatore a funzione fn
come
type di ritorno se la chiusura cattura qualche valore dal suo scope.
Al contrario, normalmente userai la sintassi impl Trait
che abbiamo imparato
nel Capitolo 10. Puoi restituire qualsiasi tipo di funzione usando Fn
,
FnOnce
e FnMut
. Per esempio, il codice nel Listato 20-32 verrà compilato
senza problemi.
#![allow(unused)] fn main() { fn ritorna_chiusura() -> impl Fn(i32) -> i32 { |x| x + 1 } }
impl Trait
Tuttavia, come abbiamo notato in “Inferenza e Annotazione del Type delle Chiusure” nel Capitolo 13, ogni chiusura è anche il suo type distinto. Se ti serve lavorare con più funzioni che hanno la stessa firma ma implementazioni diverse, dovrai usare un oggetto trait per loro. Considera cosa succede se scrivi un codice come nel Listato 20-33.
fn main() {
let handlers = vec![ritorna_chiusura(), ritorna_chiusura_inizializzata(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn ritorna_chiusura() -> impl Fn(i32) -> i32 {
|x| x + 1
}
fn ritorna_chiusura_inizializzata(init: i32) -> impl Fn(i32) -> i32 {
move |x| x + init
}
Vec<T>
di chiusure definite tramite funzioni che restituiscono type impl Fn
Qui abbiamo due funzioni, ritorna_chiusura
e ritorna_chiusura_inizializzata
,
che entrambe ritornano impl Fn(i32) -> i32
. Nota che le chiusure restituite
sono diverse anche se implementano lo stesso type. Se provi a compilare, Rust
ti dice che non funziona:
$ cargo run
Compiling esempio-funzioni v0.1.0 (file:///progetti/esempio-funzioni)
error[E0308]: mismatched types
--> src/main.rs:2:45
|
2 | let handlers = vec![ritorna_chiusura(), ritorna_chiusura_inizializzata(123)];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9 | fn ritorna_chiusura() -> impl Fn(i32) -> i32 {
| ------------------- the expected opaque type
...
13 | fn ritorna_chiusura_inizializzata(init: i32) -> impl Fn(i32) -> i32 {
| ------------------- the found opaque type
|
= note: expected opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:9:26>)
found opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:13:49>)
= note: distinct uses of `impl Trait` result in different opaque types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `esempio-funzioni` (bin "esempio-funzioni") due to 1 previous error
Il messaggio di errore dice che ogni volta che ritorni un impl Trait
, Rust
crea un type opaco univoco, un type di cui non possiamo conoscere i
dettagli di come Rust l’ha costruito né sapere il type generato. Quindi anche
se queste funzioni ritornano chiusure che implementano lo stesso trait, i
type opachi che Rust genera sono diversi. (Questo è simile a come Rust genera
type concreti distinti per blocchi async diversi anche se hanno lo stesso
type di output, come abbiamo visto in “Lavorare con un Numero Qualsiasi di
Future” nel Capitolo 17.) Abbiamo già
visto una soluzione a questo problema: possiamo usare un oggetto trait, come
nel Listato 20-34.
fn main() { let handlers = vec![ritorna_chiusura(), ritorna_chiusura_inizializzata(123)]; for handler in handlers { let output = handler(5); println!("{output}"); } } fn ritorna_chiusura() -> Box<dyn Fn(i32) -> i32> { Box::new(|x| x + 1) } fn ritorna_chiusura_inizializzata(init: i32) -> Box<dyn Fn(i32) -> i32> { Box::new(move |x| x + init) }
Vec<T>
di chiusure definite tramite funzioni che ritornano Box<dyn Fn>
Questo codice si compila correttamente. Per più informazioni sugli oggetti trait, vedi la sezione “Usare gli Oggetti Trait per Astrarre Comportamenti Condivisi” nel Capitolo 18.
Passiamo ora a vedere le macro!