Ahol az előző lecke abbamaradt

Az előző leckében megismerted, hogyan definiálhatsz egy trait-et, és hogyan implementálhatod azt konkrét struct-okra. Minden metódust neked kellett megírnod minden egyes impl blokkban — még akkor is, ha a viselkedés valójában ugyanaz lett volna több típusnál. Ez a lecke pont ezt a problémát oldja meg: megtanulod, hogyan adhatsz a trait-nek egy kész, alapértelmezett metódustestet, amit az implementorok akár változatlanul is használhatnak, és hogyan építhetsz egy trait-et egy másikra, hogy a közös alapokat egyszer írd meg.

Alapértelmezett metódus definiálása

Egy trait metódusának nem kötelező üres deklarációnak lennie — megadhatsz neki törzset (body) is. Ha egy típus implementálja a trait-et, de nem ír felül egy ilyen metódust, akkor automatikusan az alapértelmezett viselkedést kapja.

trait Greet {
    // kötelező metódus: minden implementornak meg kell adnia
    fn name(&self) -> String;

    // alapértelmezett metódus: van kész törzse
    fn greet(&self) -> String {
        format!("Szia, {}!", self.name())
    }
}

struct Person {
    name: String,
}

impl Greet for Person {
    fn name(&self) -> String {
        self.name.clone()
    }
    // greet-et nem írjuk felül, marad az alapértelmezett
}

fn main() {
    let anna = Person { name: String::from("Anna") };
    println!("{}", anna.greet()); // Szia, Anna!
}

Fontos: az alapértelmezett metódus törzse csak azokat a metódusokat érheti el, amiket a trait maga definiál (jelen esetben self.name()-t) — a struct konkrét mezőihez nincs közvetlen hozzáférése, csak a trait interfészén keresztül.

Ha egy típus elfelejti implementálni a kötelező name metódust, a fordító azonnal hibát jelez:

struct Robot;

impl Greet for Robot {}
error[E0046]: not all trait items implemented, missing: `name`

Az alapértelmezett metódus tehát nem mentesít a kötelező metódusok megadása alól, csak a plusz, kényelmi metódusokat engedi elhagyni.

Mikor éri meg felülírni, mikor hagyni?

Az alapértelmezett metódus egy jó kiindulási pont, de nem mindig illik minden típushoz. Ha egy adott struct-nak más viselkedés kell, egyszerűen megadhatod a saját változatát ugyanazzal a metódusnévvel — ez felülírja az alapértelmezettet.

trait Greet {
    fn name(&self) -> String;

    fn greet(&self) -> String {
        format!("Szia, {}!", self.name())
    }
}

struct Robot {
    id: u32,
}

impl Greet for Robot {
    fn name(&self) -> String {
        format!("Robot-{}", self.id)
    }

    // saját greet, mert a robotok nem \"szia\"-znak
    fn greet(&self) -> String {
        format!("BEEP BOOP {}", self.name())
    }
}

fn main() {
    let r2 = Robot { id: 2 };
    println!("{}", r2.greet()); // BEEP BOOP Robot-2
}

Általános ökölszabály: ha a legtöbb implementornak megfelel egy közös viselkedés, hagyd meg alapértelmezettként — ezzel kódot spórolsz. Ha viszont egy típusnak érdemben más logikára van szüksége, írd felül. Az sem baj, ha egy trait-nek egyáltalán nincs alapértelmezett metódusa — ez csak egy lehetőség, nem kötelező elem.

Tipp

Az alapértelmezett metódusok remekül jönnek akkor, amikor egy trait-hez utólag szeretnél új funkciót adni anélkül, hogy minden meglévő implementációt módosítanod kellene: elég az új metódusnak alapértelmezett törzset adni, és a régi kód is fordulni fog tovább.

Trait-öröklés: supertrait

Előfordulhat, hogy egy trait feltételezi egy másik trait meglétét — például egy Greet trait csak akkor ad értelmes köszönést, ha a típus rendelkezik névvel. Ezt a kapcsolatot a trait-öröklés (supertrait) fejezi ki: a trait A: B szintaxis azt jelenti, hogy aki A-t implementálja, annak B-t is implementálnia kell.

trait Named {
    fn name(&self) -> String;
}

// a Greet trait-nek szüksége van a Named trait-re: ez a supertrait
trait Greet: Named {
    fn greet(&self) -> String {
        format!("Szia, {}!", self.name())
    }
}

struct Person {
    name: String,
}

impl Named for Person {
    fn name(&self) -> String {
        self.name.clone()
    }
}

impl Greet for Person {}

fn main() {
    let anna = Person { name: String::from("Anna") };
    println!("{}", anna.greet());
}

A Greet trait default metódusa a self.name()-t hívja meg — ezt csak azért teheti meg biztonságosan, mert a Greet: Named fejrész garantálja, hogy minden Greet-et implementáló típus egyben Named is. Ha kihagynád az impl Named for Person blokkot, a fordító hibát jelezne: a Person nem felelne meg annak a feltételnek, amit a Greet: Named megkövetel, így nem implementálhatná a Greet trait-et.

Több szintű öröklés is lehetséges: egy trait több supertrait-re is épülhet, és egy lánc alján lévő trait mögött több réteg közös viselkedés állhat. A gyakorlatban ez akkor hasznos, ha logikailag különálló képességeket (pl. "van neve", "köszön") szeretnél elkülönítve definiálni, majd egy magasabb szintű trait-ben összekapcsolni.

Megjegyzés

A supertrait nem "mezőket" ad át, hanem viselkedést garantál: a Greet trait-nek nincs közvetlen hozzáférése a Person struct mezőihez, csak a Named trait name metódusán keresztül tud "belenézni" az adatokba.

Próbáld ki!

Vegyél elő a fenti Named + Greet páros kódot, és bővítsd egy harmadik alapértelmezett metódussal: fn farewell(&self) -> String, amely a self.name()-t felhasználva egy elköszönő mondatot ad vissza (pl. "Viszlát, Anna!"). Hozz létre egy új struct-ot (pl. Cat), implementáld rá a Named trait-et, és nézd meg, hogy a Greet trait mindkét default metódusát (greet és farewell) automatikusan megkapja-e felülírás nélkül.

Önellenőrzés

Megoldás Kérdés: Mi a különbség egy trait \"kötelező\" és \"alapértelmezett\" metódusa között?

A kötelező metódusnak nincs törzse a trait definíciójában — minden implementornak meg kell adnia a saját változatát, különben fordítási hiba keletkezik. Az alapértelmezett metódusnak van kész törzse, ezért az implementorok elhagyhatják a felülírást, és automatikusan a trait-ben megadott viselkedést kapják.

Megoldás Kérdés: Mikor éri meg felülírni egy alapértelmezett metódust?

Akkor, amikor egy konkrét típusnak érdemben más logikára van szüksége, mint amit az alapértelmezett viselkedés ad — például egy Robot struct-nak nem "szia"-znia kell, hanem "BEEP BOOP"-olnia. Ha a típusnak megfelel a közös viselkedés, nincs szükség felülírásra.

Megoldás Kérdés: Mit jelent az, hogy egy trait \"supertrait\"-je egy másiknak?

A trait A: B jelölés azt jelenti, hogy B a A supertrait-je: aki A-t implementálja, annak kötelező B-t is implementálnia. Ez lehetővé teszi, hogy A default metódusai biztonságosan hívják B metódusait, mert a fordító garantálja, hogy azok léteznek az adott típuson.

Összefoglalás és mi jön legközelebb

  • Egy trait metódusa kaphat kész törzset (default metódus); az implementorok ezt megtarthatják vagy felülírhatják.
  • A kötelező metódusokat (nincs törzsük) minden implementornak meg kell adnia, különben fordítási hiba jön.
  • A trait-öröklés (trait A: B) kikényszeríti, hogy aki A-t implementálja, B-t is implementálja — ez lehetővé teszi, hogy A default metódusai B metódusait hívják.
  • Ökölszabály: hagyd meg az alapértelmezettet, ha a legtöbb típusnak megfelel; írd felül, ha egy típusnak eltérő logikára van szüksége.
  • A következő leckében megnézzük, hogyan tudsz különböző típusokat egyetlen gyűjteményben vagy paraméterben kezelni közös trait-jük alapján — ez a trait objektumok (dyn) világa.

További olvasmány: