مقدمه

در درس‌های گذشته با دو مفهوم قدرتمند یعنی نوع‌های جنریک و Traitها آشنا شدیم. در این درس به سراغ سومین مفهوم کلیدی و منحصر به فرد Rust می‌رویم: «طول عمر» یا Lifetime. هدف اصلی سیستم lifetime، جلوگیری از «رفرنس‌های آویزان» (dangling references) است. یک رفرنس آویزان، رفرنسی است که به مکانی از حافظه اشاره می‌کند که ممکن است به داده‌ی دیگری تخصیص داده شده باشد یا قبلاً آزاد شده باشد.

کامپایلر Rust از یک «بررسی‌کننده وام‌گیری» یا borrow checker برای مقایسه حوزه‌ها (scopes) و تضمین اینکه تمام رفرنس‌ها همیشه معتبر خواهند بود، استفاده می‌کند. Lifetimeها در واقع بخشی از سینتکس زبان هستند که به ما اجازه می‌دهند تا به borrow checker کمک کنیم تا روابط بین طول عمر رفرنس‌های مختلف را درک کند، به خصوص زمانی که این روابط برای کامپایلر مبهم است.

مشکلی که Lifetimeها حل می‌کنند

بیایید به یک مثال نگاه کنیم. کد زیر سعی می‌کند یک رفرنس به متغیری برگرداند که در داخل همان محدوده تعریف شده است.

Copy Icon src/main.rs
fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    } // `x` goes out of scope here, its memory is freed.
    
    // `r` now refers to invalid memory. This is a dangling reference.
    // println!("r: {}", r); // Rust's compiler prevents this.
}

کامپایلر Rust این کد را رد می‌کند، زیرا متغیر x قبل از اینکه r از آن استفاده کند، از حوزه خارج شده است. borrow checker با مقایسه طول عمر r (که در حوزه بیرونی است) و x (که در حوزه درونی است) متوجه این مشکل می‌شود.

سینتکس Lifetime Annotation

در بسیاری از موارد، کامپایلر می‌تواند به صورت خودکار و بدون نیاز به کمک ما، طول عمرها را استنتاج کند. اما در برخی موارد، به خصوص در امضای توابع، این روابط مبهم می‌شوند و ما باید با استفاده از «حاشیه‌نویسی طول عمر» (lifetime annotation) به کامپایلر کمک کنیم.

سینتکس lifetimeها کمی غیرمعمول است: نام آنها همیشه با یک آپاستروف (') شروع شده و معمولاً یک نام کوتاه و با حروف کوچک است. حاشیه‌نویسی lifetime طول عمر یک رفرنس را تغییر نمی‌دهد؛ بلکه صرفاً روابط بین طول عمر رفرنس‌های مختلف را توصیف می‌کند تا کامپایلر بتواند آنها را تحلیل کند.

بیایید به تابع largest که در درس‌های قبل با آن کار کردیم، بازگردیم. نسخه زیر کامپایل نخواهد شد:

Copy Icon src/main.rs
// This function won't compile!
fn largest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

خطای کامپایلر به ما می‌گوید که "missing lifetime specifier". مشکل این است که کامپایلر نمی‌داند طول عمر رفرنس بازگشتی به طول عمر x وابسته است یا به طول عمر y. اگر ما یک رفرنس به x و یک رفرنس به y با طول عمرهای متفاوت به این تابع پاس دهیم، کامپایلر نمی‌داند که طول عمر رفرنس بازگشتی چقدر باید باشد تا از ایجاد یک رفرنس آویزان جلوگیری کند.

راه‌حل: افزودن پارامترهای جنریک Lifetime

برای رفع این مشکل، ما باید یک پارامتر lifetime جنریک به امضای تابع اضافه کنیم تا یک رابطه بین طول عمر رفرنس‌های ورودی و خروجی ایجاد کنیم.

Copy Icon src/main.rs
fn largest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

این سینتکس به کامپایلر می‌گوید: "برای یک lifetime مشخص به نام 'a این تابع دو رفرنس می‌گیرد که هر دو باید حداقل به اندازه 'a عمر کنند، و یک رفرنس برمی‌گرداند که آن هم حداقل به اندازه 'a عمر خواهد کرد." در عمل، این یعنی طول عمر رفرنس بازگشتی، برابر با کوچکترین طول عمر از بین رفرنس‌های ورودی خواهد بود. این کار تضمین می‌کند که رفرنس بازگشتی هرگز بیشتر از داده‌ای که به آن اشاره می‌کند، عمر نخواهد کرد.

Lifetimeها در تعریف struct و impl

زمانی که یک struct شامل یک رفرنس باشد، باید در تعریف struct نیز از حاشیه‌نویسی lifetime استفاده کنیم.

Copy Icon src/main.rs
struct ImportantExcerpt<'a> {
    part: &'a str,
}

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

این تعریف به کامپایلر می‌گوید که یک نمونه از ImportantExcerpt نمی‌تواند بیشتر از رفرنسی که در فیلد part خود نگهداری می‌کند، عمر کند.

Lifetime ایستا ('static)

یک lifetime خاص و از پیش تعریف شده، 'static است. این lifetime به این معنی است که رفرنس مورد نظر می‌تواند برای کل طول عمر برنامه معتبر باقی بماند. تمام لیترال‌های رشته‌ای دارای لایف‌تایم 'static هستند، زیرا آنها مستقیماً در باینری برنامه ذخیره می‌شوند و همیشه در دسترس هستند.

در این درس با مفهوم lifetime به عنوان یکی از ستون‌های اصلی ایمنی در Rust آشنا شدیم. دیدیم که چگونه حاشیه‌نویسی lifetime به borrow checker کمک می‌کند تا از ایجاد رفرنس‌های آویزان جلوگیری کند. ما اکنون سه ابزار قدرتمند را برای نوشتن کدهای انتزاعی، قابل استفاده مجدد و ایمن در اختیار داریم. در فصل بعدی، به سراغ «نوشتن تست‌های خودکار» خواهیم رفت و یاد می‌گیریم که چگونه با استفاده از امکانات تست داخلی Rust، از صحت و پایداری کدهای خود اطمینان حاصل کنیم.