Le funzioni in Rust

Le funzioni sono blocchi di codice scritte fuori da quella principale che possono essere richiamati più volte senza dovere scrivere le istruzioni al loro interno più volte all’interno del programma. In Rust possiamo dichiarare una funzione con la parola chiave fn e la nominiamo usando parole minuscole separate dal trattino basso _.

Dopo il nome inseriamo le parentesi tonde che contengono i parametri e le parentesi graffe per le istruzioni. Possiamo inserire il tipo di ritorno dopo il simbolo -> prima delle graffe.

Possiamo creare una funzione anonima inserendo del codice dentro le parentesi graffe e senza nominarla. Ovviamente funzionerà soltanto una volta nell’ambito in cui viene scritta e non potrà essere riutilizzata.

Possiamo anche creare una variabile e scrivere una funzione che ritorna un valore al suo interno. Non è necessario usare return, come in altri linguaggi, ma scrivere semplicemente come istruzione finale il valore da ritornare

La funzione principale in Rust è main. Le funzioni fuori dal file main.rs devo essere rese pubbliche utilizzando la parola pub all’inizio. Ecco alcuni esempi:

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

    println!("Il valore di x è: {}", x);
}
fn main() {
    let sum = add(3,5);

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

fn add(n_1: i32, n_2: i32) -> i32 {
    n_1 + n_2
}

Ownership e borrowing nelle funzioni

Quando si scrivono delle funzioni bisogna tenere conto delle regole di ownership e di borrowing di Rust. Ne parliamo brevemente qui ma verranno pubblicati degli articoli di approfondimento su questo argomento.

Ogni valore conservato in memoria può avere soltanto un proprietario in Rust, cioè un’unica variabile alla volta può accedere all’indirizzo di memoria di quel valore. Se creiamo una nuova variabile con lo stesso valore della prima, questa viene eliminata automaticamente. Inoltre, quando il proprietario esce dal suo ambito, cioè quando il blocco di codice che contiene una variabile si chiude, anche il suo valore viene eliminato automaticamente. Questo non vale per i dati statici, come stringhe letterali e numeri, i cui valori vengono automaticamente clonati perché non occupano molta memoria; però anche questi valori vengono eliminati quando i proprietari escono dal loro scope.

Inoltre, se modifichiamo il valore di una variabile mutabile, il vecchio valore viene automaticamente eliminato: abbiamo un passaggio di proprietà, il proprietario di un valore passa al valore nuovo. Possiamo evitare tutto ciò usando la funzione .clone() sulla variabile che passiamo come parametro.

Inoltre, quando ritorniamo un valore dalla funzione, gli stiamo assegnando un proprietario. Ad esempio, la funzione add usata sopra passa la somma alla variabile x.

Possiamo evitare che il passaggio di proprietà quando passiamo il valore di una variabile alla funzione usando il carattere & che indica a Rust che vogliamo passare il riferimento del valore.

fn main() {
    let s = String::from("Hello");

    do_something(&s);

    println!("{s}")
}

//Possiamo passare direttamente il valore di String e non tutto l'oggetto
fn do_something(s: &str) {
    todo!()
}

In questi casi possiamo creare tutti i riferimenti che vogliamo. Nel caso di variabili mutabili, possiamo avere soltanto un riferimento all’interno del proprio ambito. Questo perché se una variabile modificasse il valore, la seconda variabile leggerebbe le modifiche e non il valore originale. Nel caso di programmi dove vengono fatti più lavori contemporaneamente sullo stesso riferimento, come il multithread, rischiamo di avere problemi.

fn main() {
    let mut s = String::from("Hello");

    let s2 = &mut s;

    *s2 = String::from("World"); //In realtà abbiamo modificato s.

    println!("{s}");
}