SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI
Sintassi dei Metodi
I metodi sono simili alle funzioni: li dichiariamo con la parola chiave fn
seguita da 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 all’interno del contesto di una struct (o di un enum o di un oggetto trait, che tratteremo rispettivamente nei Capitoli 6 e 17) e il loro primo parametro è sempre self
, che rappresenta l’istanza della struct su cui il metodo viene chiamato. Definizione dei Metodi
Modifichiamo la funzione area
che ha un’istanza di Rectangle
come parametro e invece creiamo un metodo area
definito sulla struct Rectangle
, come mostrato nel Codice 5-13.
rust
#[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!(
"L'area del rettangolo è di {} pixel quadrati.",
rect1.area()
);
}
Per definire la funzione all’interno del contesto di Rectangle
, iniziamo un blocco impl
(implementazione) per Rectangle
. Tutto ciò che è all’interno di questo blocco impl
sarà associato al tipo Rectangle
. Quindi spostiamo la funzione area
all’interno delle parentesi graffe di 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 utilizzare 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 e eventuali argomenti.
Nella firma per area
, utilizziamo &self
invece di rectangle: &Rectangle
. Il &self
è in realtà abbreviazione per self: &Self
. All’interno di un blocco impl
, il tipo Self
è un alias per il tipo a cui il blocco impl
si riferisce. I metodi devono avere un parametro chiamato self
di tipo Self
come loro primo parametro, quindi Rust ti permette di abbreviare questo con solo il nome self
nella prima posizione dei parametri. Nota che abbiamo ancora bisogno di usare il &
di fronte allo shorthand self
per indicare che questo metodo prende in prestito l’istanza Self
, proprio come abbiamo fatto in rectangle: &Rectangle
. I metodi possono prendere in proprietà self
, prendere in prestito self
in modo immutabile, come abbiamo fatto qui, o prendere in prestito self
in modo mutabile, 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 la proprietà, e vogliamo solo leggere i dati nella struct, non scriverli. 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 in proprietà l’istanza utilizzando solo self
come primo parametro è raro; questa tecnica viene di solito utilizzata quando il metodo trasforma self
in qualcos’altro e si desidera impedire al chiamante di utilizzare l’istanza originale dopo la trasformazione.
Il motivo principale per utilizzare i metodi invece delle funzioni, oltre a fornire la 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 blocco impl
anziché far cercare agli utenti futuri del nostro codice le capacità di Rectangle
in vari luoghi nella libreria che forniamo.
Nota 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 si chiama anche width
:
rust
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
Qui, stiamo scegliendo di fare in modo che il metodo width
restituisca true
se il valore nel campo width
dell’istanza è maggiore di 0 e false
se il valore è 0: possiamo utilizzare un campo all’interno di un metodo dello stesso nome per qualsiasi scopo. In main
, quando seguiamo rect1.width
con le parentesi, Rust sa che intendiamo il metodo width
. Quando non usiamo parentesi, Rust sa che intendiamo il campo width
.
Spesso, ma non sempre, quando diamo a un metodo lo stesso nome di un campo vogliamo solo che restituisca il valore del campo e non faccia altro. I metodi come questo sono chiamati getter, e Rust non li implementa automaticamente per i campi della struct come fanno alcuni altri linguaggi. I getter sono utili perché è possibile 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 sono pubblici e privati e come designare un campo o un metodo come pubblico o privato nel Capitolo 7.
mathematica
Dove è l'operatore ->?
In C e C++, vengono utilizzati due operatori diversi per chiamare i metodi: si utilizza . se si chiama un metodo sull'oggetto direttamente 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 riferimento e dereferenziamento automatico. Chiamare i metodi è uno dei pochi luoghi in Rust che ha questo comportamento. Ecco come funziona: quando chiami un metodo con object.something(), Rust aggiunge automaticamente &, &mut o * in modo che object corrisponda alla firma del metodo. In altre parole, i seguenti sono gli stessi: 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. Date il ricevitore e il nome di un metodo, Rust può capire definitivamente se il metodo sta leggendo (&self), mutando (&mut self) o consumando (self). Il fatto che Rust renda il prestito implicito per i ricevitori dei metodi è una parte importante nel rendere l'ownership ergonomico nella pratica.
Metodi con più parametri
Esercitiamoci nell’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 restituisca true
se il secondo Rectangle può stare completamente all’interno di self
(il primo Rectangle); altrimenti, dovrebbe restituire false
. Cioè, una volta che abbiamo definito il metodo can_hold
, vogliamo essere in grado di scrivere il programma mostrato nel Codice 5-14.
rust
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Rect1 può contenere rect2? {}", rect1.can_hold(&rect2));
println!("Rect1 può contenere rect3? {}", rect1.can_hold(&rect3));
}
L’output atteso sarà simile al seguente perché entrambe le dimensioni di rect2
sono più piccole delle dimensioni di rect1
, ma rect3
è più larga di rect1
:
arduino
Rect1 può contenere rect2? true
Rect1 può contenere rect3? false
Sappiamo che vogliamo definire un metodo, quindi sarà all’interno del blocco impl Rectangle
. Il nome del metodo sarà can_hold
, e prenderà un prestito immutabile di un altro Rectangle come parametro. Possiamo capire qual è il tipo del parametro guardando il codice che chiama il metodo: rect1.can_hold(&rect2)
passa &rect2
, che è un prestito immutabile a rect2
, un’istanza di Rectangle. Ha senso perché dobbiamo solo leggere rect2
(piuttosto che scriverlo, 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 booleano, e l’implementazione verificherà se la larghezza e l’altezza di self
sono maggiori della larghezza e altezza dell’altro Rectangle, rispettivamente. Aggiungiamo il nuovo metodo can_hold
al blocco impl
dal Codice 5-13, mostrato nel Codice 5-15.
rust
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
}
}
Quando eseguiamo questo codice con la funzione main
nel Codice 5-14, otterremo l’output desiderato. I metodi possono prendere più parametri che aggiungiamo alla firma dopo il parametro self
, e quei parametri funzionano esattamente 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 hanno bisogno di un’istanza del tipo con cui lavorare. Abbiamo già usato una funzione del genere: la funzione String::from
che è definita sul tipo String
.
Le funzioni associate che non sono metodi sono spesso utilizzate per i costruttori che restituiranno una nuova istanza della struct. Questi sono spesso chiamati new
, ma new
non è un nome speciale e non è incorporato nel linguaggio. Ad esempio, potremmo scegliere di fornire una funzione associata chiamata square
che avrebbe un parametro di dimensione e utilizzerebbe quello come larghezza e altezza, rendendo così più facile creare un Rectangle quadrato piuttosto che dover specificare lo stesso valore due volte:
rust
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
I 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, utilizziamo la sintassi ::
con il nome della struct; let sq = Rectangle::square(3);
è un esempio. Questa funzione è inserita nello spazio dei nomi dalla struct: la sintassi ::
è utilizzata sia per le funzioni associate che per i namespace creati dai moduli. Discuteremo dei moduli nel Capitolo 7. Blocchi impl
Multipli
Ogni struct è consentita di avere più blocchi impl
. Ad esempio, il Codice 5-15 è equivalente al codice mostrato nel Codice 5-16, che ha ciascun metodo nel proprio blocco impl
.
rust
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
}
}
Non c’è motivo per separare questi metodi in più blocchi impl
qui, ma questa è una sintassi valida. Vedremo un caso in cui blocchi impl
multipli sono utili nel Capitolo 10, dove discutiamo dei tipi generici e dei trait. Sommario
Le structs ti permettono di creare tipi personalizzati che sono significativi per il tuo dominio. Utilizzando le structs, puoi mantenere i pezzi associati di dati connessi tra loro e dare un nome a ciascun pezzo per rendere il tuo codice chiaro. Nei blocchi impl
, puoi definire funzioni che sono associate al tuo tipo, e i metodi sono una sorta di funzione associata che ti permette di specificare il comportamento che le istanze delle tue structs hanno.
Ma le structs non sono l’unico modo per creare tipi personalizzati: passiamo alla funzionalità enum di Rust per aggiungere un altro strumento al tuo kit.