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.
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.
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 akiA-t implementálja,B-t is implementálja — ez lehetővé teszi, hogyAdefault metódusaiBmetó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: