00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

Parametri delle Funzioni in Rust

In Rust, il modo in cui si passano i parametri a una funzione è strettamente legato al sistema di ownership e borrowing. Capire le diverse modalita di passaggio dei parametri è essenziale per scrivere codice corretto e performante. Ogni scelta ha implicazioni su chi possiede il dato e su cosa la funzione può fare con esso.

Passaggio per valore

Quando un parametro viene passato per valore, la funzione riceve una copia del dato (se il tipo implementa Copy) oppure ne acquisisce l’ownership (move).

Tipi Copy (stack)

I tipi primitivi come interi, float, bool e char implementano il trait Copy. Vengono copiati automaticamente.

fn raddoppia(n: i32) -> i32 {
    n * 2
}

fn main() {
    let x = 10;
    let y = raddoppia(x);
    println!("x = {x}, y = {y}"); // x è ancora utilizzabile
}

Tipi non-Copy (move)

I tipi che allocano memoria sullo heap, come String e Vec, non implementano Copy. Passarli per valore trasferisce l’ownership.

fn stampa_lunghezza(s: String) {
    println!("'{}' ha {} caratteri", s, s.len());
    // s viene deallocato qui (drop)
}

fn main() {
    let nome = String::from("Rust");
    stampa_lunghezza(nome);
    // Errore! nome non è più valido dopo il move:
    // println!("{nome}");
}

Passaggio per riferimento immutabile (&T)

Per evitare il trasferimento di ownership, si può passare un riferimento immutabile con &. La funzione può leggere il dato ma non modificarlo.

fn stampa_lunghezza(s: &String) {
    println!("'{}' ha {} caratteri", s, s.len());
}

fn main() {
    let nome = String::from("Rust");
    stampa_lunghezza(&nome);
    println!("nome è ancora: {nome}"); // Funziona: ownership non trasferita
}

I riferimenti immutabili sono il modo più comune di passare dati a funzioni che necessitano solo di leggerli. Si possono avere multipli riferimenti immutabili allo stesso dato contemporaneamente.

fn lunghezza(s: &str) -> usize {
    s.len()
}

fn contiene_vocale(s: &str) -> bool {
    s.chars().any(|c| "aeiouAEIOU".contains(c))
}

fn main() {
    let parola = String::from("ciao");
    let r1 = &parola;
    let r2 = &parola;

    println!("Lunghezza: {}", lunghezza(r1));
    println!("Ha vocali: {}", contiene_vocale(r2));
}

Passaggio per riferimento mutabile (&mut T)

Un riferimento mutabile permette alla funzione di modificare il dato. Solo un riferimento mutabile alla volta può esistere per ogni dato.

fn aggiungi_esclamazione(s: &mut String) {
    s.push('!');
}

fn raddoppia_vettore(v: &mut Vec<i32>) {
    let len = v.len();
    for i in 0..len {
        v[i] *= 2;
    }
}

fn main() {
    let mut saluto = String::from("Ciao");
    aggiungi_esclamazione(&mut saluto);
    println!("{saluto}"); // Ciao!

    let mut numeri = vec![1, 2, 3, 4, 5];
    raddoppia_vettore(&mut numeri);
    println!("{:?}", numeri); // [2, 4, 6, 8, 10]
}

Trasferimento di ownership

Passare un valore per ownership è utile quando la funzione deve diventare il nuovo proprietario del dato, ad esempio per conservarlo in una struttura dati.

fn aggiungi_a_lista(mut lista: Vec<String>, elemento: String) -> Vec<String> {
    lista.push(elemento);
    lista
}

fn main() {
    let nomi = vec![String::from("Alice")];
    let nomi = aggiungi_a_lista(nomi, String::from("Bob"));
    println!("{:?}", nomi); // ["Alice", "Bob"]
}

Parametri slice

Le slice sono il modo idiomatico per passare porzioni di dati. Sono preferibili ai riferimenti a tipi specifici perche sono piu flessibili.

// Preferisci &str a &String
fn conta_parole(testo: &str) -> usize {
    testo.split_whitespace().count()
}

// Preferisci &[T] a &Vec<T>
fn somma(numeri: &[i32]) -> i32 {
    numeri.iter().sum()
}

fn main() {
    let frase = String::from("Rust è fantastico");
    println!("Parole: {}", conta_parole(&frase));

    // Funziona anche con literal &str
    println!("Parole: {}", conta_parole("Ciao mondo"));

    let valori = vec![1, 2, 3, 4, 5];
    println!("Somma: {}", somma(&valori));

    // Funziona anche con array
    let arr = [10, 20, 30];
    println!("Somma array: {}", somma(&arr));
}

Parametri generici

I generics permettono di scrivere funzioni che operano su tipi diversi. Il tipo generico viene indicato tra parentesi angolari <T>.

fn massimo<T: PartialOrd>(a: T, b: T) -> T {
    if a >= b {
        a
    } else {
        b
    }
}

fn main() {
    println!("Max intero: {}", massimo(10, 20));
    println!("Max float: {}", massimo(3.14, 2.71));
    println!("Max char: {}", massimo('a', 'z'));
}

I generics possono avere vincoli (trait bound) che specificano quali capacita il tipo deve avere:

fn stampa_lista<T: std::fmt::Display>(elementi: &[T]) {
    for (i, elem) in elementi.iter().enumerate() {
        println!("  {i}. {elem}");
    }
}

fn main() {
    stampa_lista(&[1, 2, 3]);
    stampa_lista(&["mela", "pera", "banana"]);
}

Clausole where

Quando i vincoli sui generics diventano complessi, la clausola where rende la firma più leggibile.

fn confronta_e_stampa<T>(a: &T, b: &T)
where
    T: PartialOrd + std::fmt::Display,
{
    if a > b {
        println!("{a} è maggiore di {b}");
    } else if a < b {
        println!("{a} è minore di {b}");
    } else {
        println!("{a} è uguale a {b}");
    }
}

fn cerca_e_conta<T, U>(collezione: &[T], criterio: U) -> usize
where
    T: PartialEq<U>,
    U: Copy,
{
    collezione.iter().filter(|&elem| *elem == criterio).count()
}

fn main() {
    confronta_e_stampa(&10, &20);
    confronta_e_stampa(&"abc", &"def");

    let numeri = [1, 3, 5, 3, 7, 3, 9];
    println!("Il 3 appare {} volte", cerca_e_conta(&numeri, 3));
}

Riepilogo delle modalita di passaggio

Modalita Sintassi Ownership Modifica Uso tipico
Per valore (copy) f(x: i32) Copia Si (locale) Tipi primitivi
Per valore (move) f(x: String) Trasferita Si Consumare il dato
Riferimento immutabile f(x: &T) Prestito No Solo lettura
Riferimento mutabile f(x: &mut T) Prestito Si Modificare il dato
Slice f(x: &[T]) Prestito No Porzioni di dati

Conclusione

La scelta del tipo di parametro in Rust non è solo una questione di stile: ha implicazioni dirette sulla sicurezza della memoria e sulle prestazioni. Usa i riferimenti immutabili come scelta predefinita, i riferimenti mutabili quando devi modificare i dati, e il passaggio per valore quando l’ownership deve essere trasferita. I generics e le clausole where permettono di scrivere codice flessibile che mantiene la sicurezza dei tipi a tempo di compilazione.