Salta ai contenuti

Trait in Rust

Un trait definisce un insieme di comportamenti che un tipo può implementare: un po’ come un’interfaccia in altri linguaggi. È il modo in cui Rust dice «questo tipo sa fare X», ed è ciò che sta dietro ai vincoli di trait che hai visto nella pagina sui generici.

trait Speak {
fn speak(&self) -> String;
}

Questo dice: qualsiasi tipo che implementa Speak deve fornire un metodo speak che restituisce una String. Da solo, un trait è solo una promessa: non fa niente finché un tipo non lo implementa.

trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String {
String::from("Bau!")
}
}
impl Speak for Cat {
fn speak(&self) -> String {
String::from("Miao!")
}
}
fn main() {
let dog = Dog;
let cat = Cat;
println!("{}", dog.speak());
println!("{}", cat.speak());
}

Output:

Bau!
Miao!
▶ Provalo tu

Dog e Cat sono tipi che non hanno niente in comune, ma entrambi implementano Speak, quindi su entrambi puoi chiamare .speak(). È lo stesso schema dei blocchi impl della pagina sulle struct: impl Speak for Dog dice solo «ecco come Dog rispetta il trait Speak».

Ed ecco il vero vantaggio: puoi scrivere una funzione che accetta qualsiasi tipo che implementa un trait, senza preoccuparti di quale tipo preciso sia:

trait Speak {
fn speak(&self) -> String;
}
struct Dog;
impl Speak for Dog {
fn speak(&self) -> String { String::from("Bau!") }
}
fn announce(animal: &impl Speak) {
println!("L'animale dice: {}", animal.speak());
}
fn main() {
let dog = Dog;
announce(&dog);
}
▶ Provalo tu

animal: &impl Speak vuol dire «qualsiasi tipo che implementa Speak, non mi serve sapere quale». Aggiungi un tipo Bird che implementa anche lui Speak, e announce funziona anche con quello, senza cambiare niente.

Ricordi #[derive(Debug)] dalla pagina sulle struct? Anche Debug è un trait: derive genera automaticamente l’implementazione al posto tuo, invece di fartela scrivere a mano. Altri trait comuni che incontrerai: Clone (per .clone()), PartialEq (per ==) e PartialOrd (per < e >, che hai visto usato come vincolo di trait nella pagina sui generici).

Un trait può fornire il corpo predefinito di un metodo, che i tipi che lo implementano possono usare così com’è oppure sovrascrivere:

trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("Ciao, {}!", self.name())
}
}

Qualsiasi tipo che implementa Greet deve definire solo name(): greet() arriva gratis, a meno che il tipo non decida di sovrascrivere anche quello.

I trait hanno molto di più di quello che vedi in questa pagina. La pagina sui trait comuni presenta quelli standard che userai ogni giorno, e quella sui trait avanzati spiega i tipi associati, i supertrait e altri strumenti che vedrai nei progetti più grandi.

Definisci un trait Area con un solo metodo, fn area(&self) -> f64. Implementalo per un Rectangle (con width e height) e per un Square (con side). Stampa l’area di un rettangolo 3 per 4 e di un quadrato di lato 5. Suggerimento: ogni blocco impl Area for ... ha bisogno solo di quel metodo, e il suo corpo è una sola moltiplicazione.

Mostra la soluzione
trait Area {
fn area(&self) -> f64;
}
struct Rectangle {
width: f64,
height: f64,
}
struct Square {
side: f64,
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
impl Area for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
fn main() {
let rect = Rectangle { width: 3.0, height: 4.0 };
let square = Square { side: 5.0 };
println!("Rettangolo: {}", rect.area());
println!("Quadrato: {}", square.area());
}

Output:

Rettangolo: 12
Quadrato: 25
▶ Provalo tu

Definisci un trait Describe con fn describe(&self) -> String. Implementalo per un Book (con un title) e per un Movie (con un title e minutes). Poi scrivi una sola funzione fn show(item: &impl Describe) che stampa la descrizione, e chiamala con entrambi. Suggerimento: usa format! per costruire ogni stringa di descrizione.

Mostra la soluzione
trait Describe {
fn describe(&self) -> String;
}
struct Book {
title: String,
}
struct Movie {
title: String,
minutes: u32,
}
impl Describe for Book {
fn describe(&self) -> String {
format!("Libro: {}", self.title)
}
}
impl Describe for Movie {
fn describe(&self) -> String {
format!("Film: {} ({} min)", self.title, self.minutes)
}
}
fn show(item: &impl Describe) {
println!("{}", item.describe());
}
fn main() {
let book = Book { title: String::from("Lo Hobbit") };
let movie = Movie { title: String::from("Up"), minutes: 96 };
show(&book);
show(&movie);
}

Output:

Libro: Lo Hobbit
Film: Up (96 min)
▶ Provalo tu

Prendi il trait Greet di questa pagina, con il suo metodo predefinito greet(). Implementalo per una Person, che tiene il comportamento predefinito, e per un Pirate, che sovrascrive greet() per dire invece Ahoy, ...!. Suggerimento: entrambi i tipi devono definire name(). Solo Pirate scrive anche il suo greet().

Mostra la soluzione
trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("Ciao, {}!", self.name())
}
}
struct Person {
name: String,
}
struct Pirate {
name: String,
}
impl Greet for Person {
fn name(&self) -> String {
self.name.clone()
}
}
impl Greet for Pirate {
fn name(&self) -> String {
self.name.clone()
}
fn greet(&self) -> String {
format!("Ahoy, {}!", self.name())
}
}
fn main() {
let ana = Person { name: String::from("Ana") };
let jack = Pirate { name: String::from("Jack") };
println!("{}", ana.greet());
println!("{}", jack.greet());
}

Output:

Ciao, Ana!
Ahoy, Jack!
▶ Provalo tu
  • Un trait definisce un comportamento che un tipo può implementare, in modo simile a un’interfaccia.
  • impl NomeTrait for Tipo fornisce l’implementazione.
  • &impl NomeTrait come tipo di un parametro accetta qualsiasi tipo che implementa quel trait.
  • I trait possono fornire corpi predefiniti per i metodi, che chi li implementa può usare o sovrascrivere.
  • Debug, Clone e PartialEq sono trait comuni della libreria standard: #[derive(...)] li implementa automaticamente.

Verifica veloce

1. Cosa definisce un trait?

2. Cosa vuol dire animal: &impl Speak come tipo di un parametro?

3. Cosa fa davvero #[derive(Debug)]?

4. Cos'è un'implementazione predefinita in un trait?

5. Secondo questa pagina, quale di questi è un trait, e non una struct o un'enum?

Punteggio: 0 / 5