Az előző leckében, az ellenőrző kvízben, alighanem összeraktad már a fejedben a borrow-szabályokat, a trait-eket és a generikákat egy nagyobb egésszé. A lifetime-ok pontosan ott lépnek be a képbe, ahol ezek találkoznak: amikor egy függvény vagy egy struct referenciákkal dolgozik, a Rust-nak garantálnia kell, hogy azok a referenciák sosem mutatnak majd "üres helyre" – olyan memóriára, amit már felszabadítottak. Eddig ezt a garanciát a borrow checker többnyire csendben, a hátunk mögött oldotta meg. Most megtanulod azt a szintaxist, amivel magad is elmondhatod neki, hogyan függenek össze a referenciák érvényességi idői.
Miért van szükség egyáltalán lifetime-okra?
Emlékezz vissza: egy &T referencia sosem élhet tovább, mint az az érték, amire mutat – ezt a scope és a drop kapcsán már láttuk. A probléma ott kezdődik, amikor egy függvény több referenciát kap paraméterként, és ezek közül visszaad egyet. A fordítónak ilyenkor tudnia kell, hogy a visszaadott referencia melyik bemenet érvényességi idejéhez van kötve – különben nem tudja garantálni, hogy a hívó kód biztonságosan használja majd az eredményt.
Nézzünk egy klasszikus esetet: egy függvényt, amely két string-szeletből visszaadja a hosszabbikat.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let s1 = String::from("hosszú szöveg");
let s2 = String::from("rövid");
let result = longest(s1.as_str(), s2.as_str());
println!("A hosszabb: {result}");
}
A 'a itt nem valamiféle konkrét időtartamot jelöl, hanem egy kapcsolatot ír le: "a visszaadott referencia legalább addig érvényes, ameddig x és y mindegyike érvényes". Ez nem futásidejű ellenőrzés – a 'a semmilyen extra kódot nem generál, tisztán fordítási időben létező annotáció, amit a borrow checker használ a bizonyításhoz.
A 'a nevet szabadon választhatnád akár 'x-nek vagy 'input-nak is – a betűk csak konvenció, a fordítónak nem az elnevezés számít, hanem a paraméterek és a visszatérési érték közti megfeleltetés.
A 'a szintaxis: mit jelöl pontosan?
Ha kihagyjuk a lifetime annotációt a longest függvényből, a fordító nem találgat – hibát dob:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
error[E0106]: missing lifetime specifier
--> src/main.rs:1:38
|
1 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
Ez a hibaüzenet lényegében pontosan megmondja a megoldást: mivel a függvény két bemeneti referencia közül választ, és a fordító nem tudja eldönteni, melyikhez tartozik a kimenet, neked kell kimondanod, hogy mindkettő ugyanazt a 'a lifetime-ot kapja. Ezzel azt ígéred: a visszatérési érték nem élhet tovább, mint a rövidebb életű bemenet.
Fontos, hogy a 'a a paraméterlistában és a visszatérési típusban ugyanazt a lifetime-ot jelöli – ez egy generikus paraméter, hasonlóan a <T>-hez, csak nem típusra, hanem érvényességi időre vonatkozik. Éppen ezért a <'a> a <T>-hez hasonlóan a függvénynév után, hegyes zárójelben szerepel.
Lifetime elision: amikor a fordító helyetted gondolkodik
Ha minden referenciát tartalmazó függvénynél ki kellene írnod a 'a-t, a Rust-kód gyorsan olvashatatlanná válna. Szerencsére a fordító három egyszerű szabály (az úgynevezett lifetime elision-szabályok) alapján a legtöbb esetben magától kitalálja az annotációt:
- Minden referencia-paraméter saját, külön lifetime-ot kap.
- Ha pontosan egy bemeneti referencia van, annak lifetime-ja lesz a kimenet lifetime-ja is.
- Ha a paraméterek között van
&selfvagy&mut self(tehát metódusról van szó), akkor aselflifetime-ja lesz a kimenet lifetime-ja.
Nézzünk egy függvényt, ahol pontosan a 2. szabály miatt nem kell semmit kiírnod:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
s
}
fn main() {
let sentence = String::from("Rust lifetime-ok haladóknak");
let word = first_word(&sentence);
println!("Első szó: {word}");
}
A fordító itt magától úgy "látja" a szignatúrát, mintha ezt írtad volna:
fn first_word<'a>(s: &'a str) -> &'a str {
// ...
s
}
Ez épp az elision lényege: nem arról van szó, hogy ilyenkor nincs lifetime, hanem arról, hogy a szabály alapján egyértelműen kitalálható, így nem kötelező kiírni. A longest esetében viszont két bemeneti referencia volt, ezért a 2. szabály nem alkalmazható – a fordító nem tudja, melyik paraméterhez kötődik a kimenet, ezért kéri az explicit annotációt.
Ökölszabályként: ha a hibaüzenetben E0106-ot (missing lifetime specifier) látsz, majdnem biztos, hogy több bemeneti referencia van, és a fordító nem tudja eldönteni, melyikhez kösse a kimenetet. Ilyenkor te mondod meg neki explicit 'a annotációval.
Struct-lifetime: amikor egy struct referenciát tárol
Eddig csak függvényeket néztünk, de a struct-ok is tárolhatnak referenciát mezőként – ehhez azonban a struct-nak is szüksége van egy lifetime paraméterre, ugyanúgy, ahogy a generikáknál a <T>-re volt szükség. Ha egy struct mezője &str vagy bármilyen más referencia, a Rust nem engedi meg, hogy a struct-nak ne legyen tudása arról, milyen hosszan él az, amire mutat.
struct Wrapper {
text: &str,
}
fn main() {
let s = String::from("példa szöveg");
let w = Wrapper { text: &s };
println!("{}", w.text);
}
error[E0106]: missing lifetime specifier
--> src/main.rs:2:11
|
2 | text: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 | struct Wrapper<'a> {
2 | text: &'a str,
|
A javítás pontosan azt teszi, amit a hibaüzenet javasol: a struct maga is kap egy 'a lifetime paramétert, és minden benne tárolt referencia ehhez kötődik.
struct Excerpt<'a> {
part: &'a str,
}
impl<'a> Excerpt<'a> {
fn announce(&self, prefix: &str) -> &str {
println!("{prefix}");
self.part
}
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
s
}
fn main() {
let novel = String::from("Élt egyszer egy sárkány. Messze innen.");
let first_sentence = first_word(&novel);
let excerpt = Excerpt { part: first_sentence };
let announced = excerpt.announce("Figyelem, kivonat jön:");
println!("Kivonat: {announced}");
}
Itt két dolog is történik egyszerre. Egyrészt az Excerpt<'a> struct kimondja: "nem élhetek tovább, mint az a &str, amit a part mezőben tárolok". Ez a borrow checker számára garantálja, hogy sosem keletkezhet olyan Excerpt, amely egy már megszűnt String-re mutatna.
Másrészt az announce metódus szignatúrájában két referencia is szerepel (&self és prefix: &str), de nem kellett kiírni a lifetime-okat – ez a 3. elision-szabály miatt van: mivel van &self, a fordító automatikusan a self lifetime-jához köti a kimenetet, függetlenül attól, hogy prefix-nek is van saját lifetime-ja.
Ha egy impl blokkban lifetime-os struct-hoz írsz metódust, magát az impl-t is meg kell jelölni: impl<'a> Excerpt<'a>. Ez könnyen elfelejthető, és a fordító ilyenkor is világos hibaüzenetet ad – de érdemes már most rögzíteni, hogy ez a szintaxis mindig együtt jár.
Gyakorlat: egy tipikus lifetime hiba és a javítása
Nézzünk egy olyan esetet, ahol a hiba oka nem hiányzó annotáció, hanem az, hogy egy referencia rövidebb életű, mint amire a struct vagy a függvény számít:
struct Excerpt<'a> {
part: &'a str,
}
fn make_excerpt() -> Excerpt<'static> {
let text = String::from("ideiglenes szöveg");
Excerpt { part: &text }
} // `text` itt megszűnik, de az Excerpt még "élne"
error[E0515]: cannot return value referencing local variable `text`
--> src/main.rs:5:5
|
5 | Excerpt { part: &text }
| ^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function
A hiba oka nem az annotáció hiánya, hanem a valóság: a text a függvény végén, a scope lezárásakor drop-olódik, tehát semmiképp nem élhet olyan hosszan, mint amit a 'static lifetime kér. A javítás itt nem lifetime-trükk, hanem architekturális döntés: vagy a hívótól kapott, hosszabb életű &str-t adjuk tovább, vagy a struct helyett egy önálló String-et tárolunk, amelynek saját ownership-je van:
struct Excerpt {
part: String,
}
fn make_excerpt() -> Excerpt {
let text = String::from("ideiglenes szöveg");
Excerpt { part: text }
}
fn main() {
let excerpt = make_excerpt();
println!("{}", excerpt.part);
}
Ez a példa jól mutatja a lifetime-ok igazi célját: nem korlátoznak, hanem kimondják azt, ami a memóriakezelésben már korábban is igaz volt. Ha egy referencia nem élhet elég hosszan, a megoldás gyakran nem egy újabb annotáció, hanem az, hogy a struct inkább birtokolja az adatot (String), ne csak kölcsönvegye (&str).
Próbáld ki!
Írj egy shortest<'a>(x: &'a str, y: &'a str) -> &'a str függvényt, amely a két bemenet közül a rövidebbet adja vissza (egyenlő hossz esetén az elsőt). Ezután írj egy struct Pair<'a> { first: &'a str, second: &'a str } struct-ot és egy hozzá tartozó shorter(&self) -> &str metódust, amely a shortest függvényt hívja meg a két mezővel. Figyeld meg, hogy a metódusnál kell-e explicit lifetime annotáció, és ha nem, melyik elision-szabály miatt.
Önellenőrzés
Megoldás
Előbb próbáld megválaszolni magadban, csak utána nyisd ki!1. Miért nem fordul le a longest függvény lifetime annotáció nélkül, ha first_word-nél nem kellett kiírni semmit?
Megoldás
A `first_word`-nek egyetlen bemeneti referenciája van, így a 2. elision-szabály (egy bemenet lifetime-ja = kimenet lifetime-ja) egyértelműen alkalmazható. A `longest`-nek két bemeneti referenciája van, és a fordító nem tudhatja, a kimenet melyikhez kötődik – ezért kell explicit `'a`-val kimondani, hogy mindkettőhöz egyszerre kötődik.2. Mit jelent a lifetime elision harmadik szabálya, és miért releváns metódusoknál?
Megoldás
Ha a paraméterlistában szerepel `&self` vagy `&mut self`, a fordító a kimeneti referencia lifetime-ját automatikusan a `self` lifetime-jához köti, még akkor is, ha van más referencia-paraméter is. Ez azért praktikus, mert a metódusok túlnyomó része pontosan ezt a mintát követi: valamit visszaad a saját belső állapotából.3. Miért nem lehet egy Excerpt<'a> struct-ból 'static életű értéket visszaadni egy lokális String-ből képzett &str-rel?
Megoldás
Mert a lokális `String` a függvény scope-jának végén drop-olódik, tehát a rá mutató referencia legfeljebb addig érvényes, ameddig a `String` létezik – ez pedig sokkal rövidebb, mint a `'static` (a program teljes futása). A borrow checker ezt pontosan felismeri, és nem engedi a \"dangling reference\" létrehozását.Összefoglalás és mi jön legközelebb
- A
'alifetime-annotáció nem futásidejű mechanizmus, hanem fordítási időben létező jelölés, amely megmutatja a borrow checkernek, mely referenciák érvényességi ideje függ össze. - A lifetime elision három szabálya (saját lifetime minden paraméternek, egy bemenet → kimenet,
&self→ kimenet) miatt a mindennapi kódban ritkán kell explicit'a-t írnod. - Amint egy struct referenciát tárol mezőként, a struct-nak is szüksége van egy lifetime paraméterre (
struct Excerpt<'a> { part: &'a str }), és azimplblokkot is meg kell jelölni ugyanazzal. - Ha egy lifetime-hiba oka nem hiányzó annotáció, hanem valódi életciklus-probléma (pl. lokális változóra mutató referencia visszaadása), a megoldás gyakran nem trükk, hanem ownership: a struct tárolja inkább
String-ként az adatot, ne&str-ként. - A következő leckében elhagyjuk a lifetime-okat, és a típusos hibakezelés felé fordulunk: megnézzük a
Resulttípust, a?operátort és athiserrorcrate-et, amivel idiomatikus, jól olvasható hibakezelést írhatsz.
Ha szeretnél még mélyebbre menni, ezek jó kiindulópontok: