مقدمه

دیدیم که Rust چطور با تکیه بر سیستم مالکیت (ownership system) کار مدیریت حافظه را بدون توسل به GC انجام می‌دهد. در درس قبل با قوانین مالکیت و تأثیری که روی نوشتن کدهای Rust دارند، آشنا شدیم. اما از پازل مربوط به مدیریت حافظه در Rust یک تکه‌ی مهم باقی مانده که در اینجا قصد داریم به آن بپزدازیم و آن مفهوم Reference است. رفرنس‌ها امکان دسترسی به داده را بدون انتقال مالکیت، فراهم می‌کنند. رفرنس‌ها هم مثل متغیرها، در حالت پیش‌فرض، تغییرناپذیر یا immutable هستند اما امکان استفاده از ورژن تغییرپذیر یا mutable آنها هم وجود دارد؛ منتها در مورد رفرنس‌های mutable قوانین سخت‌گیرانه‌ای وجود دارد که با هدف جلوگیری از بروز برخی باگ‌ها و خطاهای رایج مربوط به حافظه، وضع شده‌اند. در ادامه، به بررسی این موارد می‌پردازیم.

نقش رفرنس‌ها

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

Copy Icon src/main.rs
fn main() {
  let s1 = String::from("hello");
        
  let len = calculate_length(s1);
        
  println!("The length of '{s1}' is {len}.");
}
        
fn calculate_length(s: String) -> usize {
  s.len()
}

در اینجا تابعی با نام calculate_length() تعریف شده که یک String را دریافت می‌کند و طول آن را برمی‌گرداند. اما علت بروز خطا، گزاره‌ی println!() در متد main() است که از متغیر s1 استفاده کرده، در حالی که این متغیر پس از فراخوانی تابع calculate_length() از دسترس خارج شده است.

راه حلی که تا اینجا برای این مشکل بلدیم، این است که با استفاده از متد clone() یک کپی کامل از s1 را به تابع پاس کنیم. یعنی گزاره‌ی دوم در متد main() را با گزاره‌ی زیر تعویض کنیم.

Copy Icon src/main.rs
let len = calculate_length(s1.clone());

اما تأکید می‌کنم که انجام کپی کامل، به کندی صورت می‌گیرد و در خیلی از موارد، انتخاب بهینه‌ای محسوب نمی‌شود. راه حل‌‌های دیگری هم وجود دارد که بیشتر جنبه‌ی ترفند و تکنیک دارند و خیلی منطقی نیستند. راه حل اصولی در این موارد، استفاده از رفرنس‌هاست که همانطور که در مقدمه گفته شد، امکان استفاده از داده‌ها بدون انتقال مالکیت را فراهم می‌کنند. برای مثال، در کد بالا می‌توانیم به جای s1 یک رفرنس از s1 را به تابع calculate_length() پاس کنیم. یک رفرنس به متغیری مانند s1 با &s1 نمایش می‌شود. البته قبل از آن، باید در تعریف تابع calculate_length() هم نوع پارامتر را به جای String با &String مشخص کنیم. این یعنی این تابع یک رفرنس از یک مقدار String را دریافت می‌کند.

Copy Icon src/main.rs
fn main() {
  let s1 = String::from("hello");
          
  let len = calculate_length(&s1);
          
  println!("The length of '{s1}' is {len}.");
}
          
fn calculate_length(s: &String) -> usize {
  s.len()
}

سینتکس &s1 به مقدار ذخیره‌شده در s1 ارجاع می‌دهد، بدون اینکه مالکیت آن را بگیرد. عمل ایجاد یک رفرنس را Borrowing به معنای قرض گرفتن هم می‌گویند. درست مثل زندگی واقعی که ما چیزی را از کسی قرض می‌گیریم و بعد از پایان کارمان، آن را به مالکش برمی‌گردانیم. خوب، حالا چه اتفاقی می افتد اگر سعی کنیم، چیزی را که قرض گرفته‌ایم، دستکاری کنیم؟ به کد زیر دقت کنید.

Copy Icon src/main.rs
fn main() {
  let s = String::from("hello");
          
  change(&s);
}
          
fn change(some_string: &String) {
  some_string.push_str(", world");
}

نتیجه، تولید خطای زیر است.

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --< src/main.rs:8:5
  |
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
7 | fn change(some_string: &mut String) {
  |                         +++

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
          

این مثال و خطای حاصل از ان نشان می‌دهد که رفرنس‌ها هم مثل متغیرها، در حالت پیش‌فرض، immutable هستند.

رفرنس‌های Mutable

مثل متغیرها، رفرنس‌ها را هم می‌توان با استفاده از کلمه کلیدی mut به آیتم‌های mutable تبدیل کرد.

Copy Icon src/main.rs
fn main() {
  let mut s = String::from("hello");
          
  change(&mut s);
}
          
fn change(some_string: &mut String) {
  some_string.push_str(", world");
}

توجه داشته باشید که برای اینکه بتوان یک رفرنس mutable به یک متغیر ایجاد کرد، خود آن متغیر باید mutable باشد. بنابراین، در اینجا ما قبل از هر چیز، متغیر s را به صورت mutable تعریف کرده‌ایم. در تعریف تابع change() هم نوع پارامتر را &mut String تعیین کرده‌ایم که به این معناست که آرگومان ارسالی به این تابع باید یک رفرنس mutable به یک مقدار String باشد.

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

Copy Icon src/main.rs
fn main() {
  let mut s = String::from("hello");
          
  let r1 = &mut s;
  let r2 = &mut s;
          
  println!("{}, {}", r1, r2);
}

کامپایلر، محل بروز خطا را گزاره‌ی سوم، یعنی جایی که یک رفرنس از s به متغیر r2 داده شده، نشان می‌دهد. اما اگر گزاره‌ی println!() را حذف یا کامنت کنید، خواهید دید که خطا از بین می‌رود. داستان چیست؟ به توضیح زیر خوب دقت کنید.

scope یا محدوده‌ی اعتبار یک رفرنس از جایی که تعریف شده، شروع می‌شود و جایی که برای آخرین بار مورد استفاده قرار گرفته، به پایان می‌رسد. اولین رفرنس mutable به s به متغیر r1 داده شده و تا گزاره‌ی println!() ادامه یافته است. در این فاصله، نمی‌توان رفرنس دیگری به s ایجاد کرد اما ما این کار را کرده‌ایم و به این دلیل است که خطا رخ داده است. در واقع، حتی اگر رفرنس دوم از نوع immutable هم باشد، همچنان خطا خواهیم داشت. چون قاعده این است که تا زمانی که یک رفرنس mutable به یک مقدار به پایان نرسیده، نمی‌توان هیچ رفرنس دیگری به ان مقدار، چه mutable و چه immutable ایجاد کرد. حطای زیر در نتیجه‌ی اجرای کد بالا، گزارش می‌شود.

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --< src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
          

اما کد زیر بدون خطا کامپایل می‌شود. چون محدوده‌ی رفرنس‌های r1 و r2 بعد از گزاره‌ی println!() به پایان رسیده و بنابراین، رفرنس r3 بدون مشکل ایجاد می‌شود.

Copy Icon src/main.rs
fn main() {
  let mut s = String::from("hello");
          
  let r1 = &s; // no problem
  let r2 = &s; // no problem
  println!("{r1} and {r2}");
  // variables r1 and r2 will not be used after this point
          
  let r3 = &mut s; // no problem
  println!("{r3}");
}

محدودیت سخت‌گیرانه‌ای که برای رفرنس‌های mutable در نظر گرفته شده، هدف و فلسفه‌ی روشنی دارد. در زبان‌های برنامه‌نویسی که از اشاره‌گرها استفاده می‌کنند، بروز خطاهایی موسوم به data race بسیار رایج است. این خطاها از دسترسی همزمان اشاره گرها به مقادیر و احتمال ویرایش مقادیر توسط اشاره‌گرها، ناشی می‌شوند. این دست خطاها هم خیلی رایج‌اند و هم اینکه پیدا کردن علت و محل بروز آنها سخت است. اما Rust با ممنوعیت رفرنس‌های mutable همزمان، امکان بروز این خطاها را از بین می‌برد و در واقع، از زمان اجرا به زمان کامپایل منتقل می‌کند. این هنر کامپایلر Rust است که خطایی را که می‌تواند در زمان اجرا رخ دهد، در زمان کامپایل به دام می‌اندازد و ما را از نوشتن کدهایی که مستعد تولید خطاهای data race هستند، منع می‌کند.

رفرنس‌های Dangling

یکی دیگر از اتفاقات بدی که در زبان‌های مجهز به اشاره‌گر، می‌تواند رخ دهد، امکان ایجاد اشاره‌گرهایی است که به مکان غلطی از حافظه اشاره می‌کنند. این اشاره‌گرها را dangling pointers یا اشاره‌گرهای آویزان یا کَنه می‌گویند. علت این نامگذاری این است که مقداری که یک چنین اشاره‌گری به آن اشاره می کند، حذف شده اما اشاره‌گر ول‌کن ماجرا نیست و همچنان به آن نقطه از حافظه اشاره می‌کند. حالا وقتی آن بخش از حافظه با مقدار دیگری پر شد، ما اشاره‌گری داریم که به دیتای غلطی اشاره می‌کند. اما خبر خوب اینکه رفرنس‌های Rust این مشکل را ندارند. اجازه دهید سعی کنیم یک رفرنس dangling ایجاد کنیم.

Copy Icon src/main.rs
fn main() {
  let reference_to_nothing = dangle();
}
          
fn dangle() -> &String {
  let s = String::from("hello");
          
  &s
}

چیزی که تابع dangle() برمی‌رگداند، یک رفرنس به متغیر s است. اما متغیر s با پایان تابع، از دسترس خارج می‌شود و بنابراین، خروجی این تابع که در متد main() به یک متغیر تخصیص داده شده، یک رفرنس است که به دیتای حذف‌شده اشاره می‌کند؛ یعنی یک رفرنس از نوع dangling. اما اگر این کد را اجرا کنید، خواهید دید که کامپایلر مانع کامپایل آن شده و خطای زیر را گزارش می‌کند.

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --< src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named 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 using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
5 | fn dangle() -> &'static String {
  |                 +++++++
help: instead, you are more likely to want to return an owned value
  |
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
  |

error[E0515]: cannot return reference to local variable `s`
 --< src/main.rs:8:5
  |
8 |     &s
  |     ^^ returns a reference to data owned by the current function

Some errors have detailed explanations: E0106, E0515.
For more information about an error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 2 previous errors
          

در واقع، کامپایلر حتی منتظر نمی‌ماند که ما از خروجی تابع dangle() استفاده کنیم و به محض دیدن تعریف تابع dangle() خطا را تولید می‌کند. یعنی در کد بالا، حتی اگر گزاره‌ی درون متد main() را حذف کنیم، باز هم خطای کامپایلر را دریافت خواهیم کرد.

در پیغام خطای بالا به مفهومی به نام lifetime اشاره شده که ما هنوز در مورد آن صحبتی نکرده‌ایم. lifetime یک مفهوم کلیدی است که در فصل دهم به آن می‌پردازیم اما فعلاً همینقدر بدانید که رفرنس‌ها دارای یک ویژگی به نام lifetime یا طول عمر هستند. منهای بخشی که از lifetime نام برده شده، بخش زیر در پیغام خطای بالا، علت اصلی بروز خطا را مشخص می‌کند.

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
          

این پیغام می‌گوید که نوع بازگشتی تابع یک رفرنس یا یک مقدار قرض گرفته‌شده (borrowed value) است اما مقداری وجود ندارد که بخواهد قرض گرفته شود.

قوانین مربوط به رفرنس‌ها

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

  • در هر لحظه، می توانیم فقط یک رفرنس mutable یا هر تعداد رفرنس immutable داشته باشیم.
  • رفرنس‌ها باید همیشه معتبر (valid) باشند.

در درس بعد، در مورد یک نوع متفاوت از رفرنس‌ها با نام slice صحبت می‌کنیم.