Rust - from Mozilla

Funzioni         

Le funzioni sono una parte fondamentale di questo linguaggio. D'altronde ogni programma ne contiene almeno una, il main che fa da entry point.
Una funzione è introdotta dalla keyword fn. Segue il nome e la lista dei parametri opzionali, ovvero potrebbero anche non essercene. Se invece sono presenti, essi devono veder specificato il proprio tipo e, se più di uno, devono essere separati tramite virgola. Quindi, la definizione base è la seguente:

fn nomefunzione (argomento1 : tipo, argomento2 : tipo,....)
{
  codice della funzione
}

  Esempio 11.1
1
2
3
4
5
6
7
8
9
fn somma(x1 : i32, x2 : i32)
{
  println!("{}", x1 + x2);
}

fn main()
{
  somma(5,6);
}

Il programma è molto semplice, come si vede la definizione della funzione è in linea con quanto detto in precedenza. La funzione viene richiamata alla riga 8 col suo nome passando i due parametri richiesti. Non c'è limite al numero di parametri ed ovviamente i valori passati devono essere tutti quelli richiesti e di tipo compatibile con quanto dichiarato nella signature della funzione, la sua dichiarazione, in soldoni se non volete ottenere dal compilatore un errore che recita:
this function takes 2 parameters but 1 parameter was supplied [E0061]
oppure
mismatched types [E0308]
Va detto che quando definite gli argomenti di una funzione dovete specificarne il tipo. Questa è una scelta (sarebbe anche tecnicamente possibile usare l'inferenza ma in questo caso si deciso per una strada diversa) che forza una best practice, almeno così pare, in fase di codifica.

Se la funzione deve restituire un valore allora la definizione cambia come segue:

fn nomefunzione (argomento1 : tipo, argomento2 : tipo,....) -> tipo
{
  codice della funzione
}

La novità è evidenziata in rosso. In pratica l'operatore
-> seguito dal tipo indica cosa sarà restituito dalla funzione. In particolare, il valore è restituito dall'ultima istruzione.

  Esempio 11.2
1
2
3
4
5
6
7
8
9
10
11
12
fn somma(x1 : i32, x2 : i32) -> i32
{
  println!("{}", x1 + x2);
  x1 * x2
}

fn main()
{
  let mut y = 0;
  y = somma(3,4);
  println!("{}", y);
}

La riga 4 è quella che restituisce il valore che viene attribuito alla variabile y tramite l'assegnazione alla riga 10. Va notato che l'istruzione alla riga 4 termina senza il classico punto e virgola. Questo perchè altrimenti il compilatore non la considererebbe l'ultima istruzione e quindi non potrebbe restituire nulla. Ricordatevi di questo.

E' possibile definire la stessa funzione con, con lo stesso nome, in due blocchi diversi:

  Esempio 11.3
1
2
3
4
5
6
7
8
9
10
11
fn main()
{
  {
    fn fn01() { println!("a"); }
    fn01();
  }
  {
    fn fn01() { println!("b"); }
    fn01();
  }
}

Il primo blocco è definito dalla riga 3 alla 6, il secondo dalla 7 alla 10, in essi abbiamo definito un funzione di nome fn01 che si comporta in maniera diversa, nel senso che stampa un carattere diverso nelle due versioni. Da qui risulta anche che una funzione può essere chiamata solo nel blocco in cui è definita, per cui se volete averla a livello globale è bene che la mettiate al di fuori del vostro main. A questo proposito però bisogna stare attenti ad un possibile rischio che vediamo nel programma seguente:

  Esempio 11.4
1
2
3
4
5
6
7
8
9
10
11
fn saluta()
{
  println!("Bello");
}

fn main()
{
  saluta();
  fn saluta() { println!("brutto"); }
  saluta();
}

L'output di questo programma è:

brutto
brutto

Questo vuol dire, ma il compilatore vi avvisa con un warning, che la funzione definita alla riga 9 oscura quella definita alla 1 che infatti non viene mai richiamata.
Un'altra cosa che risulta evidente è che la funzione è stata usata prima della sua definizione (riga 8). Per una funzione questo è legale per una variabile no.

I parametri così come li abbiamo visti nei precedenti esempi sono passati per valore. Questo significa che il loro valore viene passato alla funzione ma la copia, per così dire orgiinale, non viene in alcun modo modificata. Se vogliamo interagire direttamente coi valori del chiamante possiamo passare il riferimento e permettere quindi di manipolare la copia originale:

  Esempio 11.5
1
2
3
4
5
6
7
8
9
10
11
fn cambia(x: &mut i32)
{
  *x = 10;
}

fn main()
{
  let mut x = 0;
  cambia(&mut x);
  println!("{}", x);
}

Chi conosce il C/C++ troverà degli operatori noti che in Rust si comportano come in quei linguaggi:

&  che indica l'indirizzo di un certo elemento in memoria
*   che indica l'elemento che si trova ad un certo indirizzo

quindi l'istruzione alla riga 9 passa un indirizzo di memoria e quella alla 3 cambia il contenuto il quanto in quell'indirizzo. Anche il parametro, riga 1, per coerenza deve essere un indirizzo di memoria. output espresso alla riga 10 pertanto stamperà il valore assegnato alla riga 3, ovvero il numero 10.