Sintassi del Metodo

I metodi sono simili alle funzioni: li 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 qualche altra parte. A differenza delle funzioni, i metodi sono definiti nel contesto di una struct (o un enum o un trait object, che copriremo rispettivamente nel Capitolo 6 e Capitolo 17), e il loro primo parametro è sempre self, che rappresenta l'istanza della struct su cui il metodo viene chiamato.

Definire Metodi

Cambiamo la funzione area che ha un'istanza di Rectangle come parametro e invece facciamo un metodo area definito sulla struct Rectangle, come mostrato nel Listing 5-13.

Nome del file: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

Listing 5-13: Definire un metodo area sulla struct Rectangle

Per definire la funzione nel contesto di Rectangle, iniziamo un blocco impl (implementazione) per Rectangle. Tutto ciò che si trova all'interno di questo blocco impl sarà associato al tipo Rectangle. 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 all'interno del corpo. In main, dove abbiamo chiamato la funzione area e passato rect1 come argomento, possiamo invece usare la sintassi del metodo per chiamare il metodo area sulla nostra istanza di Rectangle. La sintassi del metodo va dopo un'istanza: aggiungiamo un punto seguito dal nome del metodo, parentesi tonde e qualsiasi argomento.

Nella firma per area, usiamo &self invece di rectangle: &Rectangle. Il &self è in realtà una forma abbreviata per self: &Self. All'interno di un blocco impl, il tipo Self è un alias per il tipo per cui è il blocco impl. I metodi devono avere un parametro chiamato self di tipo Self come loro primo parametro, quindi Rust permette di abbreviare questo con solo il nome self nella prima posizione del parametro. Notare che dobbiamo ancora usare il & davanti al self abbreviato per indicare che questo metodo prende in prestito l'istanza di Self, proprio come abbiamo fatto in rectangle: &Rectangle. I metodi possono prendere possesso di self, prendere in prestito self immutabilmente, come abbiamo fatto qui, o prendere in prestito self mutabilmente, proprio come possono fare con qualsiasi altro parametro.

Abbiamo scelto &self qui per lo stesso motivo per cui abbiamo usato &Rectangle nella versione della funzione: non vogliamo prendere possesso, e vogliamo solo leggere i dati nella struct, non scriverci. Se volessimo cambiare l'istanza su cui abbiamo chiamato il metodo come parte di ciò che il metodo fa, useremmo &mut self come primo parametro. Avere un metodo che prende possesso dell'istanza usando solo self come primo parametro è raro; questa tecnica è di solito usata quando il metodo trasforma self in qualcos'altro e si vuole prevenire l'utente dal usare l'istanza originale dopo la trasformazione.

Il motivo principale per usare i metodi anziché le funzioni, oltre a fornire una sintassi del metodo e non dover ripetere il tipo di self in ogni firma del metodo, è per l'organizzazione. Abbiamo messo tutte le cose che possiamo fare con un'istanza di un tipo in un unico blocco impl anziché far cercare agli utenti futuri del nostro codice le capacità di Rectangle in vari posti nella libreria che forniamo.

Notare che possiamo scegliere di dare a un metodo lo stesso nome di uno dei campi della struct. Ad esempio, possiamo definire un metodo su Rectangle che è anche chiamato width:

Nome del file: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    if rect1.width() {
        println!("The rectangle has a nonzero width; it is {}", rect1.width);
    }
}

Qui, scegliamo di fare in modo che il metodo width ritorni true se il valore nel campo width 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 qualsiasi scopo. In main, quando seguiamo rect1.width con delle parentesi tonde, Rust sa che intendiamo il metodo width. Quando non usiamo le parentesi tonde, Rust sa che intendiamo il campo width.

Spesso, ma non sempre, quando diamo a un metodo lo stesso nome di un campo vogliamo che solo ritorni il valore nel campo e non faccia nient'altro. Metodi come questi sono chiamati getter, e Rust non li implementa automaticamente per i campi della struct come fanno alcuni altri linguaggi. I getter sono utili perché puoi rendere il campo privato ma il metodo pubblico, e quindi abilitare l'accesso in sola lettura a quel campo come parte dell'API pubblica del tipo. Discuteremo cosa significano 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 chiamare metodi: si usa . se si chiama un metodo direttamente sull'oggetto e -> se si chiama il metodo su un puntatore all'oggetto e si ha bisogno di dereferenziare il puntatore prima. In altre parole, se object è un puntatore, object->something() è simile a (*object).something().

Rust non ha un equivalente dell'operatore ->; invece, Rust ha una funzionalità chiamata riferiemento e dereferimento automatico. Chiamare metodi è uno dei pochi posti in Rust che ha questo comportamento.

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

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

impl Point {
   fn distance(&self, other: &Point) -> f64 {
       let x_squared = f64::powi(other.x - self.x, 2);
       let y_squared = f64::powi(other.y - self.y, 2);

       f64::sqrt(x_squared + y_squared)
   }
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
}

Il primo sembra molto più pulito. Questo comportamento di riferimento automatico funziona perché i metodi hanno un chiaro ricevitore, il tipo di self. Dato il ricevitore e il nome di un metodo, Rust può capire in modo definitivo se il metodo sta leggendo (&self), mutando (&mut self), o consumando (self). Il fatto che Rust renda implicito il trasferimento di proprietà per i ricevitori di metodo è una grande parte di ciò che rende la proprietà ergonomica in pratica.

Metodi con Più Parametri

Pratichiamo l'uso dei metodi implementando un secondo metodo sulla struct Rectangle. Questa volta vogliamo che un'istanza di Rectangle prenda un'altra istanza di Rectangle e ritorni true se la seconda Rectangle può essere contenuta completamente all'interno di self (la prima Rectangle); altrimenti, dovrebbe ritornare false. Cioè, una volta che abbiamo definito il metodo can_hold, vogliamo essere in grado di scrivere il programma mostrato nel Listing 5-14.

Nome del file: src/main.rs

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-14: Utilizzare il metodo can_hold non ancora scritto

L'output previsto sarebbe simile al seguente perché entrambe le dimensioni di rect2 sono più piccole delle dimensioni di rect1, ma rect3 è più largo di rect1:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

Sappiamo di voler definire un metodo, quindi sarà all'interno del blocco impl Rectangle. Il nome del metodo sarà can_hold e prenderà in prestito immutabilmente un'altra Rectangle come parametro. Possiamo dire quale sarà il tipo del parametro guardando il codice che chiama il metodo: rect1.can_hold(&rect2) passa &rect2, che è un prestito immutabile di rect2, un'istanza di Rectangle. Questo ha senso perché dobbiamo solo leggere rect2 (anziché scriverci, cosa che significherebbe che avremmo bisogno di un prestito mutabile), e vogliamo che main mantenga la proprietà di rect2 così possiamo usarlo di nuovo dopo aver chiamato il metodo can_hold. Il valore di ritorno di can_hold sarà un valore booleano, e l'implementazione controllerà se la larghezza e l'altezza di self sono maggiori della larghezza e dell'altezza dell'altra Rectangle, rispettivamente. Aggiungiamo il nuovo metodo can_hold al blocco impl dal Listing 5-13, mostrato nel Listing 5-15.

Nome del file: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-15: Implementare il metodo can_hold su Rectangle che prende un'altra istanza di Rectangle come parametro

Quando eseguiamo questo codice con la funzione main nel Listing 5-14, otterremo il nostro output desiderato. I metodi possono avere diversi parametri che aggiungiamo alla firma dopo il parametro self, e quei parametri funziano proprio come i parametri nelle funzioni.

Funzioni Associate

Tutte le funzioni definite all'interno di un blocco impl sono chiamate funzioni associate perché sono associate al tipo denominato dopo l'impl. Possiamo definire funzioni associate che non hanno self come loro primo parametro (e quindi non sono metodi) perché non necessitano di un'istanza del tipo con cui lavorare. Abbiamo già usato una funzione di questo tipo: la funzione String::from che è definita sul tipo String.

Le funzioni associate che non sono metodi sono spesso utilizzate per costruttori che ritorneranno una nuova istanza della struct. Questi sono spesso chiamati new, ma new non è un nome speciale e non è integrato nel linguaggio. Per esempio, potremmo scegliere di fornire una funzione associata chiamata square che avrebbe un parametro di dimensione e userebbe quello sia come larghezza che come altezza, rendendo così più facile creare un Rectangle quadrato piuttosto che dover specificare lo stesso valore due volte:

Nome del file: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Rectangle::square(3);
}

Le parole chiave Self nel tipo di ritorno e nel corpo della funzione sono alias per il tipo che appare dopo la parola chiave impl, che in questo caso è Rectangle.

Per chiamare questa funzione associata, usiamo la sintassi :: con il nome della struct; let sq = Rectangle::square(3); è un esempio. Questa funzione è nello spazio dei nomi della struct: la sintassi :: è utilizzata sia per le funzioni associate che per gli spazi dei nomi creati dai moduli. Discuteremo i moduli nel Capitolo 7.

Molteplici Blocchi impl

Ogni struct è autorizzata ad avere molteplici blocchi impl. Ad esempio, il Listing 5-15 è equivalente al codice mostrato nel Listing 5-16, che ha ogni metodo nel proprio blocco impl.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-16: Riscrivere il Listing 5-15 usando molteplici blocchi impl

Non c'è nessun motivo per separare questi metodi in molteplici blocchi impl qui, ma questa è una sintassi valida. Vedremo un caso in cui molteplici blocchi impl sono utili nel Capitolo 10, dove discuteremo i tipi generici e i trait.

Sommario

Le struct ti permettono di creare tipi personalizzati significativi per il tuo dominio. Usando le struct, puoi mantenere pezzi di dati associati tra di loro e nominare ogni pezzo per rendere il tuo codice chiaro. Nei blocchi impl, puoi definire funzioni che sono associate al tuo tipo, 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 in cui puoi creare tipi personalizzati: passiamo alla caratteristica degli enum di Rust per aggiungere un altro strumento al tuo toolbox.