نقش رفرنسها
با توجه به مطالب بیانشده در درس قبل، باید روشن باشد که کد زیر با خطا همراه خواهد بود.
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() را با گزارهی زیر
تعویض کنیم.
src/main.rs
let len = calculate_length(s1.clone());
اما تأکید میکنم که انجام کپی کامل، به کندی صورت میگیرد و در خیلی از موارد، انتخاب
بهینهای محسوب نمیشود. راه حلهای دیگری هم وجود دارد که بیشتر جنبهی ترفند و تکنیک دارند و
خیلی منطقی نیستند. راه حل اصولی در این موارد، استفاده از رفرنسهاست که همانطور که در مقدمه
گفته شد، امکان استفاده از دادهها بدون انتقال مالکیت را فراهم میکنند.
برای مثال، در کد بالا میتوانیم به جای s1 یک رفرنس از s1 را به تابع calculate_length() پاس کنیم.
یک رفرنس به متغیری مانند s1 با &s1 نمایش میشود.
البته قبل از آن، باید در تعریف تابع calculate_length() هم نوع پارامتر را به جای
String
با &String مشخص کنیم. این یعنی این تابع یک رفرنس از یک مقدار String را دریافت میکند.
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 به معنای قرض گرفتن هم میگویند. درست مثل زندگی واقعی که
ما چیزی را از کسی قرض میگیریم و بعد از پایان کارمان، آن را به مالکش برمیگردانیم.
خوب، حالا چه اتفاقی می افتد اگر سعی کنیم، چیزی را که قرض گرفتهایم، دستکاری کنیم؟
به کد زیر دقت کنید.
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
تبدیل کرد.
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 به یک مقدار ایجاد کنیم که با خطا همراه خواهد شد.
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 بدون مشکل ایجاد
میشود.
src/main.rs
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}");
let r3 = &mut s;
println!("{r3}");
}
محدودیت سختگیرانهای که برای رفرنسهای mutable در نظر گرفته شده، هدف و فلسفهی روشنی دارد.
در زبانهای برنامهنویسی که از اشارهگرها استفاده میکنند، بروز خطاهایی موسوم به
data race بسیار رایج است. این خطاها از دسترسی همزمان اشاره گرها به مقادیر و
احتمال ویرایش مقادیر توسط اشارهگرها، ناشی میشوند. این دست خطاها هم خیلی رایجاند و هم
اینکه پیدا کردن علت و محل بروز آنها سخت است. اما Rust با ممنوعیت رفرنسهای mutable همزمان، امکان
بروز این خطاها را از بین میبرد و در واقع، از زمان اجرا به زمان کامپایل منتقل میکند.
این هنر کامپایلر Rust است که خطایی را که میتواند در زمان اجرا رخ دهد، در
زمان کامپایل به دام میاندازد و ما را از نوشتن کدهایی که مستعد تولید خطاهای data race هستند، منع میکند.
رفرنسهای Dangling
یکی دیگر از اتفاقات بدی که در زبانهای مجهز به اشارهگر، میتواند رخ دهد، امکان
ایجاد اشارهگرهایی است که به مکان غلطی از حافظه اشاره میکنند. این اشارهگرها را dangling pointers یا
اشارهگرهای
آویزان یا کَنه میگویند. علت این نامگذاری این است که مقداری که یک چنین اشارهگری به
آن اشاره می کند، حذف شده اما اشارهگر ولکن ماجرا نیست و همچنان به آن نقطه از حافظه اشاره میکند.
حالا وقتی آن بخش از حافظه با مقدار دیگری پر شد، ما اشارهگری داریم که به دیتای غلطی اشاره میکند.
اما خبر خوب اینکه رفرنسهای Rust این مشکل را ندارند. اجازه دهید سعی کنیم یک رفرنس dangling ایجاد کنیم.
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) است اما
مقداری وجود ندارد که بخواهد قرض گرفته شود.