Traits: definirea comportamentului partajat

O trăsătură (trait) definește funcționalitățile pe care un tip le posedă și care pot fi partajate cu alte tipuri. Noi utilizăm trăsăturile pentru a stabili în mod abstract comportamentul partajat. Totodată, prin limitele trăsăturii specificăm că un tip generic poate fi orice tip care manifestă anumite comportamente.

Notă: Trăsăturile sunt similare cu o funcționalitate frecvent întâlnită în alte limbaje de programare, des numită interfețe, deși există diferențe notabile.

Definirea unei trăsături

Comportamentul unui tip este caracterizat de metodele care pot fi invocate pe acel tip. Diverse tipuri au un comportament comun dacă este posibil să apelăm aceleași metode pe fiecare dintre ele. Definițiile trăsăturilor reprezintă o metodă de a grupa semnături de metode pentru a contura un set de comportamente necesare pentru îndeplinirea unui obiectiv anume.

Să presupunem, de exemplu, că dispunem de structuri multiple care stochează diferite cantități și tipuri de text: o structură NewsArticle care conține o știre legată de o locație specifică și un Tweet care poate avea până la 280 de caractere, împreună cu metadate care precizează dacă acesta este un tweet nou, un retweet sau un răspuns la un alt tweet.

Ne propunem să construim o crate de bibliotecă de agregare ale articolelor media numită aggregator, capabilă să prezinte sumare ale datelor care pot fi conținute de instanțe ale NewsArticle sau Tweet. Pentru acest lucru, avem nevoie de un rezumat din partea fiecărui tip, pe care îl solicităm prin apelarea metodei summarize pe o instanță respectivă. Listarea 10-12 ilustrează definiția unei trăsături publice Summary ce exprimă acest comportament.

Numele fișierului: src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String;
}

Listarea 10-12: O trăsătură Summary ce constă din comportamentul oferit de metoda summarize

În acest caz, introducem o trăsătură utilizând cuvântul cheie trait urmat de numele trăsăturii, care este Summary. Am declarat trăsătura ca fiind publică (pub), astfel încât alte crate-uri care depind de acesta să aibă posibilitatea de a o folosi, așa cum vom observa în unele exemple ulterioare. În spațiul dintre acolade, sunt prezentate semnăturile metodelor care definesc comportamentele asumate de tipurile care implementează această trăsătură; în cazul de față fiind fn summarize(&self) -> String.

În loc să furnizăm o implementare a metodei între acolade, punem un punct și virgulă după semnătură. Fiecărui tip care realizează această trăsătură îi revine sarcina de a implementa propriul comportament pentru corpul metodei. Compilatorul va asigura că orice tip cu trăsătura Summary va avea definită metoda summarize cu anume această semnătură exactă.

O trăsătură poate include în corpul său mai multe metode: semnăturile acestora sunt enumerate independent, pe rânduri separate, iar fiecare rând se încheie cu un punct și virgulă.

Implementarea unei trăsături pentru un tip

Noi am definit semnăturile dorite ale metodelor trăsăturii Summary și acum e timpul să le implementăm pentru tipurile din agregatorul nostru de media. Listarea 10-13 ilustrează implementarea trăsăturii Summary pentru structura NewsArticle, utilizând titlul, autorul și locația pentru a crea valoarea de retur a metodei summarize. În cazul structurii Tweet, metoda summarize este definită ca numele de utilizator urmat de întregul text al tweet-ului, având în vedere că conținutul tweet-ului este limitat la 280 de caractere.

Numele fișierului: src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

Listarea 10-13: Implementarea trăsăturii Summary pentru tipurile NewsArticle și Tweet

Implementarea unei trăsături pentru un tip este similară cu implementarea metodelor obișnuite. Diferența constă în faptul că, după impl, specificăm numele trăsăturii pe care dorim să o realizăm, folosim cuvântul cheie for, și apoi numele tipului pentru care implementăm trăsătura. În blocul impl, includem semnăturile metodelor definite de trăsătura respectivă. În loc să finalizăm fiecare semnătură cu un punct și virgulă, adăugăm acolade și detaliem corpul fiecărei metode pentru a defini comportamentul specific pe care îl dorim de la metodele trăsăturii pentru tipul respectiv.

Odată ce biblioteca noastră a implementat trăsătura Summary pentru NewsArticle și Tweet, utilizatori crate-ului pot folosi metodele trăsăturii pe instanțe de NewsArticle și Tweet, similar cu modul în care sunt apelate metodele obișnuite. Singura diferență este că utilizatorii trebuie să includă atât trăsătura cât și tipurile în domeniul de vizibilitate. Aici este un exemplu de cum ar putea fi utilizată biblioteca noastră aggregator în cadrul unui crate binar:

use aggregator::{Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

Acest exemplu de cod va afișa 1 new tweet: horse_ebooks: of course, as you probably already know, people.

Alte crate-uri care utilizează crate-ul aggregator pot de asemenea să își aducă trăsătura Summary în domeniul de vizibilitate pentru a o implementa pe tipurile proprii. O restricție de reținut este că putem implementa o trăsătură pe un tip doar în cazul în care cel puțin unul dintre elemente - trăsătura sau tipul - este definit local în propriul nostru crate. De exemplu, putem implementa trăsături standard ale bibliotecii, cum ar fi Display, pentru un tip nativ crate-ului nostru ca Tweet, ca parte a funcționalității aggregator. De asemenea, putem implementa Summary pentru Vec<T> în cadrul aggregator, din moment ce trăsătura Summary este locală crate-ului nostru.

Cu toate acestea, nu avem posibilitatea să implementăm trăsături externe pentru tipuri externe. Spre exemplu, nu putem să realizăm implementarea trăsăturii Display pentru Vec<T> în cadrul crate-ului aggregator, pentru că atât Display cât și Vec<T> sunt parte din biblioteca standard și nu sunt locale lui aggregator. Această limitare este o parte din principiul de coerență, mai precis din regula orfanilor. Această regulă asigură că codul altor persoane nu poate interfera cu al tău și în același timp protejează codul tău de a fi afectat de alții. Fără această regulă, ar exista posibilitatea ca două crate-uri diferite să implementeze aceeași trăsătură pentru același tip, ceea ce ar crea confuzie pentru Rust cu privire la care implementare ar trebui utilizată.

Implementări implicite

Câteodată e util să avem un comportament implicit pentru unele sau pentru toate metodele unei trăsături în loc să necesităm implementări pentru toate metodele pe fiecare tip. Prin urmare, atunci când implementăm trăsătura pe un anumit tip, putem menține sau suprascrie comportamentul implicit al fiecărei metode.

În Listarea 10-14, am specificat un string implicit pentru metoda summarize a trăsăturii Summary în loc doar să definim semnătura metodei, cum am făcut în Listarea 10-12.

Numele fișierului: src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

Listarea 10-14: Definirea trăsăturii Summary cu o implementare implicită a metodei summarize

Pentru a folosi o implementare implicită în rezumarea instanțelor de NewsArticle, specificăm un bloc impl gol cu impl Summary for NewsArticle {}.

Deși nu mai definim metoda summarize în mod direct pe NewsArticle, am furnizat o implementare implicită și am specificat că NewsArticle implementează trăsătura Summary. Drept rezultat, putem totuși apela metoda summarize pe o instanță de NewsArticle, astfel:

use aggregator::{self, NewsArticle, Summary};

fn main() {
    let article = NewsArticle {
        headline: String::from("Penguins win the Stanley Cup Championship!"),
        location: String::from("Pittsburgh, PA, USA"),
        author: String::from("Iceburgh"),
        content: String::from(
            "The Pittsburgh Penguins once again are the best \
             hockey team in the NHL.",
        ),
    };

    println!("New article available! {}", article.summarize());
}

Acest cod afișează New article available! (Read more...).

Crearea unei implementări implicite nu ne obligă să modificăm nimic în ceea ce privește implementarea Summary pentru Tweet, conform Listării 10-13. Acest lucru este datorat faptului că sintaxa pentru a suprascrie o implementare implicită este identică cu sintaxa pentru implementarea unei metode de trăsătură care nu are nicio implementare implicită.

Implementările implicite pot apela alte metode din aceeași trăsătură, chiar dacă aceste alte metode nu dispun de implementări implicite. Astfel, o trăsătură poate oferi multe funcționalități utile și poate cere implementatorilor să definească doar o mică parte din acelea. De exemplu, am putea defini trăsătura Summary să includă o metodă summarize_author ce necesită implementare, și apoi să definim o metodă summarize care are o implementare implicită ce invocă summarize_author:

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

Când folosim această versiune de Summary, trebuie să definim summarize_author doar când implementăm trăsătura pentru un tip:

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

După ce definim summarize_author, putem invoca summarize pe instanțele structurii Tweet, iar implementarea implicită a summarize va utiliza definiția de summarize_author pe care am furnizat-o. Întrucât am implementat summarize_author, trăsătura Summary ne oferă funcționalitatea metodei summarize fără să fie nevoie să scriem cod suplimentar.

use aggregator::{self, Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

Acest cod afișează 1 new tweet: (Read more from @horse_ebooks...).

Notă: nu este posibil să apelăm implementarea implicită din cadrul unei implementări care o suprascrie pe aceeași metodă.

Utilizarea trăsăturilor drept parametri

Înarmat cu abilitatea de a defini și implementa trăsături, ești acum pregătit să descoperi cum să folosești trăsături pentru a concepe funcții care să accepte o varietate de tipuri diferite. Vom apela la trăsătura Summary, implementată pentru NewsArticle și Tweet în Listarea 10-13, pentru a defini funcția notify care invocă metoda summarize pe parametrul său item. Acest item trebuie să fie de un tip care implementează trăsătura Summary. Procedăm astfel folosind sintaxa impl Trait, în modul următor:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

În loc de un tip explicit pentru parametrul item, specificăm cuvântul cheie impl și numele trăsăturii. Acest parametru va accepta orice tip care implementează trăsătura indicată. În cadrul funcției notify, avem posibilitatea să apelăm orice metode asociate cu item ce derivă din trăsătura Summary, precum summarize. Funcția notify poate fi apelată utilizând orice exemplar de NewsArticle sau Tweet. Încercarea de a folosi funcția cu tipuri care nu implementează Summary, cum ar fi String sau i32, nu va fi compilată, deoarece aceste tipuri nu îndeplinesc cerința trăsăturii Summary.

Sintaxa delimitării de trăsături

Sintaxa impl Trait este practică în situații simple, dar de fapt reprezintă o formă abreviată pentru o formă mai extinsă, cunoscută ca delimitare de trăsături (trait bound); aceasta se prezintă astfel:

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

Această variantă completă este identică cu exemplul din secțiunea anterioară, dar mai explicită. Plasăm delimitările de trăsături împreună cu declararea parametrului de tip generic după două puncte (:) și în interiorul parantezelor unghiulare.

Sintaxa impl Trait oferă concizie în cazurile simple, în timp ce sintaxa extinsă a delimitărilor de trăsături poate să exprime mai multă complexitate în alte situații. De pildă, putem avea doi parametri ce implementează Summary. Aplicarea sintaxei impl Trait apare în felul următor:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

Utilizarea impl Trait este adecvată dacă dorim ca funcția să permită lui item1 și item2 să fie de tipuri diferite (atât timp cât ambele tipuri implementează Summary). Totuși, dacă vrem să constrângem ambii parametri să fie de același tip, atunci trebuie să apelăm la delimitarea de trăsături, ca în exemplul următor:

pub fn notify<T: Summary>(item1: &T, item2: &T) {

Tipul generic T, specificat ca tip pentru parametrii item1 și item2, restricționează funcția astfel încât tipul concret al valorilor transmise ca argumente pentru item1 și item2 trebuie să fie același.

Indicarea mai multor delimitări de trăsătură folosind sintaxa +

Este posibil să specificăm concurent mai multe delimitări de trăsătă. De exemplu, dacă dorim ca notify să utilizeze formatare prin afișare, precum și metoda summarize pentru item, trebuie să notăm în definiția notifyitem trebuie să implementeze trăsăturile Display și Summary. Putem realiza acest lucru utilizând sintaxa +:

pub fn notify(item: &(impl Summary + Display)) {

Această sintaxă + poate fi folosită și în cazul limitelor impuse pe trăsături pentru tipuri generice:

pub fn notify<T: Summary + Display>(item: &T) {

Odată ce ambele trăsături sunt specificate, în cadrul lui notify putem invoca summarize și folosi {} pentru a formata item, ceea ce este posibil datorită implementării trăsăturii Display.

Delimitări de trăsătură mai clare cu clauza where

Abundența de delimitări de trăsătură pe generici poate avea dezavantaje. Fiecare tip generic vine cu propriile sale restricții de trăsătură, astfel funcțiile cu mai mulți parametri generici pot fi supraîncărcate cu informații privind trăsăturile între numele funcției și lista parametrilor, complicând citirea semnăturii funcției. Pentru a simplifica această problemă, Rust oferă o sintaxă alternativă prin utilizarea unei clauze where care se plasează după semnătura funcției. Astfel, în loc de a scrie în modul următor:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

putem opta pentru utilizarea unei clauze where, cum ar fi:

fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    unimplemented!()
}

Semnătura funcției este astfel mai clară: numele funcției, lista parametrilor și tipul returnat sunt regrupate în proximitate, similar unei funcții simple care nu include numeroase restricții de trăsătură.

Returnarea tipurilor ce implementează trăsături

Este posibil de asemenea să utilizăm sintaxa impl Trait în poziția de returnare pentru a returna o valoare de un tip care implementează o trăsătură, așa cum este arătat în exemplul de mai jos:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}

Prin aplicarea impl Summary ca tip de retur, indicăm că funcția returns_summarizable va întoarce un tip ce implementează trăsătura Summary, fără a specifica tipul concret. În acest caz specific, returns_summarizable returnează un Tweet, dar codul care cheamă această funcție nu are nevoie să cunoască acest amănunt.

Posibilitatea de a specifica un tip de retur exclusiv prin trăsătura implementată se dovedește a fi extrem de valoroasă în contextul închiderilor (closure) și iteratorilor, teme ce vor fi explorate în Capitolul 13. Aceste închideri și iteratori produc tipuri cunoscute doar de compilator sau tipuri care ar fi oneros de specificat complet. Sintaxa impl Trait facilitează specificarea succintă a faptului că o funcție returnează un tip care implementează trăsătura Iterator, eliminând necesitatea de a descrie un tip extensiv.

Totuși, impl Trait poate fi folosit doar atunci când se returnează un tip unic. Spre exemplu, codul care face returnarea unui NewsArticle sau un Tweet, având tipul de retur definit ca impl Summary, nu va funcționa:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from(
                "Penguins win the Stanley Cup Championship!",
            ),
            location: String::from("Pittsburgh, PA, USA"),
            author: String::from("Iceburgh"),
            content: String::from(
                "The Pittsburgh Penguins once again are the best \
                 hockey team in the NHL.",
            ),
        }
    } else {
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            retweet: false,
        }
    }
}

Este interzisă returnarea fie a unui NewsArticle, fie a unui Tweet, pe fondul restricțiilor asociate modului de implementare a sintaxei impl Trait în compilator. Metodologia de scriere a unei funcții cu acest comportament va fi detaliată în secțiunea „Utilizând obiecte de trăsătură ce permit valori pentru tipuri diverse” din Capitolul 17.

Folosirea delimitărilor de trăsături pentru implementarea condiționată a metodelor

Prin utilizarea unei delimitări de trăsături într-un bloc impl ce include parametri de tip generic, noi putem să implementăm metode în mod condiționat pentru tipurile care implementează trăsăturile specificate. Spre exemplu, tipul Pair<T> din Listarea 10-15 implementează constant funcția new pentru a crea o nouă instanță de Pair<T> (reîmprospătăm aici din secțiunea “Definirea Metodelor” a Capitolului 5 că Self este un sinonim pentru tipul blocului impl, care în acest caz este Pair<T>). Totuși, în următorul bloc impl, Pair<T> implementează metoda cmp_display doar dacă tipul său intern T aderă atât la trăsătura PartialOrd ce face posibilă compararea cât și la trăsătura Display ce permite afișarea.

Numele fișierului: src/lib.rs

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

Listarea 10-15: Implementarea condiționată a metodelor pe un tip generic, în dependență de delimitările de trăsătură

De asemenea, putem implementa condiționat o trăsătură pentru orice tip care implementează o altă trăsătură. Aceste implementări, realizate pentru orice tip care îndeplinește delimitările de trăsătură, poartă numele de implementări generalizate (blanket implementations) și sunt utilizate extensiv în biblioteca standard Rust. De exemplu, biblioteca standard oferă implementarea trăsăturii ToString pentru orice tip care implementează trăsătura Display. Blocul impl din biblioteca standard este similar cu acest fragment de cod:

impl<T: Display> ToString for T {
    // --snip--
}

Datorită acestei implementări generalizate prezente în biblioteca standard, noi putem invoca metoda to_string, definită de trăsătura ToString, pe orice tip ce implementează trăsătura Display. Ca urmare, avem posibilitatea de a transforma numere întregi în echivalentele lor String pentru că întregii implementează Display:

#![allow(unused)]
fn main() {
let s = 3.to_string();
}

Implementările generalizate pot fi găsite în documentația referitoare la trăsătura respectivă, în secțiunea “Implementors”.

Trăsăturile și delimitările de trăsături ne permit să concepem cod care utilizează parametrii de tip generic pentru a reduce dublarea, dar ne și permit să indicăm compilatorului că dorim ca tipul generic să prezinte un anumit comportament. Compilatorul, folosind informațiile delimitărilor de trăsături, poate verifica dacă toate tipurile concrete folosite în codul nostru corespund comportamentului cerut. În limbajele de programare cu tipizare dinamică, erorile legate de apeluri ale unor metode nedefinite pe un tip apar la runtime, pe când Rust transferă aceste erori la timpul de compilare, obligându-ne să rezolvăm problemele înainte ca programul nostru să fie capabil să ruleze. Mai mult, evităm necesitatea de a scrie cod care să verifice comportamentul la runtime deoarece verificările au loc în timpul compilării. Acest proces îmbunătățește performanța fără a renunța la flexibilitatea oferită de utilizarea genericilor.