Cél és előfeltételek

Ez a lecke gyakorlat: apró lépésekben megírjuk azokat a függvényeket, amelyek megmutatják, hogyan adhatunk kölcsön adatot anélkül, hogy átadnánk a tulajdonjogát. Az előző, „Ownership és move” leckében láttad, hogy egy nem-Copy érték átadásakor a régi változó használhatatlanná válik — a hívó fél elveszíti a hozzáférést. Ez sokszor túl szigorú, ha csak olvasni vagy ideiglenesen módosítani szeretnénk egy értéket.

A megoldás a referencia: &T olvasásra, &mut T írásra. Ehhez tartozik két szigorú borrow-szabály, és egy új típuskategória, a slice, amivel egy tömb vagy string egy darabjára hivatkozhatunk másolás nélkül.

Előfeltétel: ismerd az ownership-szabályokat, a move-szemantikát, a Copy/Clone közti különbséget, a String/&str viszonyát, és a for/if/match szerkezeteket.

Előkészítés

Hozz létre egy új projektet:

cargo new borrowing_practice
cd borrowing_practice

A Cargo.toml marad az alapértelmezett, nincs szükség külső függőségre:

[package]
name = "borrowing_practice"
version = "0.1.0"
edition = "2024"

[dependencies]

Minden lépést a src/main.rs-be írhatsz. A lecke végén egy teljes, összefüggő megoldást is találsz.

1. lépés: Kölcsönzés &T-vel

Ha egy String-et érték szerint adsz át egy függvénynek, a függvény átveszi a tulajdonjogot, és az eredeti változó használhatatlanná válik. Ha csak olvasni szeretnél, adj át egy referenciát helyette: &String, általánosabban &T. A függvény „kölcsönveszi” az adatot, és a scope végén automatikusan visszaadja.

Feladat: írj egy describe_length függvényt, amely &String-et vesz paraméterül, kiírja a hosszát (.len()), és hívd meg kétszer ugyanazzal a stringgel — bizonyítva, hogy a tulajdonjog nem száll el.

Megoldás
fn describe_length(s: &String) {
    println!("A string hossza: {} karakter", s.len());
}

fn main() {
    let name = String::from("Rustacean");

    describe_length(&name);
    describe_length(&name);

    println!("A név még mindig használható: {name}");
}

A &name egy referenciát ad át, nem az értéket magát. A describe_length tudja, hogy csak kölcsönbe kapta az adatot — a String a hívó félnél marad, a referencia csak addig él, amíg a függvény fut.

Megjegyzés

Valódi kódban paraméterként inkább &str-t használnánk, mert rugalmasabb. Itt szándékosan &String-gel kezdünk, a &str-re a slice-os résznél térünk vissza.

2. lépés: Mutable kölcsönzés &mut T-vel

Az &T csak olvasásra jó. Ha egy függvénynek módosítania is kell a kölcsönvett adatot, &mut Tmutable referencia — szükséges. Fontos: &mut referenciát csak mut változóra kaphatunk.

Feladat: írj egy append_exclamation függvényt, amely &mut String-et vesz, és .push_str()-tel egy "!"-et fűz a végéhez.

Megoldás
fn append_exclamation(s: &mut String) {
    s.push_str("!");
}

fn main() {
    let mut greeting = String::from("Szia");

    append_exclamation(&mut greeting);
    append_exclamation(&mut greeting);

    println!("{greeting}");
}

A &mut greeting azt jelenti: „kölcsönveheted, és módosíthatod, de végül visszaadod”. A main-ben greeting a hívások után is a tulajdonos marad — csak időlegesen írási jogot adtunk ki.

Tipp

A &T olyan, mintha egy könyvet adnál kölcsön elolvasásra — a másik nem írhat bele. A &mut T mintha egy füzetet adnál, amibe írhat, de végül visszakerül hozzád.

3. lépés: A borrow-szabályok

Ha korlátlanul osztogathatnál &mut referenciákat, két függvény egyszerre módosíthatna ugyanazt az adatot — pontosan ezt a hibaosztályt akarja kizárni a Rust fordítási időben. Két szigorú szabály van:

  1. Egy értékre egyszerre lehet több &T (immutable) referenciád — ezek békésen megférnek egymás mellett.
  2. Egy értékre egyszerre legfeljebb egy &mut T referenciád lehet, és amíg él, semmilyen más referencia (sem &T, sem &mut T) nem létezhet rá.

Feladat: írj egy main-t, amiben két immutable referenciád van ugyanarra a stringre (egyszerre használod egy println!-ben), majd — azután, hogy ezeket már nem használod — kérj egy mutable referenciát, és módosítsd az értéket.

Megoldás
fn main() {
    let mut text = String::from("Rust");

    let r1 = &text;
    let r2 = &text;
    println!("Két olvasás: {r1} és {r2}");

    let r3 = &mut text;
    r3.push_str("!");
    println!("Módosítva: {r3}");
}

A fordító nyomon követi, hol használjuk utoljára egy referenciát. Amint r1 és r2 utolsó használata (a println!) lezajlott, a kölcsönzésük lejár, és onnantól szabadon kérhetünk &mut-ot ugyanarra az értékre.

Ha r3-at próbálnánk használni még r1/r2 élő kölcsönzése alatt, a fordító hibát adna: valami olyasmit, hogy nem lehet mutable-ként kölcsönvenni, mert már immutable-ként is kölcsön van adva. Ez elsőre bosszantó, de pontosan ez akadályozza meg, hogy két rész egyszerre olvasson és írjon ugyanabba a memóriába.

Figyelem

A borrow-szabályok fordítási időben érvényesülnek — ezt a fordító-részt hívjuk borrow checkernek. A hibaüzenetei valójában segítő üzenetek, nem büntetések.

4. lépés: Slice-ok tömbökön

Eddig egész értékeket kölcsönöztünk. Mi van, ha egy tömb egy részére akarunk hivatkozni, másolás nélkül? Erre valók a slice-ok: a &[T] egy tömb (vagy másik slice) összefüggő darabjára mutató referencia, a hosszával együtt.

Feladat: írj egy largest függvényt, amely &[i32]-t kap, és visszaadja a legnagyobb elemet.

Megoldás
fn largest(numbers: &[i32]) -> i32 {
    let mut max = numbers[0];
    for &num in numbers {
        if num > max {
            max = num;
        }
    }
    max
}

fn main() {
    let numbers = [3, 7, 2, 9, 4];

    let whole_max = largest(&numbers);
    let partial_max = largest(&numbers[1..4]);

    println!("Teljes maximum: {whole_max}");
    println!("Részleges maximum: {partial_max}");
}

A &numbers a teljes tömbre mutató slice; a &numbers[1..4] csak az 1., 2. és 3. indexű elemeket fogja át (a felső határ nem tartalmazott). A largest szignatúrája &[i32], nem &[i32; 5] — így bármilyen hosszú tömbre, vagy annak részletére is működik.

5. lépés: Slice-ok stringeken

A &str valójában maga is slice: egy UTF-8 szövegre mutató referencia, hosszinformációval. Ezért lehet egy String-ből is kimetszeni egy darabot &s[start..end] szintaxissal, pontosan úgy, mint tömbökből.

Feladat: írj egy first_word függvényt, amely &str-t kap, és visszaadja az első szót (a szöveget az első szóközig), továbbra is &str-ként.

Megoldás
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    let mut end = s.len();

    for i in 0..bytes.len() {
        if bytes[i] == b' ' {
            end = i;
            break;
        }
    }

    &s[0..end]
}

fn main() {
    let sentence = String::from("Rust nagyon szeretem a borrow checkert");

    let word = first_word(&sentence);
    println!("Az első szó: {word}");
}

A first_word bemenete &str; a hívásnál &sentence (egy &String) automatikusan &str-ré alakul — ezt most csak fogadd el. A visszatérés (&s[0..end]) egy slice az eredeti stringbe, nem egy lemásolt string — amíg a word-öt használod, a sentence-nek életben kell maradnia.

Jó tudni

Egy slice sosem élhet tovább, mint az adat, amire mutat. Ha egy String megszűnne, amíg egy rá mutató slice még használatban van, a borrow checker hibát jelezne.

Teljes referencia-megoldás

Az alábbi program egyben tartalmazza a fenti lépéseket, egyetlen fordítható fájlban:

fn describe_length(s: &String) {
    println!("A string hossza: {} karakter", s.len());
}

fn append_exclamation(s: &mut String) {
    s.push_str("!");
}

fn largest(numbers: &[i32]) -> i32 {
    let mut max = numbers[0];
    for &num in numbers {
        if num > max {
            max = num;
        }
    }
    max
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    let mut end = s.len();

    for i in 0..bytes.len() {
        if bytes[i] == b' ' {
            end = i;
            break;
        }
    }

    &s[0..end]
}

fn main() {
    // &T: olvasás kölcsönzéssel
    let name = String::from("Rustacean");
    describe_length(&name);
    println!("A név még mindig használható: {name}");

    // &mut T: módosítás kölcsönzéssel
    let mut greeting = String::from("Szia");
    append_exclamation(&mut greeting);
    println!("{greeting}");

    // borrow-szabályok: több olvasás, majd egy írás
    let mut text = String::from("Rust");
    let r1 = &text;
    let r2 = &text;
    println!("Két olvasás: {r1} és {r2}");
    let r3 = &mut text;
    r3.push_str("!");
    println!("Módosítva: {r3}");

    // slice tömbön
    let numbers = [3, 7, 2, 9, 4];
    println!("Teljes maximum: {}", largest(&numbers));
    println!("Részleges maximum: {}", largest(&numbers[1..4]));

    // slice stringen
    let sentence = String::from("Rust nagyon szeretem a borrow checkert");
    println!("Első szó: {}", first_word(&sentence));
}

Bónusz-kihívások

Ha van még idő, próbáld megoldani ezeket megoldás nélkül:

  • Írj egy last_word függvényt, amely a first_word tükörképeként az utolsó szót adja vissza egy &str-ből — visszafelé is végigmehetsz a bájtokon egy csökkenő for ciklussal.
  • Írj egy swap_first_last függvényt, amely &mut [i32]-t kap, és felcseréli a tömb első és utolsó elemét — ügyelj arra, hogy csak egy mutable referenciát használj a slice-ra egyszerre.

Gratulálunk — vége az útnak!

Ezzel lezártad a „A Rust alapjai” útvonalat! A toolchain telepítésétől az első println!-en, a vezérlési szerkezeteken és az ownership-szabályokon át mostanra megvan az az alapszókincs, amire a nyelv minden további része épül: a referenciák és a slice-ok pontosan azt a hidat adják, amivel az ownership szigorú szabályai a napi gyakorlatban is kényelmesen kezelhetők maradnak.

Ez nem a végpont, hanem kiindulás — innen nyitva áll az út a saját típusok, a trait-ek vagy az aszinkron Rust felé. Most viszont dőlj hátra, és legyél büszke: a legtöbb kezdőnek éppen az ownership és a borrow a legnehezebb rész, és ezen már túl vagy.