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
selfparamé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ányosannewné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.
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.
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ás
A 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ás
Mert 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ás
A `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
structnévvel ellátott mezőkből álló saját adattípus, azenumpedig egy zárt választási lehetőséget ír le variánsokkal. - Az
implblokkban adjuk hozzá a viselkedést egy típushoz: metódusokat (&selfparaméterrel, ponttal hívva) és asszociált függvényeket (nincsself,Tipus::fuggveny()formában hívva). - A
traitegy 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átimpl Trait for Tipusblokkjában adja meg. - Ha egy típus nem implementálja a trait összes metódusát, a fordító
E0046hibá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: