Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Metodi

I metodi (method) sono simili alle funzioni: le dichiariamo con la parola chiave fn e un nome, possono avere parametri e un valore di ritorno, e contengono del codice che viene eseguito quando il metodo viene chiamato da un’altra parte. Diversamente dalle funzioni, i metodi sono definiti nel contesto di una struct (o di un’enum o di un trait object, che tratteremo nel Capitolo 6 e Capitolo 18, rispettivamente), e il loro primo parametro è sempre self, che rappresenta l’istanza della struct su cui il metodo viene chiamato.

Sintassi dei Metodi

Trasformiamo la funzione area che prende un’istanza di Rettangolo come parametro rendendola invece un metodo definito sulla struct Rettangolo, come mostrato nel Listato 5-13.

File: src/main.rs
#[derive(Debug)]
struct Rettangolo {
    larghezza: u32,
    altezza: u32,
}

impl Rettangolo {
    fn area(&self) -> u32 {
        self.larghezza * self.altezza
    }
}

fn main() {
    let rettangolo1 = Rettangolo {
        larghezza: 30,
        altezza: 50,
    };

    println!(
        "L'area del rettangolo è di {} pixel quadrati.",
        rettangolo1.area()
    );
}
Listato 5-13: Definizione di un metodo area nella struct Rettangolo

Per definire la funzione nel contesto di Rettangolo, iniziamo un blocco impl (implementazione) per Rettangolo. Tutto ciò che sta dentro questo blocco impl sarà associato al type Rettangolo. Poi spostiamo la funzione area all’interno delle parentesi graffe dell’impl e cambiamo il primo (e in questo caso, unico) parametro in self nella firma e ovunque nel corpo. In main, dove chiamavamo la funzione area passando rettangolo1 come argomento, possiamo invece usare la sintassi dei metodi per chiamare il metodo area sull’istanza di Rettangolo. La sintassi del metodo va dopo un’istanza: aggiungiamo un punto seguito dal nome del metodo, parentesi tonde ed eventuali argomenti.

Nella firma di area usiamo &self invece di rettangolo: &Rettangolo. Il &self è in realtà l’abbreviazione di self: &Self. All’interno di un blocco impl, il type Self è un alias del type per cui il blocco impl è stato scritto. I metodi devono avere un parametro chiamato self di type Self come primo parametro, quindi Rust permette di abbreviare questo con soltanto il nome self nella prima posizione dei parametri. Nota che dobbiamo comunque usare & davanti alla forma abbreviata self per indicare che questo metodo prende in prestito l’istanza Self, esattamente come facevamo con rettangolo: &Rettangolo. I metodi possono prendere la ownership di self, prendere un reference immutabile a self, come abbiamo fatto qui, oppure prendere un reference mutabile a self, proprio come possono fare con qualsiasi altro parametro.

Qui abbiamo scelto &self per lo stesso motivo per cui abbiamo usato &Rettangolo nella versione precedente: non serve che prendiamo la ownership, vogliamo solo leggere i dati nella struct, non modificarli. Se volessimo modificare l’istanza su cui chiamiamo il metodo come parte di ciò che il metodo fa, useremmo &mut self come primo parametro. Avere un metodo che prende la ownership dell’istanza usando semplicemente self come primo parametro è raro; questa tecnica è solitamente usata quando il metodo trasforma self in qualcos’altro e si vuole impedire al chiamante di usare l’istanza originale dopo la trasformazione.

La ragione principale per usare i metodi invece delle funzioni, oltre a fornire la sintassi dei metodi e a non dover ripetere il type di self in ogni firma dei metodi, è per organizzazione. Abbiamo messo tutte le cose che possiamo fare con un’istanza di un type in un unico blocco impl invece di costringere chi dovrà in futuro leggere o manutenere il nostro codice a cercare le funzionalità di Rettangolo in vari posti nella libreria che forniamo.

Nota che possiamo scegliere di dare a un metodo lo stesso nome di uno dei campi della struct. Per esempio, possiamo definire un metodo su Rettangolo che si chiama anch’esso larghezza:

File: src/main.rs
#[derive(Debug)]
struct Rettangolo {
    larghezza: u32,
    altezza: u32,
}

impl Rettangolo {
    fn larghezza(&self) -> bool {
        self.larghezza > 0
    }
}

fn main() {
    let rettangolo1 = Rettangolo {
        larghezza: 30,
        altezza: 50,
    };

    if rettangolo1.larghezza() {
        println!("La larghezza del rettangolo è > 0; è {}", rettangolo1.larghezza);
    }
}

Qui scegliamo di fare in modo che il metodo larghezza ritorni true se il valore nel campo larghezza dell’istanza è maggiore di 0 e false se il valore è 0: possiamo usare un campo all’interno di un metodo con lo stesso nome per qualunque scopo. In main, quando seguiamo rettangolo1.larghezza con le parentesi tonde, Rust sa che si intende il metodo larghezza. Quando non usiamo le parentesi tonde, Rust sa che intendiamo il campo larghezza.

Spesso, ma non sempre, quando diamo a un metodo lo stesso nome di un campo vogliamo che esso ritorni soltanto il valore del campo e non faccia altro. I metodi di questo tipo sono chiamati getter (metodi di incapsulamento) e Rust non li implementa automaticamente per i campi della struct come fanno alcuni altri linguaggi di programmazione. I getter sono utili perché puoi rendere il campo privato ma il metodo pubblico, abilitando così accesso in sola lettura a quel campo come parte dell’API pubblica del type. Discuteremo cosa sono pubblico e privato e come designare un campo o un metodo come pubblico o privato nel Capitolo 7.

Dov’è l’Operatore ->?

In C e C++ si usano due operatori diversi per accedere ai membri: si usa . quando si lavora direttamente con un oggetto, e -> quando si lavora con un puntatore all’oggetto e prima bisogna de-referenziarlo. In C++, questi operatori possono essere usati per chiamare i metodi; in C, sono usati solo per accedere ai campi delle struct. In altre parole, se oggetto è un puntatore, oggetto->qualcosa() è simile a (*oggetto).qualcosa().

Rust non ha un equivalente dell’operatore ->; invece, Rust ha una funzionalità chiamata referenziamento e de-referenziamento automatico (automatic referencing and dereferencing). Chiamare i metodi è uno dei pochi posti in Rust che implementa questa funzionalità.

Ecco come funziona: quando chiami un metodo con oggetto.qualcosa(), Rust aggiunge automaticamente &, &mut, o * affinché oggetto corrisponda alla firma del metodo. In altre parole, i seguenti sono equivalenti:

#![allow(unused)]
fn main() {
#[derive(Debug,Copy,Clone)]
struct Punto {
    x: f64,
    y: f64,
}

impl Punto {
   fn distanza(&self, altro: &Punto) -> f64 {
       let x_quad = f64::powi(altro.x - self.x, 2);
       let y_quad = f64::powi(altro.y - self.y, 2);

       f64::sqrt(x_quad + y_quad)
   }
}
let p1 = Punto { x: 0.0, y: 0.0 };
let p2 = Punto { x: 5.0, y: 6.5 };
p1.distanza(&p2);
(&p1).distanza(&p2);
}

Il primo sembra molto più pulito. Questo comportamento di referencing automatico funziona perché i metodi hanno un receiver (recettore) chiaro, il type di self. Dato il receiver e il nome di un metodo, Rust può determinare in modo definitivo se il metodo sta leggendo (&self), mutando (&mut self), o consumando (self). Il fatto che Rust renda implicito il borrowing per i receiver dei metodi è una parte importante per rendere l’ownership ergonomica nella pratica.

Metodi con Più Parametri

Esercitiamoci ad usare i metodi implementando un secondo metodo sulla struct Rettangolo. Questa volta vogliamo che un’istanza di Rettangolo prenda un’altra istanza di Rettangolo e ritorni true se la seconda Rettangolo può entrare completamente dentro self (la prima Rettangolo); altrimenti dovrebbe ritornare false. Cioè, una volta definito il metodo può_contenere, dovremmo poter scrivere il programma mostrato nel Listato 5-14.

File: src/main.rs
fn main() {
    let rettangolo1 = Rettangolo {
        larghezza: 30,
        altezza: 50,
    };
    let rettangolo2 = Rettangolo {
        larghezza: 10,
        altezza: 40,
    };
    let rettangolo3 = Rettangolo {
        larghezza: 60,
        altezza: 45,
    };

    println!("Può rettangolo1 contenere rettangolo2? {}", rettangolo1.può_contenere(&rettangolo2));
    println!("Può rettangolo1 contenere rettangolo3? {}", rettangolo1.può_contenere(&rettangolo3));
}
Listato 5-14: Uso del metodo può_contenere ancora da scrivere

L’output atteso sarà il seguente perché entrambe le dimensioni di rettangolo2 sono più piccole delle dimensioni di rettangolo1, ma rettangolo3 è più larga di rettangolo1:

Può rettangolo1 contenere rettangolo2? true
Può rettangolo1 contenere rettangolo3? false

Sappiamo che vogliamo definire un metodo, quindi sarà all’interno del blocco impl Rettangolo. Il nome del metodo sarà può_contenere, e prenderà un reference immutabile di un’altra Rettangolo come parametro. Possiamo dedurre il type del parametro osservando il codice che chiama il metodo: rettangolo1.può_contenere(&rettangolo2) passa &rettangolo2, che è un reference immutabile di rettangolo2, un’istanza di Rettangolo. Questo ha senso perché abbiamo solo bisogno di leggere rettangolo2 (invece di modificarlo, il che richiederebbe un reference mutabile), e vogliamo che main mantenga l’ownership di rettangolo2 così da poterlo usare di nuovo dopo la chiamata a può_contenere. Il valore di ritorno di può_contenere sarà un Booleano, e l’implementazione verificherà se la larghezza e l’altezza di self sono maggiori rispetto alla larghezza e all’altezza dell’altra Rettangolo, rispettivamente. Aggiungiamo il nuovo metodo può_contenere al blocco impl del Listato 5-13, come mostrato nel Listato 5-15.

File: src/main.rs
#[derive(Debug)]
struct Rettangolo {
    larghezza: u32,
    altezza: u32,
}

impl Rettangolo {
    fn area(&self) -> u32 {
        self.larghezza * self.altezza
    }

    fn può_contenere(&self, altro: &Rettangolo) -> bool {
        self.larghezza > altro.larghezza && self.altezza > altro.altezza
    }
}

fn main() {
    let rettangolo1 = Rettangolo {
        larghezza: 30,
        altezza: 50,
    };
    let rettangolo2 = Rettangolo {
        larghezza: 10,
        altezza: 40,
    };
    let rettangolo3 = Rettangolo {
        larghezza: 60,
        altezza: 45,
    };

    println!("Può rettangolo1 contenere rettangolo2? {}", rettangolo1.può_contenere(&rettangolo2));
    println!("Può rettangolo1 contenere rettangolo3? {}", rettangolo1.può_contenere(&rettangolo3));
}
Listato 5-15: Implementazione del metodo può_contenere in Rettangolo che riceve un’altra istanza di Rettangolo come parametro

Quando eseguiamo questo codice con la funzione main del Listato 5-14, otterremo l’output desiderato. I metodi possono prendere parametri multipli che aggiungiamo alla firma dopo il parametro self, e quei parametri funzionano proprio come i parametri nelle funzioni.

Funzioni Associate

Tutte le funzioni definite all’interno di un blocco impl sono chiamate funzioni associate (associated functions) perché sono associate al type nominato dopo la parola impl. Possiamo definire funzioni associate che non hanno self come primo parametro (e quindi non sono metodi) perché non hanno bisogno di un’istanza del type per svolgere il loro compito. Ne abbiamo già usata una: la funzione String::from implementata sul type String.

Le funzioni associate che non sono metodi sono spesso usate come costruttori che ritornano una nuova istanza della struct. Spesso si chiamano new perché new non è una parola chiave e non è incorporata nel linguaggio. Per esempio, potremmo decidere di fornire una funzione associata chiamata quadrato che prende un parametro di dimensione e lo usa sia come larghezza sia come altezza, rendendo più semplice creare un Rettangolo quadrato invece di dover specificare lo stesso valore due volte:

File: src/main.rs

#[derive(Debug)]
struct Rettangolo {
    larghezza: u32,
    altezza: u32,
}

impl Rettangolo {
    fn quadrato(dimensione: u32) -> Self {
        Self {
            larghezza: dimensione,
            altezza: dimensione,
        }
    }
}

fn main() {
    let quad = Rectangle::quadrato(3);
}

La parola chiave Self nel type di ritorno e nel corpo della funzione è un alias per il type che appare dopo la parola chiave impl, che in questo caso è Rettangolo.

Per chiamare questa funzione associata, usiamo la sintassi :: con il nome della struct; let quad = Rettangolo::quadrato(3); è un esempio. Questa funzione è organizzata nel namespace della struct: la sintassi :: è usata sia per le funzioni associate sia per i namespace creati dai moduli. Parleremo più approfonditamente dei moduli nel Capitolo 7.

Blocchi impl Multipli

A ogni struct è permesso avere più blocchi impl. Per esempio, il Listato 5-15 è equivalente al codice mostrato nel Listato 5-16, che ha ognuno dei metodi nel proprio blocco impl.

#[derive(Debug)]
struct Rettangolo {
    larghezza: u32,
    altezza: u32,
}

impl Rettangolo {
    fn area(&self) -> u32 {
        self.larghezza * self.altezza
    }
}

impl Rettangolo {
    fn può_contenere(&self, altro: &Rectangle) -> bool {
        self.larghezza > altro.larghezza && self.altezza > altro.altezza
    }
}

fn main() {
    let rettangolo1 = Rettangolo {
        larghezza: 30,
        altezza: 50,
    };
    let rettangolo2 = Rettangolo {
        larghezza: 10,
        altezza: 40,
    };
    let rettangolo3 = Rettangolo {
        larghezza: 60,
        altezza: 45,
    };

    println!("Può rettangolo1 contenere rettangolo2? {}", rettangolo1.può_contenere(&rettangolo2));
    println!("Può rettangolo1 contenere rettangolo3? {}", rettangolo1.può_contenere(&rettangolo3));
}
Listato 5-16: Riscrittura del Listato 5-15 usando più blocchi impl

Non c’è motivo di separare questi metodi in più blocchi impl in questo caso, ma questa è una sintassi valida. Vedremo un caso in cui più blocchi impl sono utili nel Capitolo 10, dove discuteremo i type generici e i trait.

Le struct ti permettono di creare type personalizzati significativi per il tuo dominio. Usando le struct, puoi mantenere pezzi di dati correlati tra loro e dare un nome a ciascun pezzo per rendere il codice chiaro. Nei blocchi impl puoi definire funzioni associate al tuo type, e i metodi sono un tipo di funzione associata che ti permette di specificare il comportamento che le istanze delle tue struct hanno.

Ma le struct non sono l’unico modo per creare type personalizzati: passiamo ad un altro type di Rust, le enumerazioni, per aggiungere un altro strumento alla tua cassetta degli attrezzi.