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

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

آموزش زبان برنامه‌نویسی Rust – قسمت۸: Borrowing

آموزش زبان برنامه‌نویسی Rust – قسمت۸: Borrowing

با مالکیّت، مهم‌ترین و خاص‌ترین ویژگی زبان Rust، در جلسه‌ی پیش آشنا شدیم. حالا می‌خواهیم ببینیم که چطور می‌توانیم مقادیر را به توابع ارسال کنیم، بدون اینکه مالکیّت آن‌هارا منتقل کنیم.
در این جلسه می‌خواهیم درمورد referencing صحبت کنیم. یکی از پرکاربردترین بخش‌ها در هر برنامه‌ی کامپیوتری که مشکلات کار با آن بیشترین زحمت و زمان‌را از برنامه‌نویس‌ها می‌گیرید.
مشکلاتی که می‌خواهیم با کمک Rust برای همیشه حلشان کنیم.

مشکل چیست؟

آخرین چیزی که در جلسه‌ی پیش دیدیم کد زیر بود:

fn main() {
    let mut a = String::from("hello");
    a = i_am_owner(a);
    println!("a in main function: {}", a);
}
fn i_am_owner(input: String) -> String {
    println!("The input value is: {}", input);
    return input;
}

دیدیم که با دادن متغیّر a به تابع i_am_owner به عنوان پارامتر ورودی،‌ مالکیّت (Ownership) این متغیّر به آرگومان ورودی تابع منتقل می‌شود و ما دیگر نمی‌توانیم از آن در scopeی که تابع فراخوانی شده است استفاده کنیم.
برای رفع این مشکل، همانطوری که در تکّه کد بالا می‌بینید، در پایان تابع i_am_owner دوباره همان String را خروجی داده‌ایم و درون تابع main، متغیّر a را برابر خروجی این تابع قرار داده‌ایم تا مالکیّت داده را دوباره به آن بازگرداینم.
حالا می‌خواهیم ببینیم که چطوری می‌توانیم بدون انتقال مالکیّت یک مقدار، امکان استفاده از آن‌را به یک تابع بدهیم.

به مالکیّت من دست نزن!

بیایید کد بالا را کمی تغییر بدهیم. کد زیر را با دقّت نگاه کنید:

fn main() {
    let a = String::from("hello");
    i_am_owner(&a);
    println!("a in main function: {}", a);
}
fn i_am_owner(input: &String) {
    println!("The input value is: {}", input);
}

در اینجا ۴ تا تغییر رخ‌داده است:
۱-متغیّر a لازم نیست دیگر تغییر کند. به همین خاطر با پاک‌کردن mut در تعریف آن دوباره این متغیّر را immutable کرده‌ایم. (اگر مفاهیم mutable و immutable را فراموش کرده‌اید، با کلیک روی این نوشته به قسمت مربوط به آن بروید و خیلی سریع این مفاهیم را به‌خاطر بیاورید.)
۲)نوع ورودی تابع i_am_owner را تغییر داده‌ایم. حالا به جای String، پارامتر input از نوع String& است.
۳)هنگام فراخوانی i_am_owner در خط دوم تابع main، دیگر a را برابر خروجی این تابع قرار نداده‌ایم. چون در این کد دیگر مالکیّت مقدار آن به تابع منتقل نشده است که لازم باشد آن‌را پس‌بگیریم.
۴)به جای اینکه a را به عنوان ورودی به تابع بفرستیم، مقدار &a را واردش می‌کنیم. اینطوری دیگر مالکیّت این متغیّر به پارامتر ورودی تابع منتقل نمی‌شود. امّا چرا؟ بیایید دقیق‌تر نگاه‌کنیم.

Reference

وقتی که علامت & پشت یک نوع یا متغیّر قرارمی‌گیرد، یعنی داریم از یک reference صحبت می‌کنیم.
در حقیقت reference به یک مقدار اشاره می‌کند.
یک کارت ویزیت را تصوّر کنید. روی آن کارت، آدرس محل کار کسی که کارت‌را به شما داده است نوشته شده است. این کارت یک رفرنس به آن محل کار است.
ما ورودی تابع i_am_owner را از String به &String تغییر داده‌ایم. پس از حالا به بعد این تابع به جای اینکه کل یک String را به عنوان ورودی بگیرد، یک رفرنس به آن‌را قبول می‌کند. بنابراین مالکیّت String ورودی دیگر به آن منتقل نمی‌شود و رفرنس گرفته شده هم با تمام شدن scope این تابع، بدون هیچ مشکلی پاک می‌شود.
اینطوری مقدار اصلی دست‌نخورده باقی می‌ماند و همچنان متعلّق به متغیّر اصلی است (اگر مفهوم scope در Rust را فراموش کرده‌ای، با کلیک روی این نوشته یک نگاه سریع به آن بیندازید و بعد برگردید).
هروقت که پشت یک مقدار &را بگذاریم، یک رفرنس به آن می‌گیریم. حالا می‌توانیم بدون هیچ مشکلی آن رفرنس را به عنوان ورودی به هر تابعی بدهیم.
پارامتر ورودی تابع، یعنی input، یک رفرنس به متغیّر a است.
اتّفاقی که با دادن رفرنس a به تابع i_am_owner به عنوان ورودی می‌افتد شبیه شکل زیر است (تصویر از این آدرس برداشته شده است):

رفرنس دادن در زبان Rust

پارامتر ورودی تابع، یعنی input، یک رفرنس به متغیّر a است. متغیّر a هم، همانطوری که در قسمت قبل دیدیم، خودش یک اشاره‌گر به بخشی از حافظه در heap است.
بنابراین می‌بینید که drop شدن input پس از پایان‌یافتن scope تابع، آسیبی به متغیّر a و مقدارش نمی‌زند.

Dereferencing

متضاد رفرنس دادن، dereferencing نامیده می‌شود. یعنی به مقداری که reference دارد به آن اشاره می‌کند دسترسی پیدا می‌کنیم.
برای دسترسی به مقدار یک رفرنس، باید قبل از آن علامت * را قرار بدهیم. البته همانطوری که دیدید ما اینجا این کار را نکردیم. چون خود rust به خاطر ویژگی Smart pointers می‌تواند تفاوت استفاده از یک رفرنس یا مقدارش‌را تشخیص بدهد.
بعداً مفصلاً به این ویژگی خواهیم پرداخت، فعلاً کافی است بدانید که می‌توانستیم تابع‌را به شکل زیر هم بنویسیم و فعلاً این دوتا با هم تفاوتی ندارند:

fn main() {
    let a = String::from("hello");
    i_am_owner(&a);
    println!("a in main function: {}", a);
}
fn i_am_owner(input: &String) {
    println!("The input value is: {}", *input);
}

Borrowing

خب حالا دیدید که چگونه از رفرنس‌ها برای عدم انتقال مالکیّت استفاده کردیم؟ به این کار در Rust اصطلاحاً borrowing یا همان قرض‌گرفتن می‌گویند.
همانطوری که ما در دنیای واقعی وقتی چیزی می‌خواهیم آن‌را «قرض»می‌گیریم، در اینجا هم وقتی بخش دیگری از برنامه به یک مقدار نیاز دارد، آن مقدار را به او قرض می‌دهیم.
وقتی که ماشینتان‌را به کسی قرض می‌دهید، همچنان شما مالک آن ماشین هستید. اینجا هم مالک آن مقدار همچنان متغیّر اصلی است و دیگران صرفاً به صورت قرضی دارند از آن مقدار استفاده می‌کنند.
خب حالا بیایید ببینیم اگر بخواهیم مقداری که قرض‌گرفته‌ایم را تغییر بدهیم چه اتّفاقی می‌افتد؟

fn main() {
    let a = String::from("hello");
    i_am_owner(&a);
    println!("a in main function: {}", a);
}
fn modifier(reference: &String) {
    reference.push_str(" a new string to push to the old one");
}

اینجا درون تابع modifier می‌خواهیم یک string دیگر را به String اوّلیّه که به عنوان ورودی گرفته‌ایم اضافه کنیم.
وقتی که می‌خواهیم برنامه‌را کامپایل کنیم با ارور زیر مواجه می‌شویم:

error[E0596]: cannot borrow immutable borrowed content `*reference` as mutable
 --> src/main.rs:8:5
  |
7 | fn modifier(reference: &String) {
  |                        ------- use `&mut String` here to make mutable
8 |     reference.push_str(" a new string to push to the old one");
  |     ^^^^^^^^^ cannot borrow as mutable

همانطوری که در متن ارور نوشته شده است، ما نمی‌توانیم از یک رفرنس immutable به عنوان یک رفرنس mutable استفاده کنیم.
یعنی نمی‌توان مقداری که به عنوان مقدار immutable قرض‌گرفته شده است را به عنوان یک مقدار mutable استفاده کرد و آن‌را تغییر داد.
اگر دوباره به مثال ماشین برگردیم، یعنی شما نمی‌توانید تودوزی ماشینی که صرفاً برای یک مسافرت یک روزه قرض‌گرفته‌اید را تغییر دهید.
خب حالا اگر بخواهیم مقدار منتسب به یک رفرنس‌را تغییر بدهیم باید چه کار کنیم؟

ساخت رفرنس mutable

یادتان هست برای اینکه بتوانیم یک متغیّر را تغییر بدهیم چه کار می‌کردیم؟ با افزودن کلمه‌ی کلیدی mut به تعریف آن متغیّر، آن‌را تبدیل به یک مقدار mutable می‌کردیم.
حالا برای اینکه بتوانیم مقداری که یک رفرنس به آن اشاره می‌کند را تغییر بدهیم، احتمالاً باید کاری مشابه انجام بدهیم.
بیایید اوّل ببینیم اگر صرفاً خود متغیّر اوّلیّه را mutable کنیم، آیا امکان تغییردادن داده‌ی آن با استفاده از رفرنسی که از آن داریم وجود دارد یا نه؟
کد زیر را ببینید:

fn main() {
    let mut a = String::from("hello");
    modifier(&a);
    println!("a in main function: {}", a);
}
fn modifier(reference: &String) {
    reference.push_str(" a new string to push to the old one");
}

در این کد صرفاً کلمه‌ی کلیدی mut را به تعریف متغیّر a اضافه‌کرده‌ایم تا این متغیّر mutable شود و بتوانیم مقدار آن‌را تغییر بدهیم.
اگر این کد را کامپایل کنیم، کامپایلر Rust به ما warning و ارور زیر را برمی‌گرداند:

warning: variable does not need to be mutable
 --> src/main.rs:2:9
  |
2 |     let mut a = String::from("hello");
  |         ----^
  |         |
  |         help: remove this `mut`
  |
  = note: #[warn(unused_mut)] on by default

error[E0596]: cannot borrow immutable borrowed content `*reference` as mutable
 --> src/main.rs:9:5
  |
7 | fn modifier(reference: &String) {
  |                        ------- use `&mut String` here to make mutable
8 |
9 |     reference.push_str(" a new string to push to the old one");
  |     ^^^^^^^^^ cannot borrow as mutable

اوّل از همه برویم سراغ warning. این اخطار می‌گوید که لزومی ندارد که متغیّر a را mutable کنیم. به علاوه به عنوان راهنمایی از ما می‌خواهد که کلمه‌ی mut را از تعریف آن حذف کنیم.
چرا چنین چیزی را از ما می‌خواهد؟ چون هیچ‌کجا مقدار این متغیّر تغییر نکرده است. پس انگار تغییری که ما می‌خواستیم روی رفرنس آن بدهیم مورد قبول Rust نیست.
در اروری که بعد از آن اخطار به ما داده شده است، همان چیزی تکرار شده که در مرحله‌ی قبل دیدیم. یعنی هنوز هم رفرنس ما immutable است، هرچند که خود متغیّر را mutable کردیم.
حالا بیایید یک راه دیگر را امتحان کنیم. این بار به جای اینکه متغیّر را mutable کنیم، رفرنس آن‌را mutable می‌کنیم:

fn main() {
    let a = String::from("hello");
    modifier(&mut a);
    println!("a in main function: {}", a);
}
fn modifier(reference: &mut String) {
    reference.push_str(" a new string to push to the old one");
}

برای اینکه یک رفرنس mutable داشته باشیم، کافی است بعد از علامت & کلمه‌ی mut را اضافه کنیم و بعد از آن type یا نام متغیّر را قرار دهیم.
حالا اگر بخواهیم این برنامه‌را کامپایل کنیم چه اتّفاقی می‌افتد؟

error[E0596]: cannot borrow immutable local variable `a` as mutable
 --> src/main.rs:3:19
  |
2 |     let a = String::from("hello");
  |         - consider changing this to `mut a`
3 |     modifier(&mut a);
  |                   ^ cannot borrow mutably

باز هم به ارور خوردیم.
همانطوری که در خط اول ارور گفته شده است، ما نمی‌توانیم یک متغیّر immutable را به عنوان یک متغیّر mutable قرض بدهیم. به همین خاطر کامپایلر از ساخته‌شدن چنین رفرنسی جلوگیری می‌کند.
امّا وسط پیام ارور، کامپایلر برای ما یک راهنمایی قرار داده است. کامپایلر از ما خواسته است که متغیّر a را هم به عنوان یک متغیّر mutable تعریف کنیم. از آنجایی که هیچ پیام خطایی درمورد اضافه بودن رفرنس mutable وجود ندارد، پس احتمالاً نیمی از راه را دست آمده ایم.
بیایید این بار بدون اینکه رفرنس‌را تغییر بدهیم، صرفاً متغیّر a را mutable کنیم:

fn main() {
    let mut     a = String::from("hello");
    modifier(&mut a);
    println!("a in main function: {}", a);
}
fn modifier(reference: &mut String) {
    reference.push_str(" a new string to push to the old one");
}

حالا بیایید این کد که در حقیقت ترکیبی از دو تلاش قبلیمان است را کامپایل و اجرا کنیم:

a in main function: hello a new string to push to the old one

برنامه به خوبی کامپایل شد و بدون مشکل خروجی ای که می‌خواستیم را تولید کرد.
بنابراین برای اینکه بتوانیم از طریق رفرنس‌دهی یک مقدار را تغییر دهیم، باید از رفرنس‌های mutable استفاده کنیم. خلاصه‌ی همه‌ی کارهایی که در این بخش برای ساخت یک رفرنس mutable کردیم می‌شود:
۱)متغیّر اصلی باید mutable باشد.
۲)خود رفرنس هم باید mutable باشد. برای این کار باید بعد از علامت & کلمه‌ی mut را قرار دهیم.
خب حالا که دیدیم چطوری می‌توان یک رفرنس mutable ساخت، ببنیم که چطوری می‌شود به یک مقدار چندین رفرنس داد.

رفرنس‌دهی چندگانه

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

fn main() {
    let a = String::from("hello");
    let reference1 = &a;
    let reference2 = &a;
    ali(reference1);
    hossein(reference2);
    println!("a in main function: {}", a);

}

fn ali(original_text: &String) {
    println!("Ali says: {}", original_text);
}

fn hossein(text: &String) {
    println!("{} hossein", text);
}

اگر این برنامه‌را کامپایل و اجرا کنیم، بدون هیچ مشکلی خروجی‌ای که انتظارش را داریم تولید می‌شود:

Ali says: hello
hello hossein
a in main function: hello

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

fn main() {
    let mut a = String::from("hello");
    let reference1 = &a;
    let reference2 = &a;
    let reference3 = &mut a;
    ali(reference1);
    mohammad(reference3);
    hossein(reference2);
    println!("a in main function: {}", a);
}
fn ali(original_text: &String) {
    println!("Ali says: {}", original_text);
}
fn hossein(text: &String) {
    println!("{} hossein", text);
}
fn mohammad(original_input: &mut String) {
    original_input.push_str("!");
}

خب حالا اگر این برنامه‌را بخواهیم کامپایل کنیم چه اتّفاقی می‌افتد؟

error[E0502]: cannot borrow `a` as mutable because it is also borrowed as immutable
  --> src/main.rs:14:27
   |
12 |     let reference1 = &a;
   |                       - immutable borrow occurs here
13 |     let reference2 = &a;
14 |     let reference3 = &mut a;
   |                           ^ mutable borrow occurs here
...
19 | }
   | - immutable borrow ends here

error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
  --> src/main.rs:18:40
   |
14 |     let reference3 = &mut a;
   |                           - mutable borrow occurs here
...
18 |     println!("a in main function: {}", a);
   |                                        ^ immutable borrow occurs here
19 | }
   | - mutable borrow ends here

خب با یک ارور بلندبالا مواجه شدیم. بیایید قدم به قدم با ارور جلو برویم و ببینیم که چه اتّفاقی افتاده است.
اوّل ارور این متن نوشته شده است:

cannot borrow `a` as mutable because it is also borrowed as immutable

کامپایلر به ما می‌گوید که نمی‌تواند یک رفرنس mutable به متغیّر a اضافه کند، چون پیش از آن، و البته به صورت همزمان (در یک scope)، رفرنس‌های immutable به این متغیّر ساخته شده است.
در دو بخش بعدی ارور هم کامپایلر به ما نشان می‌دهد که مشکل کجای کد رخ‌داده است:

--> src/main.rs:14:27
   |
12 |     let reference1 = &a;
   |                       - immutable borrow occurs here
13 |     let reference2 = &a;
14 |     let reference3 = &mut a;
   |                           ^ mutable borrow occurs here
...
19 | }
   | - immutable borrow ends here

اینجا کامپایلر به ما نشان می‌دهد که ابتدا یک immutable borrow رخ داده است، یعنی یک رفرنس immutable به متغیّر a ساخته شده است، و بعد یک mutable borrow.

در بخش بعدی هم کامپایلر توضیحات مشابهی می‌دهد.

امّا چرا کامپایلر Rust به ما اجازه‌ی ساخت رفرنس mutable را همراه رفرنس‌های immutable نمی‌دهد؟

محدودیت‌های ساخت رفرنس

زبان Rust به ما اجازه نمی‌دهد که وقتی که داریم از یک رفرنس mutable استفاده می‌کنیم، رفرنس دیگری داشته باشیم.
محدودیت ساخت رفرنس‌های متعدد به یک داده زمانی اجرایی می‌شود که هر۲ شرط زیر برقرار باشند:
۱) دو یا چند اشاره‌گر (رفرنس) به صورت هم‌زمان به یک داده دسترسی داشته باشند.
۲) حدّاقل یکی از این اشاره‌گرها برای نوشتن روی داده استفاده شوند (رفرنس mutable).

امّا چرا این حالت مشکل‌زا است و Rust جلوی اتّفاق افتادنش را می‌گیرد؟

Data Race

وقتی که چند رفرنس immutable به صورت همزمان به یک داده اشاره می‌کنند مشکلی ایجاد نمی‌شود. هرکدام می‌توانند بدون اینکه خللی به کار دیگران وارد کنند داده‌را بخوانند.
امّا وقتی که بیش از یک رفرنس داریم و حدّاقل یکی از آن‌ها mutable است قضیه فرق می‌کند.
فرض کنید که تنها یک اشاره‌گر برای نوشتن داریم و بقیه‌ی رفرنس‌ها immutable هستند. بخش‌هایی از کد که دارند از این رفرنس‌های immutable استفاده می‌کنند، انتظار آن‌را ندارند که داده وسط کارشان تغییر کند. امّا بخشی از برنامه که توانایی نوشتن روی داده را دارد، می‌تواند در حین کار آن‌ها داده‌را تغییر بدهد و کارشان‌را خراب کند.
حالا اگر بیش از ۱ اشاره‌گر نویسنده داشته باشیم قضیه بدتر می‌شود. در این حالت هر رفرنس mutable هم می‌تواند کار بخش‌هایی که صرفاً دارند داده‌را می‌خوانند خراب کند، و هم می‌تواند با قراردادن داده‌های خود مابین داده‌های رفرنس mutable دیگر، داده‌های آن‌را هم خراب کند.
به این حالت اصطلاحاً data race می‌گویند. data race یکی از بدترین باگ‌هایی است که می‌تواند در یک برنامه ایجاد شود و پیداکردن آن بسیار سخت است.
زبان Rust برای اینکه مطمئن شود هرگز data race رخ نمی‌دهد، به شما اصلاً اجازه‌ی این‌را نمی‌دهد که به صورت هم‌زمان یک رفرنس mutable ایجاد کنید و درکنارش رفرنس‌های دیگری هم داشته باشید.
این‌طوری وقتی برنامه با موفّقیّت کامپایل شد، می‌توانید مطمئن باشید که در آن data race وجود ندارد.
علاوه‌بر حالتی که دیدیم، اگر برنامه‌ای بنویسید که مثلاً دوتا mutable reference هم داشته باشد باز با اروری مشابه چیزی که دیدیم مواجه می‌شوید.

معنی هم‌زمان بودن اشاره‌گرها

باید به کلمه‌ی هم‌زمان بودن در اوّلین شرط از شرایط محدودیت‌های ساخت رفرنس خیلی دقّت کنید.
ما زمانی دوتا رفرنس به صورت هم‌زمان داریم که آن‌ها درون یک scope واحد تعریف شده باشند.
مثلاً در همین برنامه‌ای که بالاتر نوشتیم، متغیّرهای reference1، reference2 و reference3 همه در scope تابع main قرار دارند. پس ما به صورت هم‌زمان ۳ تا رفرنس به متغیّر a داریم.
حالا به برنامه‌ی زیر که دقیقا‌ً همان کاری را می‌کند که انتظار داشتیم برنامه‌ی قبلی انجام بدهد نگاه کنید:

fn main() {
    let mut a = String::from("hello");
    ali(&a);
    mohammad(&mut a);
    hossein(&a);
    println!("a in main function: {}", a);
}
fn ali(original_text: &String) {
    println!("Ali says: {}", original_text);
}
fn hossein(text: &String) {
    println!("{} hossein", text);
}
fn mohammad (original_input: &mut String) {
    original_input.push_str("!");
}

اگر این برنامه‌را کامپایل و اجرا کنیم می‌بینیم که دقیقاً همانطوری که انتظارش‌را داشتیم کار می‌کند و خروجی زیر را تولید می‌کند:

Ali says: hello
hello! hossein
a in main function: hello!

امّا چرا این کار کرد و برنامه‌ی قبلی نه؟
پاسخ در همان کلمه‌ی هم‌زمان است. اینجا هم ما ۳ رفرنس مختلف به متغیّر a داریم که یکی از آن‌ها mutable است. امّا این بار این ۳ رفرنس هم‌زمان ایجاد نشده اند. چون هرکدام مربوط به یک scope مختلف هستند.
یعنی رفرنس اوّل متعلّق به scope تابع ali است، دومی متعلّق به scope تابع mohammad و سومی هم متعلّق به تابع hossein.
علاوه بر اینکه این ۳ رفرنس متعلّق به scope های مختلف هستند، این برنامه‌ هم به صورت sequential اجرا می‌شود نه parallel. یعنی هرکدام از این توابع پس از تمام شدن تابع قبلی فراخوانی می‌شوند، نه هم‌زمان با اجرای آن‌ها. پس هرگز اینجا data race ایجاد نمی‌شود.
به همین خاطر است که کامپایلر Rust این بار اجازه‌ی داشتن چندین رفرنس‌را به یک مقدار می‌دهد.
این نکته شمارا از خیلی از اشتباهات آینده مصون می‌کند، البته اگر آن‌را همیشه به خاطر داشته باشید.

رفرنس‌های آویزان!

به عنوان آخرین بخش این آموزش به یکی دیگر از مشکلاتی که در دیگر زبان‌ها هنگام استفاده از رفرنس‌ها ایجاد می‌شود می‌پردازیم.
Dangling reference به رفرنسی گفته می‌شود که به جایی از حافظه اشاره می‌کند که دیگر داده‌ای که انتظارش می‌رود در آنجا نیست.
این اتّفاق زمانی می‌افتد که با آزاد شدن حافظه، آن مکان الان به جای دیگری اختصاص پیدا کرده است یا اینکه دیگر داده‌های قبلی به خاطر free شدن به صورت valid در آن‌جا قرار ندارند.
مثلاً برنامه‌ی ساده‌ی زیر را به زبان c درنظر بگیرید:

#include <stdlib.h>
#include <stdio.h>
char* dangle_generator() {
    char * a = (char*) malloc(sizeof(char) * 10);
    a = "hello";
    free(a);
    return a;
}

int main() {
    char* b = dangle_generator();
    printf("%s", b);
    return 0;
}

اگر این برنامه‌را کامپایل کنید هیچ مشکلی رخ نمی‌دهد. حالا سعی کنید برنامه‌ی کامپایل شده را اجرا کنید:

munmap_chunk(): invalid pointer
Aborted (core dumped)

موقع اجرا به مشکل بدی خوردیم. وجود dangling reference ها درون برنامه می‌تواند همه‌چیز را خراب کند، و مثل اکثر باگ‌های مرتبط با اشاره‌گرها، پیداکردن مشکل هم کار خیلی سختی است.
حالا بیایید مشابه برنامه‌ی بالا را به زبان Rust بنویسیم:

fn main() {
    let b = dangle_generator();
    println!("a in main function: {}", b);
}
fn dangle_generator() -> &String {
    let a = String::from("hello");
    &a
}

حالا اگر این برنامه‌را کامپایل کنیم با چه چیزی روبه‌رو می‌شویم؟

error[E0106]: missing lifetime specifier
 --> src/main.rs:6:26
  |
6 | fn dangle_generator() -> &String {
  |                          ^ expected lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
  = help: consider giving it a 'static lifetime

باز هم مثل همیشه کامپایلر همیشه در صحنه‌ی Rust وارد عمل می‌شود و به شما یک پیام خطای دقیق و کامل می‌دهد.
ابتدای پیام خطا مربوط به ویژگی lifetime زبان Rust می‌شود که فعلاً با آن کاری نداریم و بعداً به صورت مفصّل درموردش صحبت می‌کنیم‌، امّا در بخش help پیغام خطا به ما می‌گوید که چه مشکلی پیش‌آمده است.
متن ارور می‌گوید که در انتهای تابع می‌خواهیم یک مقدار قرض‌گرفته شده را برگردانیم (یعنی یک رفرنس)، امّا مقداری برای قرض‌گرفتن وجود ندارد.
حالا این یعنی چی؟ متغیّر a یک متغیّر محلّی درون تابع dangle_generator است. بنابراین با به انتها رسیدن scope این تابع، این مقدار هم drop می‌شود و دیگر در دسترس نیست. حالا ما داریم تلاش می‌کنیم یک رفرنس به مقداری که از بین خواهد رفت‌را برگردانیم. یعنی می‌خواهیم به چیزی که اصلاً دیگر وجود ندارد رفرنس بدهیم.
اینجا Rust از به وجود آمدن یک dangling reference جلوگیری می‌کند.
پس یکی دیگر از کرامات زبان Rust این است که وقتی برنامه کامپایل شد، می‌توانید مطمئن باشید که هیچ باگی مربوط به dangling reference ها درونش وجود ندارد.
اگر بخواهیم این مشکل به وجود نیاید، کافی است به جای رفرنس، کل مقدار را از تابع خروجی بدهیم. این‌طوری مالکیّت آن مقدار منتقل می‌شود و همه‌چیز به خیر می‌گذرد.

می‌توانید نسخه‌ی ویدیویی این آموزش را در کانال یوتیوب من ببینید:

YouTube video

در قسمت بعدی درمورد slicing صحبت می‌کنیم. آخرین ویژگی‌ای که مستقیماً به بحث مالکیّت (Ownership) مربوط می‌شود.
با دانستن آن عملاً اطّلاعات لازم را برای کار با مهم‌ترین ویژگی زبان Rust پیدا می‌کنیم، می‌توانیم به مفاهیم مهم و پرکاربرد دیگر بپردازیم و به نوشتن برنامه‌های کامل به این زبان نزدیک شویم.
اگر سؤالی درمورد این مفهوم داشتید می‌توانید در قسمت نظرات بیان کنید. شاید سؤال شما سؤال دیگران هم باشد. خوشحال می‌شوم که با همدیگر عمیق‌تر یادبگیریم.

The form you have selected does not exist.

همین الان به قسمت بعدی برو و با سومین ویژگی به‌خصوص Rust آشنا بشو.

اگر قسمت قبلی‌را نخوانده‌ای همین حالا به آنجا برو تا با Ownership، مفهوم بنیادی زبان Rust آشنا بشوی.

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

یا حرفه‌ای شو یا برنامه‌نویسی را رها کن.

چطور می‌شود در بازار کار خالی از نیروی سنیور ایران، بدون کمک دیگران و وقت تلف‌کردن خودت را تبدیل به یک نیروی باتجربه بکنی؟

پاسخ ساده است. باید حرفه‌ای بشوی. چیزی که من در این کتاب به تو یاد خواهم داد.

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

یک پاسخ به “آموزش زبان برنامه‌نویسی Rust – قسمت۸: Borrowing”

  1. محمد گفت:

    سلام
    خیلی ممنون از اموزش بسیار خوبتون واقعا کامل و جامع بود
    درخواستی داشتم اگر امکانش هستش توضیحی در مورد Circular references ها بنویسید متاسفانه این مورد یک مقدار من رو گیج کرده بنظرم پیچیده اس نسبت به موارد دیگه از مشکلات borrow
    ممنون
    منتظر اموزشهای خوب بعدی شما هستم و امیدوار لایف تایم رو زودتر بتونم ببینم توی بلاگتون

دیدگاهتان را بنویسید

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

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

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

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