وبلاگ شخصی
محمّدرضا
علی حسینی

جایی که تجربیات, علایق
و چیزهایی که یادگرفته‌ام را
با هم مرور می‌کنیم.

آموزش زبان برنامه‌نویسی Rust- قسمت ۲۰: طول عمر یا Lifetime

آموزش زبان برنامه‌نویسی Rust- قسمت ۲۰: طول عمر یا Lifetime

دیدیم که مهم‌ترین هدف زبان Rust حفظ ایمنی برنامه است. حالا در این جلسه می‌خواهیم به مفهومی بپردازیم که در کنار مالکیّت و borrowing، به عنوان سومین مفهوم حیاتی، می‌خواهد که ایمنی کد ما را تضمین کند.

منظور از طول عمر یا Lifetime چیست؟

هر رفرنس در یک بخش (scope) مشخّص تعریف شده است. یعنی وقتی ما یک رفرنس را درون یک بخش تعریف می‌کنیم، زندگی آن آغاز می‌شود و هنگامی که جریان اجرای برنامه از آن بخش خارج می‌شود، زندگی آن رفرنس نگون‌بخت هم پایان می‌یابد و از این جهان فانی رخت برمی‌بندد.

به بخشی (scope) از کد که رفرنس در آن تعریف شده است، طول عمر یا Lifetime می‌گویند.

دو نکته‌ی مهم در این تعریف وجود دارد:

۱− طول عمر صرفاً برای رفرنس‌ها تعریف می‌شود و باقی نوع‌داده‌ها اصلاً چنین مفهومی ندارند.

۲− طول عمر یک رفرنس ممکن است شامل چندین بخش (scope) شود. کافی است که درون بخشی که در آن تعریف شده است چندین بخش (scope) دیگر تعریف شده باشد.

برای روشن‌تر شدن موضوع به تکّه کد زیر نگاه کنید:

fn main()
{                           // -------------------- 'a |
    let a;                  //                         |
    {                       // ------ 'b |
        let b = 10;         //           |
        a = &b;             //           |
    }                       // ------ 'b |
    println!("a = {}", a);  // ------------------------|
}                           // -------------------- 'a |

بخشی که با a' شروع شده است جایی است که زندگی a شروع می‌شود. جلوتر دلیل وجود علامت ' که در کامنت‌ها برای نشان‌دادن شروع عمر یک رفرنس آمده است را می‌فهمیم. بخشی هم که با b' شروع شده است جایی است که زندگی b آغاز می‌شود. همان‌طوری که می‌بینید a قبل از اینکه b متولّد شود زنده است و پس از مردن آن هم به زندگی ادامه داده است. به همین خاطر عمر a شامل دو تا بخش (scope) است. یکی بخشی که خودش در آن تعریف شده است و یکی هم بخشی که b در آن تعریف شده است.

چرا Rust برخلاف اکثر زبان‌ها حواسش به طول عمر رفرنس‌ها هست؟

قبل از پاسخ‌دادن به این چرا، سعی کنید که برنامه‌ی قبلی را کامپایل کنید. با چه چیزی روبه‌رو می‌شوید؟

error[E0597]: `b` does not live long enough
 --> src/main.rs:6:9
  |
6 |         a = &b;             //           |
  |         ^^^^^^ borrowed value does not live long enough
7 |     }                       // ------ 'b |
  |     - `b` dropped here while still borrowed
8 |     println!("a = {}", a);  // ------------------------|
  |                        - borrow later used here

کامپایلر جلوی اجرای این برنامه را می‌گیرد. دلیلش را هم خیلی واضح نوشته است: b به اندازه‌ی کافی زنده نمانده است تا بتوانیم از مقدارش در دستور println استفاده کنیم. چرا؟ چون a تنها یک رفرنس به مقدار b است و اینجا دیگر b وجود ندارد که ما بتوانیم به مقدار آن اشاره کنیم.

حالا برویم سراغ سؤالی که در تیتر آمده است. چرا Rust حواسش به این موضوع است؟ بیایید کد مشابه همین برنامه را به زبان C بنویسیم:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *a;
    {
        int *b = malloc(sizeof(int));
        *b = 10;
        a = b;
        free(b);
    }
    printf("a = %d\n", *a);
    return 0;
}

این کد دقیقاً همان کاری را می‌کند که ما در قطعه کد Rust قبلی انجام دادیم.

توضیح خیلی کوتاه کد برای افرادی که با زبان C آشنایی ندارند:

ما ابتدا یک اشاره‌گر به نام a تعریف می‌کنیم. سپس درون بخش (scope) داخلی به اندازه‌ی یک عدد صحیح (int) حافظه‌ی جدید از Heap می‌گیریم و با اشاره‌گر b به آن اشاره می‌کنیم.

حالا عدد ۱۰ را درون حافظه‌ای که از Heap گرفتیم می‌ریزیم و بعد از آن هم می‌گوییم که اشاره‌گر a به همان‌جایی اشاره کند که اشاره‌گر b دارد به آن اشاره می‌کند. یعنی همان حافظه‌ی گرفته‌شده از Heap.

بعد از آن هم حافظه‌ای که از Heap گرفته‌بودیم را آزاد می‌کنیم.

حالا بیایید این برنامه را اجرا کنیم:

a = 0

همانطوری که می‌بینید مقداری که متغیّر a دارد به آن اشاره می‌کند یک مقدار اشتباه است. ما در کدمان می‌خواستیم کاری کنیم که a هم به مقداری که اشاره‌گر b به آن اشاره می‌کند، یعنی ۱۰، اشاره کند. امّا a دارد به مقدار ۰ درون حافظه اشاره می‌کند. چرا؟ چون عمر b کمتر از a بود و حالا که ما می‌خواهیم از آن مقدار استفاده کنیم عملاً دیگر وجود ندارد.

Rust برای جلوگیری از رخ‌دادن چنین مشکلاتی طول عمر رفرنس‌ها را بررسی می‌کند تا مطمئن شود که ما به جایی که دیگر وجود ندارد اشاره نمی‌کنیم و مشکل رفرنس‌های آویزان (Dangling reference) که قبلاً به آن اشاره کردیم به وجود نمی‌آید.

چطوری طول عمر پارامترهای تابع را مشخّص کنیم؟

حالا بیایید یک برنامه‌ی دیگر را امتحان کنیم. فرض‌کنید که ما می‌خواهیم تابعی بنویسیم که دو رفرنس به دو آرایه را می‌گیرد و رفرنس آرایه‌ای را برمی‌گرداند که مجموع ارقامش بیشتر از دیگری است.

بیایید اوّل این تابع را به صورت عادی بنویسیم:

fn greatest_sum(array1: &[i32], array2: &[i32]) -> &[i32] {
    let mut sum1 = 0;
    let mut sum2 = 0;

    for i in array1 {
        sum1 += *i;
    }

    for j in array2 {
        sum2 += *j;
    }

    if sum1 < sum2 {
        return array1;
    }
    return array2;

}

fn main() {
    let arr1 = [1, 2, 3];
    let arr2 = [1, 2, 3, 4];

    let greatest_array = greatest_sum(&arr1, &arr2);
    println!("greatest array: {:?}", greatest_array);

}

تابع دو تا رفرنس را می‌گیرد، هرکدام را پیمایش می‌کند و مجموع اعضایش را محاسبه می‌کند. سپس اگر مجموع اوّلی بزرگ‌تر بود، آن رفرنس را برمی‌گرداند. در غیر این‌صورت دومی را برمی‌گرداند.

حالا بیایید این برنامه را کامپایل کنیم:

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:52
  |
1 | fn greatest_sum(array1: &[i32], array2: &[i32]) -> &[i32] {
  |                                                    ^ expected lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `array1` or `array2`

همانطوری که می‌بینید کامپایلر می‌گوید که ما نشان‌گر طول عمر را فراموش‌کرده‌ایم. حالا این نشان‌گر طول عمر یا به قول این کامپایلر زبان‌بسته lifetime specifier را چطوری باید به کد اضافه کنیم؟

نشان‌گر طول عمر چیزی شبیه به همان پارامترهای Generic است. با این تفاوت که اینجا به جای نشان‌دادن نوع داده، طول عمر پارامتر مرتبط را نشان می‌دهند.

نام نشان‌گرهای طول عمر باید با علامت ' مشخّص شوند. برنامه‌نویس‌های Rust عموماً برای اسم نشان‌گرهای طول عمر از حروف انگلیسی کوچک استفاده می‌کنند. یعنی اکثر نشان‌گرهای طول عمری که تا پایان زندگی‌تان خواهید دید اسم‌شان: a' خواهد بود.

یک رفرنس با نشان‌گر طول عمر a' به شکل زیر نوشته می‌شود:

&'a u16

حالا یک رفرنس قابل تغییر با نشان‌گر طول عمر a' را هم می‌توانیم به شکل زیر بنویسیم:

&'a mut u16

با این اوصاف می‌توانیم تابع greatest_sum را در برنامه‌ی قبلی به شکل زیر بازنویسی کنیم:

fn greatest_sum <'a>(array1: &'a [i32], array2: &'a [i32]) -> &'a [i32] {
    let mut sum1 = 0;
    let mut sum2 = 0;

    for i in array1 {
        sum1 += *i;
    }

    for j in array2 {
        sum2 += *j;
    }

    if sum1 < sum2 {
        return array1;
    }
    return array2;
}

خب حالا با این تغییر بیایید برنامه‌ی قبلی را اجرا کنیم:

greatest array: [1, 2, 3]

کار کرد! حالا چرا؟ بیایید با بررسی روند کاری کامپایلر بفهمیم که چرا این تغییر نیاز بود.

۳ قانون بررسی طول عمر توسّط کامپایلر

کامپایلر برای اینکه به صورت خودکار متوجّه طول عمر پارامترها بشود و بتواند از عدم وجود مشکل مطمئن شود از ۳ قانون پیروی می‌کند.

به این ۳ قانون اصطلاحاً elision rules می‌گویند. کامپایلر به وسیله‌ی این ۳ قانون تلاش می‌کند تا طول عمر (lifetime) مرتبط با تمام پارامترهای ورودی و خروجی تابع را مشخّص کند (یادتان نرود که طول عمر فقط برای رفرنس‌ها معنی دارد)، اگر در پایان اعمال این ۳ قانون تمام پارامترها طول عمر مشخّصی داشتند کار تمام است. در غیر این صورت کامپایلر به ما خطا می‌دهد و ما باید خودمان مانند مثال قبلی طول عمر پارامترها را مشخّص کنیم.

وجود این ۳ قانون باعث می‌شود که ما خیلی وقت‌ها مجبور نباشیم که کد بیشتری بنویسیم و طول عمر پارامترها را مشخّص کنیم.

حالا برویم سراغ این ۳ قانون. فقط حواس‌تان باشد که این ۳ قانون فقط برای توابع و بلوک‌های impl اعمال می‌شوند.

قانون اوّل: تعیین طول عمر پارامترهای ورودی

اوّلین قانون مربوط به پارامترهای ورودی است. فرض‌کنید که ما کد زیر را داریم:

fn example_function (param1: &u8, param2: &u8)

قانون اوّل می‌گوید که باید برای هرکدام از پارامترهای ورودی که از جنس رفرنس هستند، یک پارامتر تعیین طول عمر اختصاص بدهیم. یعنی کد بالا از نظر کامپایلر می‌شود چیزی شبیه به کد زیر:

fn example_function<'a, 'b> (param1: &'a u8, param2: &'b u8)

قانون دوم: تعیین طول عمر پارامترهای خروجی

قانون دوم می‌گوید که اگر پس از اعمال قانون اوّل تنها یک پارامتر تعیین طول عمر داشتیم، تمام طول عمرهای خروجی را برابر با همان پارامتر درنظر می‌گیریم.

مثلاً فرض‌کنید که ما کد زیر را داشته‌ایم:

fn example_function(param1: u8, param2: &u16) -> &u16

کامپایلر ابتدا قانون اوّل را روی این کد اعمال می‌کند و آن را تبدیل به کد زیر می‌کند (باز هم یادتان نرود که طول عمر فقط به رفرنس‌ها مربوط می‌شود):

fn example_function<'a>(param1: u8, param2: &'a u16) -> &u16

حالا این کد را با قانون دوم بررسی می‌کند. از آن‌جایی که تنها یک پارامتر تعیین طول عمر داریم، پس می‌توان قانون دوم را روی این کد اعمال کرد:

fn example_function<'a>(param1: u8, param2: &'a u16) -> &'a u16

حالا تمام رفرنس‌های ورودی و خروجی طول عمرشان مشخّص است. پس کامپایلر بدون هیچ مشکلی می‌تواند درستی استفاده از این تابع را در سرتاسر کد بررسی کند.

قانون سوم: تعیین طول عمر متدها

قانون سوم تنها برای متدها معنی دارد. اگر پس از اعمال قانون اوّل دیدیم که یکی از پارامترهای تعیین طول عمر مربوط به پارامتری است که از نوع self& یا mut self& است، آن پارامتر را به عنوان طول عمر خروجی هم درنظر می‌گیریم.

باز فرض‌کنید که ما کد زیر را داشته‌ایم:

fn example_method(&self, param2: &u8) -> &u8

حالا قانون اوّل را روی این کد اعمال می‌کنیم:

fn example_method<'a, 'b>(&'a self, param2: &'b u8) -> &u8

حالا می‌رویم سراغ قانون سوم:

fn example_method<'a, 'b>(&'a self, param2: &'b u8) -> &'a u8

دوباره تمام ورودی‌ها و خروجی‌ها طول عمر مشخّصی دارند، پس کار ما دیگر تمام شده است.

اگر این قوانین را یکبار دیگر با دقّت بررسی کنید متوجّه می‌شوید که هیچ تناقضی بین قوانین دوم و سوم وجود ندارد. یعنی در این مثال اگر قانون دوم را هم اعمال می‌کردیم و بعد قانون سوم را باز به همین نتیجه می‌رسیدیم.

حالا که این ۳ قانون را فهمیدیم، بیایید بررسی کنیم که چرا در تابع greatest_sum ما باید خودمان طول عمر پارامترها را مشخّص کنیم و کامپایلر نمی‌تواند درستی استفاده از تابع را متوجّه شود.

تابع ما بدون پارامترهای تعیین طول عمر این بود:

fn greatest_sum(array1: &[i32], array2: &[i32]) -> &[i32]

حالا بیایید قانون اوّل را روی آن اعمال کنیم. برای این کار باید برای هر رفرنس ورودی یک پارامتر تعیین طول عمر مجزا تعیین کنیم:

fn greates_sum<'a, 'b>(array1: &'a [i32], array2: &'b [i32]) -> &[i32]

ما نمی‌توانیم قانون دوم را روی این تابع اجرا کنیم. چون دو پارامتر تعیین طول عمر داریم. اجرای قانون سوم هم منتفی است. چون این یک تابع است نه یک متد. پس کار کامپایلر برای حدس طول عمر رفرنس‌ها همین‌جا به پایان می‌رسد.

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

بنابراین ما باید خودمان به صورت صریح به کامپایلر بگوییم که طول عمر هرکدام از ورودی‌ها و خروجی‌ها چیست.

چطوری طول عمر اعضای یک ساختار را مشخّص کنیم؟

ما تا اینجا فقط با ساختارهایی کار کردیم که خودشان صاحب مقادیرشان هستند. امّا چطوری می‌توان در یک ساختار (struct) یک رفرنس را نگهداری کرد؟

#[derive(Debug)]
struct  MyStruct {
    name: &str,
    age: u8
}


fn main() {
    let name = "Asghar";
    let person = MyStruct {
        name: name,
        age: 10
    };
    println!("{:?}", person);
}

اگر این برنامه را اجرا کنیم کامپایلر به ما خطای زیر را نمایش می‌دهد:

error[E0106]: missing lifetime specifier
  --> src/main.rs:20:11
   |
20 |     name: &str,
   |           ^ expected lifetime parameter

error: aborting due to previous error

کامپایلر نمی‌تواند رابطه‌ای بین طول عمر یک نمونه از این ساختار را با رشته‌ای که ما داریم به آن رفرنس می‌دهیم پیدا کند. به همین خاطر ما همیشه موقعی که می‌خواهیم درون ساختارمان از یک رفرنس استفاده کنیم، باید پارامتر طول عمر را هم مشخّص کنیم.

چرا؟ چون ممکن است که رفرنسی که به مقدار name داده شده است قبل از خود نمونه‌ای که از ساختار MyStruct ساخته‌ایم بمیرد و این یعنی اینکه ما از آن زمان به بعد درون این ساختار یک رفرنس به جایی داریم که دیگر وجود ندارد.

باید ساختار بالا را به شکل زیر تغییر بدهیم:

#[derive(Debug)]
struct MyStruct<'a> {
    name: &'a str,
    age: u8
}

حالا اگر برنامه را اجرا کنیم همه‌چیز به خوبی اجرا می‌شود:

MyStruct { name: "Asghar", age: 10 }

چطوری طول عمر اعضای یک enum را مشخّص کنیم؟

هنگام استفاده از رفرنس‌ها در enum ها هم دقیقاً باید مثل ساختارها عمل کنیم.

#[derive(Debug)]
enum Something <'a> {
    Some(&'a u8),
    Thing(u8)
}

fn main() {
    let number: u8 = 10;
    let a = Something::Some(&number);
    println!("{:?}", a);
}

استفاده از پارامتر تعیین طول عمر در impl

حالا بیایید همان ساختاری MyStruct را در دو بخش قبلی در نظر بگیریم. ما می‌خواهیم برای این ساختار متدی تعریف کنیم که با فراخوانی آن، اسم نمونه‌ای که از این ساختار (struct) ساخته شده است تغییرکند و نام قبلی بازگردانده‌شود.

#[derive(Debug)]
struct MyStruct<'a> {
    name: &'a str,
    age: u8
}

impl MyStruct {
    fn change_name(&mut self, new_name: &str) -> &str {
        let old_name = self.name;
        self.name = new_name;
        return old_name;
    }
}

fn main() {
    let name = "Asghar";
    let mut person = MyStruct {
        name,
        age: 10
    };
    println!("{:?}", person);

    person.change_name("Akbar");

    println!("{:?}", person);
}

حالا بیایید این برنامه را اجرا کنیم:

error[E0726]: implicit elided lifetime not allowed here
  --> src/main.rs:34:6
   |
34 | impl MyStruct {
   |      ^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

error[E0621]: explicit lifetime required in the type of `new_name`
  --> src/main.rs:37:21
   |
35 |     fn change_name(&mut self, new_name: &str) -> &str {
   |                                         ---- help: add explicit lifetime `'static` to the type of `new_name`: `&'static str`
36 |         let old_name = self.name;
37 |         self.name = new_name;
   |                     ^^^^^^^^ lifetime `'static` required

همان‌طوری که می‌بینید ما باید طول عمر را برای این بلوک impl مشخّص کنیم. چون در تعریف این ساختار ما به صورت صریح مشخّص کرده‌ایم که باید پارامتر تعیین طول عمر مشخّص شود.

برای این کار ما باید برای خود بلوک impl پارامتر طول عمر را مشخّص کنیم و آن را به ساختار و رفرنس جدید موجود در متد ربط بدهیم:

impl<'a> MyStruct<'a> {
    fn change_name(&mut self, new_name: &'a str) -> &str {
        let old_name = self.name;
        self.name = new_name;
        return old_name;
    }
}

حالا اگر برنامه را اجرا کنیم نتیجه‌ای که می‌خواستیم را خواهیم گرفت:

MyStruct { name: "Asghar", age: 10 }
MyStruct { name: "Akbar", age: 10 }

حواستان باشد که پارامتر a' که بعد از impl آمده است، پارامتر تعیین طول عمری است که متعلّق به تمام بلوک impl است. یعنی مثلاً ما می‌توانیم چند پارامتر تعیین طول عمر اینجا داشته باشیم امّا تنها یکی را به MyStruct اختصاص بدهیم.

پارامتر a' بعد از MyStruct هم پارامتری است که مشخّص کننده‌ی طول عمر مقادیر این ساختار اند. حالا ما در تعریف متد change_name مشخّص‌کرده‌ایم که رفرنس new_name هم عمری حدّاقل به اندازه‌ی نمونه‌ای دارد که ما از ساختار MyStruct ساخته‌ایم.

استفاده‌ی هم‌زمان از پارامتر طول عمر و Generic ها

ما می‌توانیم که به صورت هم‌زمان هم از پارامترهای Generic استفاده کنیم و هم از پارامترهای طول عمر. برای این کار کافی است که پارامترهای Generic را هم بین علامت‌های > و < قرار بدهیم.

مثلاً‌ فرض‌کنید که ما یک تابع داریم که هم پارامتر تعیین طول عمر a' را دارد و هم یک پارامتر Generic به نام T:

fn test_func <'a, T> (x:&'a T)

طول عمر static

ما در زبان Rust می‌توانیم مقادیری داشته باشیم که طول عمرشان static است. یعنی تا زمانی که برنامه وجود دارد این مقادیر هم زنده‌اند.

ما می‌توانیم یک مقدار static را به شکل زیر تعریف کنیم:

static STATIC_VALUE: u8 = 10;

تعریف مقادیر static با کلمه‌ی کلیدی static آغاز می‌شود. بعد از آن باید نام مقدار بیاید. بهتر است که نام مقادیر static را با حروف بزرگ بنویسیم و بخش‌های مختلف نام را با علامت _ از هم جدا کنیم.

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

در حالت کلّی مقادیر static مثل ثوابت عمل می‌کنند. یعنی شما نمی‌توانید مقدار آن را عوض‌کنید و البته امکان این را دارید که آن‌ها را به صورت Global، یعنی خارج از scope تمام توابع و … تعریف کنید.

ما می‌توانیم مقادیر static را به شکلی تعریف کنیم که مقدار آن‌ها قابل تغییر باشد:

static mut STATIC_VALUE: u8 = 10;

امّا باید حواستان باشد که در این حالت ما تنها درون بلوک‌های unsafe می‌توانیم مقدار static را تغییر بدهیم. دلیلش هم این است که تغییر دادن مقادیر static ممکن است در برنامه‌های هم‌روند (concurrent) مشکل ایجاد کند.

حالا بعداً در مورد اینکه بلوک unsafe چه چیزی است صحبت می‌کنیم.

این مقادیر static مثل همان مقادیر static در زبان‌هایی مثل C عمل می‌کنند. ما مثلاً می‌توانیم در زبان C تابعی مثل تابع زیر بنویسیم که می‌تواند تعداد دفعاتی که فراخوانی شده است را نگهداری کند:

#include <stdio.h>

void call_me() {
    static int number_of_calls = 0;		// مقدار صفر برای مقداردهی اوّلیه به کار می‌رود و در باقی فراخوانی‌ها اعمال نمی‌شود
    number_of_calls++;
    printf("number of calls: %d\n", number_of_calls);
}

int main () {
     call_me();
     call_me();
     call_me();
     call_me();
}

حالا اگر برنامه را اجرا کنیم خروجی زیر را خواهیم گرفت:

number of calls: 1
number of calls: 2
number of calls: 3
number of calls: 4

حالا با استفاده از مقادیر static ما می‌توانیم دقیقاً همین کار را در Rust هم انجام بدهیم:

fn call_me() {
    static mut NUMBER_OF_CALLS: u8 = 0;
    unsafe {
        NUMBER_OF_CALLS += 1;
        println!("number of calls: {}", NUMBER_OF_CALLS);
    }
}

fn main() {
    call_me();
    call_me();
    call_me();
    call_me();
}

حالا با اجرای این برنامه هم دقیقاً همان خروجی را می‌گیریم:

number of calls: 1
number of calls: 2
number of calls: 3
number of calls: 4

طول عمر static برای خودش یک پارامتر مجزا با اسم خاص دارد. یعنی اگر جایی بخواهید مشخّص کنید که طول عمر یک رفرنس static است، باید به جای استفاده از پارامترهایی مثل: a' و b' و … از پارامتر: static' استفاده کنید.

چه زمان‌هایی به بیشتر از یک پارامتر تعیین طول عمر نیاز داریم؟

تا اینجا هر مثالی که دیدیم تنها نیاز به یک پارامتر تعیین طول عمر داشت. امّا چه زمان‌هایی به بیش از یک پارامتر تعیین طول عمر نیاز داریم؟

می‌توان برای استفاده از بیش از یک پارامتر مثال‌های متفاوت زیادی آورد، امّا احتمالاً در اکثر موارد با حالتی مثل کد زیر روبه‌رو خواهید شد:

struct FirstStruct<'a> {
    val1: &'a str
}

struct SecondStruct<'a, 'b> {
    my_struct: &'a FirstStruct<'b>
}

ما یک ساختار به نام FirstStruct داریم که از یک رفرنس به یک رشته نگهداری می‌کند. پس موقع تعریف آن باید پارامتر طول عمرش را هم مشخّص کنیم.

حالا یک ساختار دیگر به نام SecondStruct داریم که از یک رفرنس به یک نمونه از FirstStruct نگهداری می‌کند.

ما اینجا به دو پارامتر تعیین طول عمر نیاز داریم. یک پارامتر که مشخّص‌کننده‌ی طول عمر رفرنسی باشد که در همین ساختار به نمونه‌ای از FirstStruct نگهداری می‌کنیم و یک پارامتر برای تعیین طول عمر رشته‌ای که FirstStruct دارد از آن استفاده می‌کند.

فراموش نکنید که پارامتر a' در تعریف ساختار FirstStruct با پارامتر a' در تعریف SecondStruct فرق می‌کند.

خب. این هم از آشنایی ما با مفهوم طول عمر (lifetime) در زبان Rust. طول عمر از مفاهیم خیلی مهم و البته کمی پیچیده‌ی زبان Rust است که توضیح آن به همین اندازه کافی است. چون باید با جنبه‌های دیگر آن در کاربردهای مختلف به صورت عملی آشنا بشویم.

در قسمت بعدی می‌بینمتان.

می‌توانید کدهای این بخش را در مخزن این مجموعه‌ی آموزشی رایگان در گیت‌هاب ببینید.

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

اوّلی بار است که این دوره‌ی آموزشی را می‌بینی؟ با کلیک روی این نوشته به قسمت اوّل برو و یادگیری دقیق و کامل زبان برنامه‌نویسی Rust را شروع کن.

«نوشته‌های مرتبط»

10 پاسخ به “آموزش زبان برنامه‌نویسی Rust- قسمت ۲۰: طول عمر یا Lifetime”

  1. یارو گفت:

    سلام
    خیلی مفید بود تشکر
    اگه وقت کردین در مورد None هم یه توضیح بدین ممنون میشم

    • محمّدرضا علی حسینی گفت:

      سلام.
      خواهش می‌کنم.
      به زودی در یک نوشته هم error handling و هم نحوه‌ی برخورد Rust با مفهوم None را هم با هم یاد می‌گیریم.

  2. amir گفت:

    fn print_refs(x: &’a i32, y: &’b i32) {
    println!(“x is {} and y is {}”, x, y);
    }

    سلام
    یکم گیج شدم
    در اینجا لایف تایم x با y فرق میکنه ؟

    • محمّدرضا علی حسینی گفت:

      سلام.
      این کد یعنی اینکه پارامترهای x و y می‌توانند طول عمرهای مختلفی داشته باشند. امّا تا پایان اجرای این تابع باید هر دو زنده باشند.
      یعنی یا مقدار طول عمر هر دو دقیقاً یکی است، یا اینکه کوتاه‌ترین طول عمر هم برای کاری که این تابع دارد می‌کند کافی است.
      امیدوارم توانسته‌باشم که راهنمایی‌ات بکنم. ولی اگر سؤال دیگری هم داشتی همینجا بپرس. 🙂

  3. hosein گفت:

    سلام
    در مورد اسکوپ هایی مانند crate , mod بحثی داشتین ؟

    • محمّدرضا علی حسینی گفت:

      سلام.
      نه. هنوز به این مباحث نرسیده‌ایم. امّا در قسمت‌های آینده سراغ آن‌ها هم خواهیم رفت.

  4. احسان گفت:

    آقا دمت گرم عالی بود
    منتظر قست های بعدی هستم

    یه سوال داشتم اینکه فرق mod با crate چیه و کجا از use و کجا از extern crate استفاده میشه

    • محمّدرضا علی حسینی گفت:

      سلام.
      ممنونم احسان عزیز.
      crate معادل کتابخانه یا پکیج در زبان‌های دیگر است. Cargo می‌تواند crate ها را مدیریت کند. امّا mod نشان‌گر یک ماژول است که ما با استفاده از آن می‌توانیم کدمان را ساختاربندی کنیم.
      extern crate به کامپایلر می‌گوید که می‌خواهیم از فلان crate در کدمان استفاده کنیم و باید آن را لینک کند. امّا use یک اسم در scope فعلی را به یک «مسیر» مربوط می‌کند.
      مثلاً ما با دادن مسیر ساختار x در ماژول y به عبارت use، داریم x را هم به scope فعلی معرّفی می‌کنیم.
      حالا بعداً سر فرصت در قسمت‌های آینده به این مباحث خواهیم پرداخت.

  5. احسان گفت:

    وقتی یک پکیچ رو extern crate میکنیم حتما نیازه که use هم بشه ؟؟

    • محمّدرضا علی حسینی گفت:

      ما وقتی که extern را می‌نویسیم به کامپایلر می‌گوییم که می‌خواهیم کدمان را با فلان کتابخانه لینک کنیم. البته در نسخه‌های اخیر خیلی وقت‌ها لازم نیست که این کار را بکنیم چون خود Cargo زحمت این کار را می‌کشد. 🙂
      ما از use برای این استفاده می‌کنیم تا دیگر لازم نباشد که مسیر کامل چیزهایی را که در کد ما قرار ندارند را بنویسیم. یعنی ما برای اینکه مجبور نباشیم هربار بنویسم:

      std::collections::Vec.new(...)

      می‌توانیم با استفاده از use بگوییم که می‌خواهیم کد زیر وارد Scope ما بشود:

      use std::collections::Vec

      و بعد از آن به جای اینکه آن رشته‌ی طولانی قبلی را همه‌جا بنویسیم، تنها به نوشتن Vec اکتفا کنیم.

پاسخی بگذارید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

«نوشته‌های ویژه»

«نوشته‌های محبوب»

«دیدگاه کاربران»