পাঠ ১০.৩

Lifetime দিয়ে reference validate করা

Validating References with Lifetimes

Lifetime এক ধরনের generic — কিন্তু type না, একটা reference কতদূর পর্যন্ত valid সেটা describe করে। বেশিরভাগ সময় Rust implicitly infer করে; কিন্তু সম্ভাব্য একাধিক relationship থাকলে আমাদের explicitly বলে দিতে হয়। Lifetime-এর মূল লক্ষ্য — dangling reference prevent করা

Dangling reference

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {r}");
}
compile errortext
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
5 |         let x = 5;
  |             - binding `x` declared here
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed

x inner block-এর শেষে drop। কিন্তু r তখনো বাইরে — তার reference invalid হত। Rust borrow-checker এই সম্পর্ক compile time-এ চেক করে।

Lifetime diagram

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+

'b (x-এর lifetime) 'a (r-এর lifetime)-এর চেয়ে ছোট। Reference-এর জন্য এটা violation — borrowed data borrower-এর চেয়ে দীর্ঘ বাঁচতে হবে।

উল্টোটা valid:

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          // --+       |
}                         // ----------+

Function-এ generic lifetime

এই function লিখব — দুটো string slice-এর মধ্যে যেটা বড়, সেটা return:

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
compile errortext
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | 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`

Compiler বলছে — return-এর reference x থেকে আসছে না y থেকে, signature-এ নেই। Borrow checker bound check করতে পারছে না।

Lifetime annotation syntax

Lifetime parameter apostrophe ' দিয়ে শুরু, সাধারণত ছোট nameing যেমন 'a। Reference-এ &-এর পরে:

&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

Lifetime annotation reference-এর actual lifetime change করে না — শুধু একটা relationship describe করে।

longest — fixed

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

মানে — "এমন একটা lifetime 'a আছে যেখানে x ও y দু'জনেই অন্তত ততটুকু বাঁচে, এবং return-করা reference-ও ততটুকু বাঁচে"। Returned reference-এর effective lifetime হবে input দু'টোর মধ্যে যেটা ছোট

Compile-হওয়া example

fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {result}");
    }
}

result ব্যবহার হচ্ছে inner block-এর ভিতরে — যেখানে দু'টোই valid। Compiles।

Fail-করা example

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {result}");
}
compile errortext
error[E0597]: `string2` does not live long enough

string2 inner block-এর শেষে drop। result সেটার reference হতে পারত (bigger পেলে) — পরে use unsafe। Rust সরাসরি block।

কখন কোন param-এ lifetime

সব reference-এ same lifetime দরকার নেই। যদি function কোনো particular parameter-ই return করে — তাহলে শুধু সেটাকেই annotate:

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

এখানে y-এর lifetime নিয়ে concern নেই। Return-এর reference শুধু x-এর সাথে tied।

Local data-র reference return — সবসময় ভুল:

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}
compile errortext
error[E0515]: cannot return value referencing local variable `result`

Function শেষ হলে result drop — তার reference dangling। সমাধান: reference না, owned String return।

Struct-এ lifetime

Struct-এ reference field রাখতে চাইলে lifetime annotate দরকার:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Instance-টা তার field-এর reference-এর চেয়ে বেশিদিন বাঁচতে পারবে না। Compiler enforce করে।

Lifetime elision rules

আগে আমরা first_word(s: &str) -> &str লিখেছি কোনো annotation ছাড়াই। কীভাবে compile হলো?

Compiler কিছু সহজ pattern recognize করে — সেগুলোতে annotation automatic infer। তিনটা rule:

  1. Rule 1 (input): প্রতিটা reference parameter আলাদা lifetime পায়। fn f(x: &i32, y: &i32)fn f<'a, 'b>(x: &'a i32, y: &'b i32)
  2. Rule 2 (single input): ঠিক একটাই input lifetime থাকলে, সেটাই সব output-এ apply। fn f(x: &i32) -> &i32fn f<'a>(x: &'a i32) -> &'a i32
  3. Rule 3 (method): Multiple input lifetime, কিন্তু একটা &self বা &mut self — তখন self-এর lifetime সব output-এ।

first_word(s: &str) -> &str rule 1 একটা input lifetime দেয়, rule 2 সেটাই output-এ — হয়ে গেল।

longest(x: &str, y: &str) -> &str — rule 1 দু'টা lifetime, rule 2 apply হয় না (multiple input), rule 3 apply হয় না (no self)। তাই compile error।

Method-এ lifetime

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> declare করতে হয়; &self-এর lifetime elision-এ infer।

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

Rule 3-এ output-এর lifetime self-এর সাথে tied — কোনো explicit annotation দরকার নেই।

'static lifetime

Special lifetime 'static — পুরো program চলাকালীন valid। সব string literal-এর lifetime 'static:

let s: &'static str = "I have a static lifetime.";

কারণ — literal-এর data executable-এর binary-তে hardcoded।

সতর্কতা: error message-এ "consider 'static" দেখলে সাবধান। অধিকাংশ সময় আসল সমস্যা — কোথাও dangling reference, যেটা fix করতে হবে। শুধু 'static বসিয়ে compiler-কে চুপ করানো ভুল।

তিনটাই একসাথে

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {ann}");
    if x.len() > y.len() { x } else { y }
}

Generic type T + lifetime 'a + trait bound T: Display — সব এক signature-এ।

এই পাঠ থেকে যা শিখলে

  • Lifetime — reference কতদূর valid তার generic; main goal dangling prevent।
  • Annotation syntax — &'a T; relationship describe করে, change না।
  • Function-এ একাধিক reference + reference return → annotation দরকার; single input → elision-এ ok।
  • Struct-এ reference field → struct-এ lifetime parameter।
  • তিনটা elision rule — input, single-input, method।
  • 'static = program-জীবনের জন্য valid; শুধু literal-এর জন্য সরাসরি, অন্যত্র সাবধানে।
  • Generic + trait bound + lifetime সব এক signature-এ মিলতে পারে।