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.
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 T — mutable 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.
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:
- Egy értékre egyszerre lehet több
&T(immutable) referenciád — ezek békésen megférnek egymás mellett. - Egy értékre egyszerre legfeljebb egy
&mut Treferenciá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.
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.
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_wordfüggvényt, amely afirst_wordtükörképeként az utolsó szót adja vissza egy&str-ből — visszafelé is végigmehetsz a bájtokon egy csökkenőforciklussal. - Írj egy
swap_first_lastfü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.