Cél és előfeltétel

Ebben a leckében egy kis könyvtár adatait modellezed. A végére el tudod dönteni, mikor tartoznak össze mezők egy structban, és mikor érdemes egymást kizáró állapotokat enummal kifejezni. Nem adatbázist készítünk: a cél egy olyan memóriabeli modell, amelynek jelentése a függvények aláírásából és a típusokból is olvasható.

Előfeltétel A Rust alapjai elvégzése, különösen az ownership és move megértése. Szükséged lesz függvényekre, változókra és egyszerű elágazásokra. A példákat külön-külön másold egy edition 2024 projekt src/main.rs fájljába, majd futtasd a cargo run parancsot. Az assert hibája nem dísz: azt jelzi, hogy egy elvárt viselkedés nem teljesül.

Először a kérdéseket tisztázd

Egy könyvről tudni szeretnénk a címét és azt, hogy kikölcsönözték-e. Kölcsönzéskor az olvasó neve is szükséges. Ha külön szabad és kolcsonozve logikai mezőt használnánk, mindkettőt igazra is állíthatnánk. A program ekkor olyan állapotot tárolna, amelynek nincs értelmes jelentése. Egy harmadik, mindig jelen lévő olvasónév tovább növelné az ellenőrizendő kombinációk számát.

A struct az összetartozó adatokat fogja össze, az enum pedig felsorolja az alternatívákat. A szabad könyvhöz nem tartozik olvasónév; a kölcsönzött változat viszont hordozza azt. Ettől még a név lehet üres szöveg: a típusok itt a szerkezetet rendezik, nem minden üzleti szabályt ellenőriznek automatikusan. Az üres nevek kezelését külön megírjuk.

#[derive(Debug, PartialEq, Eq)]
enum Allapot {
    Szabad,
    Kolcsonozve { olvaso: String },
}

#[derive(Debug)]
struct Konyv {
    cim: String,
    allapot: Allapot,
}

fn leiras(konyv: &Konyv) -> String {
    match &konyv.allapot {
        Allapot::Szabad => format!("{}: elérhető", konyv.cim),
        Allapot::Kolcsonozve { olvaso } => {
            format!("{}: kölcsönző: {olvaso}", konyv.cim)
        }
    }
}

fn main() {
    let szabad = Konyv {
        cim: String::from("A Pál utcai fiúk"),
        allapot: Allapot::Szabad,
    };
    let foglalt = Konyv {
        cim: String::from("Abigél"),
        allapot: Allapot::Kolcsonozve {
            olvaso: String::from("Eszter"),
        },
    };
    assert_eq!(leiras(&szabad), "A Pál utcai fiúk: elérhető");
    assert_eq!(leiras(&foglalt), "Abigél: kölcsönző: Eszter");
    println!("{}", leiras(&foglalt));
    assert_eq!(foglalt.cim, "Abigél");
}

A match mindkét esetet kezeli. Az olvasó neve csak abban az ágban hozzáférhető, ahol valóban kölcsönzött könyvet kaptunk. Ha később új változatot vezetsz be, a hiányzó ág fordítási hibát okoz. Ez hasznos emlékeztető: a modell bővítése után a viselkedést is újra át kell gondolni.

Megnézéshez kölcsönözz, módosításhoz kérj engedélyt

A leiras paramétere &Konyv, ezért nem veszi át a könyv tulajdonjogát. A hívás után a cím és az állapot továbbra is elérhető. A match &konyv.allapot szintén kölcsönöz; nem próbálja kivenni a String értéket a kölcsönzött könyvből. A visszatérő String új, saját szöveg, amely a könyvtől függetlenül használható.

Olvasás és módosítás külön feladat. Egy állapotot megváltoztató függvénynek &mut Konyv kell, de egy címke elkészítésének nem. Ne adj minden függvénynek módosítható referenciát kényelmi okból. A szűkebb aláírás az olvasónak is megmutatja, hogy mely műveletek hagyják változatlanul az adatot. A Debug a hibakeresést segíti, nem felhasználói felületet tervez helyetted.

A tesztesetek kiválasztásakor ne csak a képernyőre írt szöveget nézd. Kérdezd meg, mi marad igaz a művelet után: ugyanaz a könyv szerepel-e a nyilvántartásban, megmaradt-e a kölcsönző, és elutasításkor változott-e bármi? Egy sikeres és egy sikertelen hívás egymás után gyakran többet árul el, mint két külön, frissen létrehozott rekord. Ezért az utolsó feladatban ugyanazon a könyvön próbálkozunk ismét. A megjelenítő függvényeknél viszont tudatosan több külön példányt használunk, hogy az enum mindegyik ágát ellenőrizzük. Mielőtt lenyitod a megoldást, írd le saját szavaiddal a várt eredményt; utána fordítsd ezt konkrét állítássá.

1. feladat – Különítsd el az adatot a megjelenítésétől

Készíts Kiadvany structot címmel és oldalszámmal. Írj hozzá cimke függvényt, amely kölcsönöz, és például az „Abigél (240 oldal)” szöveget adja vissza. A nulla oldalszámot most ismeretlen terjedelemként engedjük meg: jelenjen meg, ne állítsa le a programot. Ellenőrizd azt is, hogy a címke elkészítése után az eredeti cím olvasható marad.

Megoldás
struct Kiadvany {
    cim: String,
    oldalak: u16,
}

fn cimke(kiadvany: &Kiadvany) -> String {
    format!("{} ({} oldal)", kiadvany.cim, kiadvany.oldalak)
}

fn main() {
    let konyv = Kiadvany { cim: String::from("Abigél"), oldalak: 240 };
    let ismeretlen = Kiadvany { cim: String::from("Jegyzet"), oldalak: 0 };
    assert_eq!(cimke(&konyv), "Abigél (240 oldal)");
    assert_eq!(cimke(&ismeretlen), "Jegyzet (0 oldal)");
    assert_eq!(konyv.cim, "Abigél");
}

A nulla jelentését előre rögzítettük, ezért az elfogadása itt tudatos döntés. Más alkalmazásban a nullát hibának tekinthetnéd. Az adatmodell nem dönt helyetted; a teszt azt dokumentálja, hogy ebben a feladatban melyik értelmezést választottuk.

2. feladat – Dolgozd fel az enumhoz tartozó adatot

Modellezz két átvételi módot: személyes átvételt és postázást. Csak a postázáshoz tartozzon irányítószám. A leíró függvény mindkét esetben készítsen értelmes szöveget, és ne fogyassza el az átvételi módot. Egy harmadik, valamennyi esetet elfedő match ágat ne használj: szeretnénk észrevenni a későbbi bővítéseket.

Megoldás
#[derive(Debug, PartialEq, Eq)]
enum Atvetel {
    Szemelyes,
    Posta { iranyitoszam: String },
}

fn leiras(atvetel: &Atvetel) -> String {
    match atvetel {
        Atvetel::Szemelyes => String::from("Átvétel a könyvtárban"),
        Atvetel::Posta { iranyitoszam } => format!("Postázás: {iranyitoszam}"),
    }
}

fn main() {
    let szemelyes = Atvetel::Szemelyes;
    let posta = Atvetel::Posta { iranyitoszam: String::from("0100") };
    assert_eq!(leiras(&szemelyes), "Átvétel a könyvtárban");
    assert_eq!(leiras(&posta), "Postázás: 0100");
    assert_eq!(posta, Atvetel::Posta { iranyitoszam: String::from("0100") });
}

Az irányítószám azonosító, ezért szöveg: a kezdő nulla megmarad. A példa nem ellenőrzi, hogy létezik-e ilyen postai körzet, és nem vállal országfüggetlen címellenőrzést. A változatban tárolás azt biztosítja, hogy személyes átvételhez ne kelljen üres irányítószámot gyártanunk.

3. feladat – Engedj meg egyetlen szabályos állapotváltást

Írj kölcsönzést végző függvényt. Szabad könyvet, nem üres olvasónévvel lehessen kikölcsönözni; a név széléről távolítsd el a szóközöket. A visszatérési érték jelezze, történt-e módosítás. Sikertelen próbálkozáskor a korábbi állapot maradjon meg. Vizsgáld az üres nevet és a második kölcsönzési kísérletet is.

Megoldás
#[derive(Debug, PartialEq, Eq)]
enum Allapot {
    Szabad,
    Kolcsonozve { olvaso: String },
}

struct Konyv {
    allapot: Allapot,
}

fn kolcsonoz(konyv: &mut Konyv, olvaso: &str) -> bool {
    let olvaso = olvaso.trim();
    if olvaso.is_empty() || !matches!(&konyv.allapot, Allapot::Szabad) {
        return false;
    }
    konyv.allapot = Allapot::Kolcsonozve { olvaso: olvaso.to_owned() };
    true
}

fn main() {
    let mut konyv = Konyv { allapot: Allapot::Szabad };
    assert!(!kolcsonoz(&mut konyv, "  "));
    assert_eq!(konyv.allapot, Allapot::Szabad);
    assert!(kolcsonoz(&mut konyv, " Eszter "));
    assert!(!kolcsonoz(&mut konyv, "Bence"));
    assert_eq!(konyv.allapot, Allapot::Kolcsonozve {
        olvaso: String::from("Eszter"),
    });
}

Előbb ellenőrzünk, és csak utána módosítunk. Így az elutasított művelet nem törli a korábbi kölcsönzőt. A bool itt elegendő a változás jelzésére; ha a hiba okát is szeretnénk közölni, beszédesebb visszatérési típusra lesz szükség.

Elfogadási feltételek

  1. A három megoldás külön-külön lefut, és minden assert teljesül, beleértve a nulla oldalszámot és a kezdő nullás azonosítót.
  2. A csak olvasó függvények kölcsönöznek; ugyanaz az adat legalább kétszer feldolgozható másolás nélkül.
  3. Az üres névvel és már kölcsönzött könyvvel végzett próbálkozás nem módosítja az állapotot.

Összefoglaló és továbblépés

A jól megválasztott mezők megnevezik az adatokat, az enum pedig láthatóvá teszi az egymást kizáró eseteket. A műveletek aláírása különbséget tesz megfigyelés és változtatás között. Ez még kis példában is csökkenti a fejben tartandó feltételeket. Következőként az Option, Result és adatellenőrzés leckében a hiányt és a hibát is külön típussal írjuk le, hogy a hívó ne csak egy igaz vagy hamis értékből találgasson.

Elsődleges források