Cél és előfeltétel

Egy könyvtári függvény gyakran ugyanazt a feladatot többféle szabállyal végzi. Az egyik jelentés nagybetűs nevet kér, a másik előtagot illeszt hozzá; az ellenőrzések pedig eltérő feltételeket vizsgálnak. A cél nem az összes lehetséges viselkedés előre beépítése. Egy kicsi trait segítségével a hívó adhatja át azt a műveletet, amelyre az adott helyzetben szükség van. A hibák és forrásláncok után most a felület másik oldalát, a bejövő viselkedés szerződését vizsgáljuk.

Egy szerződés, kétféle kiválasztás

A T: Trait megkötésnél a konkrét típust a fordító ismeri. A függvény minden egyes hívásában egy meghatározott T szerepel, és az adott példányosításra a trait által ígért műveletek használhatók. Ez jól illik az olyan algoritmushoz, amelynek lépései rögzítettek, de egy együttműködő objektum típusa változhat. Nem szükséges hozzá öröklési hierarchiát készíteni vagy minden implementációt egyetlen központi enumba felsorolni.

A &dyn Trait ezzel szemben futás közben használható trait objektumot kölcsönöz. A hívó a közös felületet látja, a konkrét típus rejtve marad. A Box<dyn Trait> tulajdonolt változat: különböző implementációkat lehet azonos elemtípusú vektorban tárolni. A doboz tárolási döntés, a dyn pedig dinamikus metódusválasztást jelent. A kettőt érdemes külön gondolatként kezelni.

Ebből nem következik általános gyorsasági sorrend. A döntés első kérdése az legyen, hogy a konkrét típus ismert-e a hívás helyén, illetve különböző típusú értékeket kell-e együtt tárolni. A végrehajtási költség csak a tényleges feladat és mérés alapján értékelhető. A leckében nincs benchmark, és a kis szöveges példákból nem vonunk le teljes alkalmazásokra vonatkozó teljesítményállítást.

A trait mérete közvetlenül hat a cserélhetőségre. Ha a formázás mellé naplózást, fájlmegnyitást és konfigurációmentést is előírnál, minden egyszerű formázónak ezeket is meg kellene valósítania. A példák ezért egyetlen műveletet kérnek. Ez nem általános tilalom a nagyobb felületekre: inkább annak ellenőrzése, hogy az együtt kért műveletek valóban ugyanahhoz a szerephez tartoznak-e. A hívó által készített apró tesztimplementáció jó visszajelzés arról, mennyi szükségtelen kötelezettséget ró rá a könyvtár.

Az eredmény tulajdonát is tudatosan választottuk. A formázók új String értéket adnak vissza, ezért az eredmény nem kölcsönöz a stratégiából. A hívó a stratégia későbbi eldobása után is használhatja a kapott szöveget. Más szerződés is lehetséges, például írás egy átadott pufferbe, de az más hibákat és élettartamokat hozna. Mielőtt általánosítasz, írd le egy valódi hívás formáját: hol van az adat, ki használja legközelebb, és kinek kell birtokolnia az elkészült eredményt.

1. feladat: generikus névformázás

Készíts két formázót ugyanazzal a Name traittel. A generikus függvény kölcsönözze a formázót, és adjon vissza új szöveget. Így a választott stratégia nem fogy el az első híváskor. Ellenőrizd az üres bemenetet is: a két implementáció eltérő, de előre meghatározott eredményt adjon rá.

Megoldás
trait Name { fn render(&self, input: &str) -> String; }
struct Upper;
impl Name for Upper {
    fn render(&self, input: &str) -> String { input.to_uppercase() }
}
struct Prefix;
impl Name for Prefix {
    fn render(&self, input: &str) -> String { format!("név:{input}") }
}
fn present<T: Name>(rule: &T, input: &str) -> String { rule.render(input) }
fn main() {
    assert_eq!(present(&Upper, "alma"), "ALMA");
    assert_eq!(present(&Upper, ""), "");
    assert_eq!(present(&Prefix, "alma"), "név:alma");
    assert_eq!(present(&Prefix, ""), "név:");
}

Mikor használható a dyn felület?

Nem minden trait alakítható trait objektummá. A dinamikusan hívható metódusoknak olyan aláírás kell, amely a konkrét megvalósító típus ismerete nélkül is használható. Az itt választott &self, &str és bool vagy String típusok ezt lehetővé teszik. Egy általános típusparaméteres metódus új kérdést nyitna; nem elég egyszerűen dyn szót írni elé. A traitet ezért az átadott feladat köré tervezzük, kis és következetes műveletekkel.

2. feladat: különböző ellenőrzők egy listában

Tárolj egy hosszellenőrzőt és egy tiltott karaktert vizsgáló szabályt közös vektorban. A sorrend maradjon a vektor sorrendje. A karakterhossz Unicode skalárértékeket számoljon, ne bájtokat; ez még nem látható grafémák számolása. Az összes szabályt kérdezd le, így a felület visszaadhatja a külön eredményeket.

Megoldás
trait Rule { fn accepts(&self, text: &str) -> bool; }
struct MinChars(usize);
impl Rule for MinChars {
    fn accepts(&self, text: &str) -> bool { text.chars().count() >= self.0 }
}
struct NoBang;
impl Rule for NoBang {
    fn accepts(&self, text: &str) -> bool { !text.contains('!') }
}
fn main() {
    let rules: Vec<Box<dyn Rule>> = vec![Box::new(MinChars(2)), Box::new(NoBang)];
    let check = |s: &str| rules.iter().map(|r| r.accepts(s)).collect::<Vec<_>>();
    assert_eq!(check("ár"), vec![true, true]);
    assert_eq!(check("!"), vec![false, false]);
    assert_eq!(check(""), vec![false, true]);
}

3. feladat: kölcsönzött stratégia, saját állapottal

A dinamikus híváshoz ne mindig foglalj dobozt. Készíts helyi, saját előtagot tároló formázót, és add át &dyn paraméterként. A függvénynek nem kell birtokolnia a stratégiát. A hívás után az eredeti érték tovább használható. Vizsgáld meg az üres előtagot is, amelynél a bemenet változatlan tartalmú új szövegként tér vissza.

Megoldás
trait Render { fn render(&self, input: &str) -> String; }
struct Prefix(String);
impl Render for Prefix {
    fn render(&self, input: &str) -> String { format!("{}{input}", self.0) }
}
fn apply(rule: &dyn Render, input: &str) -> String { rule.render(input) }
fn main() {
    let prefix = Prefix(String::from("jelentés: "));
    assert_eq!(apply(&prefix, "kész"), "jelentés: kész");
    assert_eq!(prefix.0, "jelentés: ");
    assert_eq!(apply(&Prefix(String::new()), "adat"), "adat");
    assert_eq!(apply(&prefix, ""), "jelentés: ");
}

Elfogadási feltételek

  1. A generikus függvény legalább két konkrét implementációval működik, és a kölcsönzött stratégiát újra lehet használni.
  2. A heterogén vektor két eltérő típust tartalmaz; eredményei ellenőrzött sorrendben maradnak.
  3. A dinamikus, kölcsönzött változat helyi állapottal működik, szükségtelen tulajdonátadás nélkül.

Továbblépés

Olvasd el a függvény aláírását a hívó helyéből is: milyen tulajdont ad át, mit tarthat meg, és melyik műveletet helyettesítheti? A validált rendelés miniprojektben ezek a döntések találkoznak a korábbi típushatárokkal. A cél ott is egy kicsi, érthető szerződés lesz, amelyre a tesztek érdemi állításokat tudnak megfogalmazni.