Miért van szükség trait-ekre?

Eddig a leckékben főleg beépített típusokkal (számok, String, tuple-ök, array-ek) dolgoztunk, és önálló függvényeket írtunk. Ahogy egy program nő, egyre gyakrabban szeretnéd a saját adataidat (struct-ok, enum-ok) és a hozzájuk tartozó viselkedést (függvényeket, amik ezeken az adatokon dolgoznak) egy helyen, logikusan csoportosítva tartani. Erre való az impl blokk.

De van egy másik probléma is: mi van, ha több, egymástól teljesen független típusnak ugyanolyan jellegű viselkedést szeretnél adni? Például egy állatnak és egy másik állatnak is legyen egy „hangot ad” metódusa, de a kettő belső megvalósítása más. Erre találták ki a trait-eket: egy trait egy közös viselkedést ír le, amit különböző típusok a saját módjukon implementálhatnak. Ez az útvonal éppen erről szól, és ez a lecke az alapokat teszi le.

Struct-ok és enum-ok újranézve

A struct egy saját adattípus, amely névvel ellátott mezőkből áll. Eddig érintettük a tuple-öket, a struct ezeknél kifejezőbb, mert a mezőknek nevük van:

struct Point {
    x: f64,
    y: f64,
}

fn main() {
    let p = Point { x: 3.0, y: 4.0 };
    println!("A pont koordinátái: ({}, {})", p.x, p.y);
}

Az enum (enumeráció) olyan típus, amelynek az értéke a felsorolt variánsok közül pontosan egy lehet. Ezt már láttad match kifejezésekben, de nézzük meg saját enum definícióval is:

enum Iranytu {
    Eszak,
    Del,
    Kelet,
    Nyugat,
}

fn nev(irany: &Iranytu) -> &str {
    match irany {
        Iranytu::Eszak => "észak",
        Iranytu::Del => "dél",
        Iranytu::Kelet => "kelet",
        Iranytu::Nyugat => "nyugat",
    }
}

fn main() {
    let irany = Iranytu::Kelet;
    println!("Az irány: {}", nev(&irany));
}

Az enum variánsok tarthatnak adatot is (ahogy korábban a tuple-ökkel láttad), de ehhez a leckéhez elég, ha egyszerű, adat nélküli variánsokkal dolgozunk.

Az impl blokk: metódusok és asszociált függvények

Egy struct vagy enum önmagában csak adatot ír le. A viselkedést az impl (implementation) blokkban adjuk hozzá. Két fajta funkciót különböztetünk meg:

  • Metódus: a paraméterlistája &self-fel (vagy &mut self, self) kezdődik, és egy konkrét példányon hívjuk meg, ponttal (ertek.metodus()).
  • Asszociált függvény: nincs self paramétere, a típus nevén keresztül hívjuk meg, dupla kettősponttal (Tipus::fuggveny()). Gyakran ezt használjuk „konstruktor” céljából, hagyományosan new néven.
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    // Asszociált függvény: nincs "self" paramétere.
    // A "Self" a jelenlegi típusra (itt Point) utal.
    fn new(x: f64, y: f64) -> Self {
        Point { x, y }
    }

    // Metódus: az &self miatt egy konkrét példányon hívható.
    fn distance_from_origin(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

fn main() {
    let p = Point::new(3.0, 4.0);
    println!("A pont távolsága az origótól: {}", p.distance_from_origin());
}

Egy típushoz több impl blokk is tartozhat, ha ez logikailag jobban strukturálja a kódot — a fordító ugyanúgy egy típusnak tekinti a metódusokat, függetlenül attól, hogy hány impl blokkban vannak felsorolva.

Tipp

A Self (nagy S-sel) mindig a jelenlegi típusra utal az impl blokkon belül. Ez hasznos, mert ha átnevezed a struct-ot, nem kell minden előfordulást kézzel javítanod.

Trait-ek: közös viselkedés leírása

Egy trait egy metódushalmazt ír le, amit egy típus ígéretként implementálhat. A trait definíciója csak a metódusok szignatúráját (nevét, paramétereit, visszatérési típusát) adja meg, a törzsüket nem — azt minden típus a saját impl Trait for Tipus blokkjában adja meg.

Nézzünk egy konkrét példát: legyen egy Hangado trait, amelyet két különböző struct, egy Kutya és egy Macska implementál, mindegyik a saját módján.

trait Hangado {
    fn hangot_ad(&self) -> String;
}

struct Kutya {
    nev: String,
}

struct Macska {
    nev: String,
}

impl Hangado for Kutya {
    fn hangot_ad(&self) -> String {
        format!("{} azt mondja: Vau!", self.nev)
    }
}

impl Hangado for Macska {
    fn hangot_ad(&self) -> String {
        format!("{} azt mondja: Miáú!", self.nev)
    }
}

fn main() {
    let kutya = Kutya { nev: String::from("Buksi") };
    let macska = Macska { nev: String::from("Cirmos") };

    println!("{}", kutya.hangot_ad());
    println!("{}", macska.hangot_ad());
}

Figyeld meg: a Kutya és a Macska semmilyen közös struct-mezővel nem rendelkezik szükségszerűen, teljesen független típusok. Az egyetlen közös pontjuk a Hangado trait, amit mindketten a saját logikájuk szerint implementálnak. Ez a lényege a trait-eknek: közös szerződést (interfészt) írnak le, nem közös adatszerkezetet.

Amikor egy típus nem implementálja a trait-et

Ha egy típus impl Trait for Tipus blokkot ír, de kihagy egy metódust, a fordító hibát jelez — jelenleg még nincs mód „alapértelmezett” megvalósítást megadni a trait-ben (ez a következő lecke témája), így minden metódust implementálni kell.

trait Hangado {
    fn hangot_ad(&self) -> String;
}

struct Hal {
    nev: String,
}

impl Hangado for Hal {}

fn main() {
    let hal = Hal { nev: String::from("Nemo") };
    println!("{}", hal.hangot_ad());
}
error[E0046]: not all trait items implemented, missing: `hangot_ad`
  --> src/main.rs:8:1
   |
8  | impl Hangado for Hal {}
   | ^^^^^^^^^^^^^^^^^^^^^^^ missing `hangot_ad` in implementation

Ez a hibaüzenet pontosan megmutatja, hogy melyik metódus hiányzik. A fordító tehát biztosítja, hogy ha egy típus azt állítja magáról, hogy egy trait-et implementál, akkor tényleg minden ígért viselkedést meg is ad.

Megjegyzés

Ha egy metódus szignatúrája nem egyezik pontosan a trait-ben deklarálttal (például más a visszatérési típus), azt a fordító egy külön hibaüzenettel jelzi — próbáld ki, mi történik, ha a hangot_ad visszatérési típusát &str-re változtatod!

Próbáld ki!

Vegyél elő egy üres Rust projektet (cargo new probald_ki), és a fenti Hangado trait mellé írj egy harmadik struct-ot, például Papagaj néven, nev: String mezővel. Implementáld rá a Hangado trait-et úgy, hogy a hangot_ad metódusa valami vicceset mondjon (pl. „X ismétli: Kraá!”). Futtasd le a main függvényben mindhárom típus egy-egy példányát, és nézd meg, hogy a kimenet valóban a saját megvalósítást hívja-e mindegyiknél.

Ha van kedved, próbálj ki egy második metódust is a trait-ben, például eletkor_evekben(&self) -> u8, és implementáld le mindhárom típusra — ez segít megérteni, hogy egy trait tetszőleges számú metódust tartalmazhat.

Önellenőrzés

Mi a különbség egy metódus és egy asszociált függvény között?

MegoldásA metódus paraméterlistája `&self` (vagy `&mut self`/`self`) paraméterrel kezdődik, és egy konkrét példányon hívjuk meg ponttal (`ertek.metodus()`). Az asszociált függvénynek nincs `self` paramétere, a típus nevén keresztül hívjuk (`Tipus::fuggveny()`), gyakran konstruktorként.

Miért kell egy típusnak minden trait-metódust implementálnia, ha az impl Trait for Tipus blokkot ír?

MegoldásMert a trait definíciója jelenleg csak a metódusok szignatúráját adja meg, alapértelmezett törzs nélkül — a fordító megköveteli, hogy minden ígért metódushoz konkrét megvalósítás tartozzon, különben `E0046` hibát kapunk. (Ez a következő leckében változik meg, amikor megismerjük az alapértelmezett metódusokat.)

Mit jelent a Self kulcsszó egy impl blokkban?

MegoldásA `Self` (nagy S-sel) az adott `impl` blokk aktuális típusára utal — ha az `impl Point { ... }` blokkban vagyunk, a `Self` egyenértékű a `Point` névvel, így nem kell mindenhol kiírni a konkrét típusnevet.

Összefoglalás és a következő lépés

  • A struct névvel ellátott mezőkből álló saját adattípus, az enum pedig egy zárt választási lehetőséget ír le variánsokkal.
  • Az impl blokkban adjuk hozzá a viselkedést egy típushoz: metódusokat (&self paraméterrel, ponttal hívva) és asszociált függvényeket (nincs self, Tipus::fuggveny() formában hívva).
  • A trait egy közös viselkedés szerződését írja le — a metódusok szignatúráját adja meg, a konkrét megvalósítást minden típus a saját impl Trait for Tipus blokkjában adja meg.
  • Ha egy típus nem implementálja a trait összes metódusát, a fordító E0046 hibával jelzi, mert jelenleg még nincs alapértelmezett megvalósítás.
  • A következő leckében („Alapértelmezett metódusok”) pontosan ezt a hiányt oldjuk fel: megnézzük, hogyan adhatunk a trait-ben kész, alapértelmezett törzset egy metódushoz, amit a típusok csak akkor kell felülírjanak, ha másra van szükségük.

Ha szeretnél mélyebben elmerülni a témában, ajánlott olvasmány: