اعتبارسنجی مراجع با طول عمرها
طول عمرها نوع دیگری از جنریکها هستند که ما قبلاً از آنها استفاده کردهایم. به جای اطمینان از اینکه یک نوع رفتار مورد نظر ما را دارد، طول عمرها تضمین میکنند که مراجع به اندازهای که نیاز داریم معتبر باقی میمانند.
یکی از جزئیاتی که در بخش “مراجع و قرض گرفتن” در فصل ۴ بررسی نکردیم این است که هر مرجع در Rust دارای یک طول عمر است، که محدودهای است که آن مرجع در آن معتبر است. بیشتر اوقات، طول عمرها ضمنی و استنتاجشده هستند، دقیقاً مانند انواع. ما فقط زمانی نیاز داریم که نوعها را حاشیهنویسی کنیم که چندین نوع ممکن باشند. به طور مشابه، ما فقط زمانی نیاز داریم که طول عمرها را حاشیهنویسی کنیم که طول عمر مراجع بتوانند به چند روش مختلف مرتبط باشند. Rust ما را ملزم میکند تا روابط را با استفاده از پارامترهای جنریک طول عمر حاشیهنویسی کنیم تا اطمینان حاصل کنیم که مراجع واقعی استفادهشده در زمان اجرا قطعاً معتبر خواهند بود.
حاشیهنویسی طول عمر مفهومی نیست که بیشتر زبانهای برنامهنویسی داشته باشند، بنابراین ممکن است این موضوع برای شما ناآشنا باشد. اگرچه در این فصل طول عمرها را به طور کامل پوشش نمیدهیم، اما روشهای رایجی که ممکن است با نحو طول عمر مواجه شوید را بررسی میکنیم تا بتوانید با این مفهوم آشنا شوید.
جلوگیری از مراجع آویزان (Dangling References) با طول عمرها
هدف اصلی طول عمرها جلوگیری از مراجع آویزان است، که باعث میشوند یک برنامه به دادههایی غیر از دادههایی که قرار بوده مراجعه کند اشاره کند. برنامهای را در نظر بگیرید که در لیست ۱۰-۱۶ نشان داده شده است و دارای یک محدوده خارجی و یک محدوده داخلی است.
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
توجه: مثالهای لیست ۱۰-۱۶، ۱۰-۱۷، و ۱۰-۲۳ متغیرهایی را بدون مقدار اولیه اعلام میکنند، بنابراین نام متغیر در محدوده خارجی وجود دارد. در نگاه اول، این ممکن است در تضاد با عدم وجود مقادیر null در Rust به نظر برسد. با این حال، اگر سعی کنیم از متغیری قبل از مقداردهی آن استفاده کنیم، یک خطای زمان کامپایل دریافت خواهیم کرد، که نشان میدهد Rust واقعاً مقادیر null را مجاز نمیداند.
محدوده خارجی یک متغیر به نام r
را بدون مقدار اولیه اعلام میکند، و محدوده داخلی یک متغیر به نام x
را با مقدار اولیه 5
اعلام میکند. در داخل محدوده داخلی، تلاش میکنیم مقدار r
را به عنوان یک مرجع به x
تنظیم کنیم. سپس محدوده داخلی به پایان میرسد و سعی میکنیم مقدار موجود در r
را چاپ کنیم. این کد کامپایل نخواهد شد زیرا مقداری که r
به آن اشاره میکند قبل از اینکه سعی کنیم از آن استفاده کنیم از محدوده خارج شده است. پیام خطای زیر را دریافت میکنیم:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
5 | let x = 5;
| - binding `x` declared here
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
8 |
9 | println!("r: {r}");
| --- borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
پیام خطا میگوید که متغیر x
“به اندازه کافی طول عمر ندارد.” دلیل این است که x
وقتی محدوده داخلی در خط ۷ پایان مییابد، از محدوده خارج میشود. اما r
همچنان برای محدوده خارجی معتبر است؛ زیرا محدوده آن بزرگتر است، میگوییم که “طول عمر بیشتری دارد.” اگر Rust به این کد اجازه کار کردن میداد، r
به حافظهای اشاره میکرد که وقتی x
از محدوده خارج شد آزاد شده است، و هر کاری که سعی میکردیم با r
انجام دهیم به درستی کار نمیکرد. پس چگونه Rust تشخیص میدهد که این کد نامعتبر است؟ از یک بررسیکننده قرض (borrow checker) استفاده میکند.
بررسیکننده قرض (Borrow Checker)
کامپایلر Rust دارای یک بررسیکننده قرض است که محدودهها را مقایسه میکند تا تعیین کند که آیا تمام قرضها معتبر هستند یا خیر. لیست ۱۰-۱۷ همان کد لیست ۱۰-۱۶ را نشان میدهد اما با حاشیهنویسیهایی که طول عمر متغیرها را نشان میدهد.
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
r
و x
که به ترتیب به نامهای 'a
و 'b
نامگذاری شدهانددر اینجا، طول عمر r
را با 'a
و طول عمر x
را با 'b
حاشیهنویسی کردهایم. همانطور که میبینید، بلوک داخلی 'b
بسیار کوچکتر از بلوک طول عمر خارجی 'a
است. در زمان کامپایل، Rust اندازه دو طول عمر را مقایسه میکند و میبیند که r
دارای طول عمر 'a
است اما به حافظهای اشاره میکند که طول عمر آن 'b
است. برنامه رد میشود زیرا 'b
کوتاهتر از 'a
است: موضوع مرجع به اندازه مرجع زنده نیست.
لیست ۱۰-۱۸ کد را اصلاح میکند تا یک مرجع آویزان نداشته باشد و بدون هیچ خطایی کامپایل شود.
fn main() { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {r}"); // | | // --+ | } // ----------+
در اینجا، x
دارای طول عمر 'b
است که در این مورد بزرگتر از 'a
است. این بدان معناست که r
میتواند به x
اشاره کند زیرا Rust میداند که مرجع در r
همیشه در حالی که x
معتبر است، معتبر خواهد بود.
حالا که میدانید طول عمر مراجع چیست و چگونه Rust طول عمرها را تحلیل میکند تا اطمینان حاصل کند که مراجع همیشه معتبر خواهند بود، بیایید طول عمرهای جنریک پارامترها و مقادیر بازگشتی را در زمینه توابع بررسی کنیم.
طول عمرهای جنریک در توابع
ما تابعی خواهیم نوشت که طولانیترین قطعه رشته (string slice) را بازمیگرداند. این تابع دو قطعه رشته میگیرد و یک قطعه رشته بازمیگرداند. پس از پیادهسازی تابع longest
، کد در لیست ۱۰-۱۹ باید The longest string is abcd
را چاپ کند.
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
main
که تابع longest
را فراخوانی میکند تا طولانیترین قطعه رشته را پیدا کندتوجه داشته باشید که میخواهیم تابع قطعه رشتهها، که مراجع هستند، بگیرد نه رشتهها، زیرا نمیخواهیم تابع longest
مالکیت پارامترهای خود را بگیرد. برای بحث بیشتر درباره اینکه چرا پارامترهایی که در لیست ۱۰-۱۹ استفاده میکنیم همانهایی هستند که میخواهیم، به بخش “قطعه رشتهها به عنوان پارامترها” در فصل ۴ مراجعه کنید.
اگر سعی کنیم تابع longest
را همانطور که در لیست ۱۰-۲۰ نشان داده شده است پیادهسازی کنیم، کامپایل نمیشود.
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
longest
که طولانیترین قطعه رشته را بازمیگرداند اما هنوز کامپایل نمیشودبه جای آن، خطای زیر را دریافت میکنیم که درباره طول عمرها صحبت میکند:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
متن کمکی نشان میدهد که نوع بازگشتی نیاز به یک پارامتر طول عمر جنریک دارد زیرا Rust نمیتواند تشخیص دهد که مرجع بازگرداندهشده به x
اشاره میکند یا به y
. در واقع، ما هم نمیدانیم، زیرا بلوک if
در بدنه این تابع یک مرجع به x
بازمیگرداند و بلوک else
یک مرجع به y
بازمیگرداند!
وقتی این تابع را تعریف میکنیم، مقادیر مشخصی که به این تابع پاس داده میشوند را نمیدانیم، بنابراین نمیدانیم که آیا حالت if
یا حالت else
اجرا خواهد شد. همچنین طول عمرهای مشخص مراجع پاسدادهشده را نمیدانیم، بنابراین نمیتوانیم به محدودهها مانند لیستهای ۱۰-۱۷ و ۱۰-۱۸ نگاه کنیم تا تعیین کنیم که مرجعی که بازمیگردانیم همیشه معتبر خواهد بود. بررسیکننده قرض هم نمیتواند این موضوع را تعیین کند زیرا نمیداند چگونه طول عمرهای x
و y
به طول عمر مقدار بازگشتی مرتبط هستند. برای رفع این خطا، پارامترهای طول عمر جنریک اضافه میکنیم که رابطه بین مراجع را تعریف میکنند تا بررسیکننده قرض بتواند تحلیل خود را انجام دهد.
نحو حاشیهنویسی طول عمر
حاشیهنویسی طول عمر طول عمر هیچیک از مراجع را تغییر نمیدهد. بلکه، آنها روابط طول عمرهای چندین مرجع را بدون تأثیر بر طول عمرها توصیف میکنند. همانطور که توابع میتوانند هر نوعی را بپذیرند وقتی امضا یک پارامتر نوع جنریک را مشخص میکند، توابع میتوانند مراجع با هر طول عمری را بپذیرند با مشخص کردن یک پارامتر طول عمر جنریک.
حاشیهنویسی طول عمر دارای نحو کمی غیرمعمول است: نامهای پارامتر طول عمر باید با یک آپاستروف ('
) شروع شوند و معمولاً همه حروف کوچک و بسیار کوتاه هستند، مانند نوعهای جنریک. بیشتر افراد از نام 'a
برای اولین حاشیهنویسی طول عمر استفاده میکنند. ما حاشیهنویسیهای پارامتر طول عمر را بعد از &
یک مرجع قرار میدهیم و از یک فاصله برای جدا کردن حاشیهنویسی از نوع مرجع استفاده میکنیم.
در اینجا چند مثال آورده شده است: یک مرجع به یک i32
بدون پارامتر طول عمر، یک مرجع به یک i32
که یک پارامتر طول عمر به نام 'a
دارد، و یک مرجع قابل تغییر به یک i32
که همچنین طول عمر 'a
دارد:
&i32 // یک مرجع
&'a i32 // یک مرجع با طول عمر صریح
&'a mut i32 // یک مرجع قابل تغییر با طول عمر صریح
یک حاشیهنویسی طول عمر به تنهایی معنای زیادی ندارد زیرا حاشیهنویسیها برای توضیح دادن به Rust هستند که پارامترهای طول عمر جنریک چندین مرجع چگونه به یکدیگر مرتبط هستند. بیایید بررسی کنیم که حاشیهنویسیهای طول عمر چگونه در زمینه تابع longest
به یکدیگر مرتبط هستند.
حاشیهنویسی طول عمر در امضاهای توابع
برای استفاده از حاشیهنویسیهای طول عمر در امضاهای توابع، باید پارامترهای طول عمر جنریک را در داخل پرانتزهای زاویهای بین نام تابع و لیست پارامتر اعلام کنیم، همانطور که با پارامترهای نوع جنریک انجام دادیم.
ما میخواهیم امضا محدودیت زیر را بیان کند: مرجع بازگرداندهشده تا زمانی که هر دو پارامتر معتبر هستند معتبر خواهد بود. این رابطه بین طول عمرهای پارامترها و مقدار بازگشتی است. طول عمر را به نام 'a
مینامیم و سپس آن را به هر مرجع اضافه میکنیم، همانطور که در لیست ۱۰-۲۱ نشان داده شده است.
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {result}"); } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
longest
که مشخص میکند تمام مراجع در امضا باید طول عمر یکسانی به نام 'a
داشته باشنداین کد باید کامپایل شود و نتیجه مورد نظر ما را زمانی که با تابع main
در لیست ۱۰-۱۹ استفاده میکنیم تولید کند.
امضای تابع اکنون به Rust میگوید که برای برخی طول عمر 'a
، تابع دو پارامتر میگیرد که هر دو قطعه رشتههایی هستند که حداقل به مدت طول عمر 'a
زندگی میکنند. امضای تابع همچنین به Rust میگوید که قطعه رشتهای که از تابع بازگردانده میشود حداقل به مدت طول عمر 'a
زندگی میکند. در عمل، این بدان معناست که طول عمر مرجعی که توسط تابع longest
بازگردانده میشود همان طول عمر کوچکتر مقادیر اشارهشده توسط آرگومانهای تابع است. این روابط چیزی است که ما میخواهیم Rust هنگام تحلیل این کد از آنها استفاده کند.
به یاد داشته باشید، وقتی پارامترهای طول عمر را در این امضای تابع مشخص میکنیم، طول عمر هیچیک از مقادیر پاسدادهشده یا بازگرداندهشده را تغییر نمیدهیم. بلکه، مشخص میکنیم که بررسیکننده قرض باید هر مقداری را که به این محدودیتها پایبند نیست رد کند. توجه داشته باشید که تابع longest
نیازی به دانستن دقیق اینکه x
و y
چقدر زنده خواهند ماند ندارد، تنها اینکه برخی محدودهها میتوانند جایگزین 'a
شوند که این امضا را برآورده کنند.
هنگام حاشیهنویسی طول عمرها در توابع، حاشیهنویسیها در امضای تابع قرار میگیرند، نه در بدنه تابع. حاشیهنویسیهای طول عمر بخشی از قرارداد تابع میشوند، مشابه انواع موجود در امضا. داشتن امضای تابع که شامل قرارداد طول عمر است به این معنی است که تحلیلی که کامپایلر Rust انجام میدهد میتواند سادهتر باشد. اگر مشکلی در نحوه حاشیهنویسی یک تابع یا نحوه فراخوانی آن وجود داشته باشد، خطاهای کامپایلر میتوانند به بخش مشخصی از کد ما و محدودیتها اشاره کنند. اگر، به جای آن، کامپایلر Rust استنتاج بیشتری درباره آنچه که قصد داریم روابط طول عمرها باشند انجام دهد، کامپایلر ممکن است فقط بتواند به استفادهای از کد ما اشاره کند که چندین مرحله دور از علت مشکل باشد.
وقتی مراجع مشخصی را به longest
پاس میدهیم، طول عمر مشخصی که برای 'a
جایگزین میشود بخشی از محدوده x
است که با محدوده y
همپوشانی دارد. به عبارت دیگر، طول عمر جنریک 'a
طول عمر مشخصی را میگیرد که برابر با کوچکترین طول عمرهای x
و y
است. از آنجا که مرجع بازگرداندهشده را با همان پارامتر طول عمر 'a
حاشیهنویسی کردهایم، مرجع بازگرداندهشده نیز برای مدت کوچکترین طول عمرهای x
و y
معتبر خواهد بود.
بیایید ببینیم حاشیهنویسی طول عمرها چگونه تابع longest
را محدود میکند با پاس دادن مراجع که طول عمرهای مشخص مختلفی دارند. لیست ۱۰-۲۲ یک مثال ساده است.
fn main() { let string1 = String::from("long string is long"); { let string2 = String::from("xyz"); let result = longest(string1.as_str(), string2.as_str()); println!("The longest string is {result}"); } } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
longest
با مراجع به مقادیر String
که طول عمرهای مشخص مختلفی دارنددر این مثال، string1
تا پایان محدوده خارجی معتبر است، string2
تا پایان محدوده داخلی معتبر است، و result
به چیزی اشاره میکند که تا پایان محدوده داخلی معتبر است. این کد را اجرا کنید و خواهید دید که بررسیکننده قرض آن را تأیید میکند؛ کد کامپایل میشود و The longest string is long string is long
را چاپ میکند.
حال، بیایید مثالی را امتحان کنیم که نشان دهد طول عمر مرجع در result
باید کوچکترین طول عمر دو آرگومان باشد. اعلام متغیر result
را به بیرون از محدوده داخلی منتقل میکنیم، اما مقداردهی به متغیر result
را درون محدوده با string2
نگه میداریم. سپس println!
که از result
استفاده میکند را به بیرون از محدوده داخلی، پس از پایان محدوده داخلی منتقل میکنیم. کد در لیست ۱۰-۲۳ کامپایل نمیشود.
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {result}");
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
result
پس از اینکه string2
از محدوده خارج شده استوقتی تلاش میکنیم این کد را کامپایل کنیم، خطای زیر را دریافت میکنیم:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
5 | let string2 = String::from("xyz");
| ------- binding `string2` declared here
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("The longest string is {result}");
| -------- borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
این خطا نشان میدهد که برای اینکه result
برای دستور println!
معتبر باشد، string2
باید تا پایان محدوده خارجی معتبر باشد. Rust این را میداند زیرا طول عمرهای پارامترهای تابع و مقادیر بازگشتی را با استفاده از همان پارامتر طول عمر 'a
حاشیهنویسی کردهایم.
به عنوان انسان، میتوانیم به این کد نگاه کنیم و ببینیم که string1
طولانیتر از string2
است، و بنابراین، result
یک مرجع به string1
خواهد داشت. زیرا string1
هنوز از محدوده خارج نشده است، یک مرجع به string1
برای دستور println!
همچنان معتبر خواهد بود. با این حال، کامپایلر نمیتواند ببیند که این مرجع در این مورد معتبر است. ما به Rust گفتهایم که طول عمر مرجع بازگرداندهشده توسط تابع longest
همان طول عمر کوچکترین مرجعهای پاسدادهشده است. بنابراین، بررسیکننده قرض کد در لیست ۱۰-۲۳ را به عنوان داشتن یک مرجع نامعتبر احتمالی رد میکند.
سعی کنید آزمایشهای بیشتری طراحی کنید که مقادیر و طول عمر مراجع پاسدادهشده به تابع longest
و نحوه استفاده از مرجع بازگرداندهشده را تغییر دهند. فرضیاتی درباره اینکه آیا آزمایشهای شما بررسیکننده قرض را پاس میکنند یا نه ایجاد کنید؛ سپس بررسی کنید که آیا درست میگویید!
تفکر بر اساس طول عمرها
نحوه نیاز شما به مشخص کردن پارامترهای طول عمر به آنچه که تابع شما انجام میدهد بستگی دارد. برای مثال، اگر پیادهسازی تابع longest
را تغییر دهیم تا همیشه اولین پارامتر را به جای طولانیترین قطعه رشته بازگرداند، نیازی به مشخص کردن طول عمر برای پارامتر y
نخواهیم داشت. کد زیر کامپایل میشود:
fn main() { let string1 = String::from("abcd"); let string2 = "efghijklmnopqrstuvwxyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {result}"); } fn longest<'a>(x: &'a str, y: &str) -> &'a str { x }
ما یک پارامتر طول عمر 'a
برای پارامتر x
و نوع بازگشتی مشخص کردهایم، اما برای پارامتر y
نه، زیرا طول عمر y
هیچ رابطهای با طول عمر x
یا مقدار بازگشتی ندارد.
هنگام بازگرداندن یک مرجع از یک تابع، پارامتر طول عمر برای نوع بازگشتی باید با پارامتر طول عمر یکی از پارامترها مطابقت داشته باشد. اگر مرجع بازگرداندهشده به یکی از پارامترها اشاره نکند، باید به مقداری که در این تابع ایجاد شده است اشاره کند. با این حال، این یک مرجع آویزان خواهد بود زیرا مقدار در پایان تابع از محدوده خارج میشود. به این پیادهسازی ناموفق تابع longest
که کامپایل نمیشود توجه کنید:
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
در اینجا، حتی اگر یک پارامتر طول عمر 'a
برای نوع بازگشتی مشخص کرده باشیم، این پیادهسازی کامپایل نمیشود زیرا طول عمر مقدار بازگشتی به هیچ وجه به طول عمر پارامترها مرتبط نیست. پیام خطایی که دریافت میکنیم به این شکل است:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ------^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `result` is borrowed here
For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
مشکل این است که result
از محدوده خارج میشود و در پایان تابع longest
پاک میشود. همچنین سعی میکنیم یک مرجع به result
را از تابع بازگردانیم. هیچ راهی وجود ندارد که بتوانیم پارامترهای طول عمری مشخص کنیم که مرجع آویزان را تغییر دهد، و Rust به ما اجازه نمیدهد یک مرجع آویزان ایجاد کنیم. در این مورد، بهترین راه حل این است که یک نوع داده مالک (owned) به جای یک مرجع بازگردانیم تا تابع فراخوانیکننده مسئول پاکسازی مقدار باشد.
در نهایت، نحو طول عمرها درباره ارتباط دادن طول عمرهای پارامترها و مقادیر بازگشتی توابع است. وقتی این ارتباط برقرار شد، Rust اطلاعات کافی برای اجازه دادن به عملیاتهای ایمن از نظر حافظه و منع عملیاتهایی که باعث ایجاد اشارهگر (Pointer)های آویزان یا نقض ایمنی حافظه میشوند، دارد.
حاشیهنویسی طول عمر در تعریف ساختارها
تا کنون، ساختارهایی که تعریف کردهایم همه دارای نوعهای مالک بودهاند. میتوانیم ساختارهایی را تعریف کنیم که مراجع نگه میدارند، اما در این صورت باید برای هر مرجعی در تعریف ساختار یک حاشیهنویسی طول عمر اضافه کنیم. لیست ۱۰-۲۴ یک ساختار به نام ImportantExcerpt
دارد که یک قطعه رشته نگه میدارد.
struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
این ساختار دارای یک فیلد به نام part
است که یک قطعه رشته نگه میدارد، که یک مرجع است. همانند نوعهای داده جنریک، ما نام پارامتر طول عمر جنریک را در داخل پرانتزهای زاویهای بعد از نام ساختار اعلام میکنیم تا بتوانیم پارامتر طول عمر را در بدنه تعریف ساختار استفاده کنیم. این حاشیهنویسی به این معنی است که یک نمونه از ImportantExcerpt
نمیتواند بیشتر از مرجعی که در فیلد part
خود نگه میدارد زنده بماند.
تابع main
در اینجا یک نمونه از ساختار ImportantExcerpt
ایجاد میکند که یک مرجع به اولین جمله از String
که توسط متغیر novel
نگه داشته میشود، نگه میدارد. دادههای novel
قبل از ایجاد نمونه ImportantExcerpt
وجود دارند. علاوه بر این، novel
تا بعد از خروج ImportantExcerpt
از محدوده از محدوده خارج نمیشود، بنابراین مرجع در نمونه ImportantExcerpt
معتبر است.
حذف طول عمر (Lifetime Elision)
آموختید که هر مرجع دارای یک طول عمر است و شما باید برای توابع یا ساختارهایی که از مراجع استفاده میکنند پارامترهای طول عمر مشخص کنید. با این حال، ما تابعی در لیست ۴-۹ داشتیم که دوباره در لیست ۱۰-۲۵ نشان داده شده است، که بدون حاشیهنویسی طول عمر کامپایل شد.
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("hello world"); // first_word works on slices of `String`s let word = first_word(&my_string[..]); let my_string_literal = "hello world"; // first_word works on slices of string literals let word = first_word(&my_string_literal[..]); // Because string literals *are* string slices already, // this works too, without the slice syntax! let word = first_word(my_string_literal); }
دلیل اینکه این تابع بدون حاشیهنویسی طول عمر کامپایل میشود تاریخی است: در نسخههای اولیه (قبل از 1.0) از Rust، این کد کامپایل نمیشد زیرا هر مرجع نیاز به یک طول عمر صریح داشت. در آن زمان، امضای تابع به این صورت نوشته میشد:
fn first_word<'a>(s: &'a str) -> &'a str {
پس از نوشتن مقدار زیادی کد Rust، تیم Rust متوجه شد که برنامهنویسان Rust در موقعیتهای خاصی حاشیهنویسیهای طول عمر یکسانی را بارها و بارها وارد میکردند. این موقعیتها قابل پیشبینی بودند و از چند الگوی تعیینکننده پیروی میکردند. توسعهدهندگان این الگوها را در کد کامپایلر برنامهریزی کردند تا بررسیکننده قرض بتواند طول عمرها را در این موقعیتها استنتاج کند و نیازی به حاشیهنویسی صریح نباشد.
این بخش از تاریخ Rust مرتبط است زیرا ممکن است الگوهای تعیینکننده بیشتری ظاهر شوند و به کامپایلر اضافه شوند. در آینده، حتی حاشیهنویسیهای طول عمر کمتری ممکن است لازم باشد.
الگوهایی که در تحلیل مراجع Rust برنامهریزی شدهاند قوانین حذف طول عمر (lifetime elision rules) نامیده میشوند. اینها قوانینی نیستند که برنامهنویسان باید رعایت کنند؛ بلکه مجموعهای از موارد خاص هستند که کامپایلر آنها را در نظر میگیرد و اگر کد شما با این موارد مطابقت داشته باشد، نیازی به نوشتن طول عمرها به صورت صریح نخواهید داشت.
قوانین حذف طول عمر استنتاج کامل ارائه نمیدهند. اگر پس از اعمال قوانین توسط Rust همچنان ابهامی در مورد طول عمر مراجع وجود داشته باشد، کامپایلر حدس نمیزند که طول عمر باقیمانده چه باید باشد. به جای حدس زدن، کامپایلر به شما خطایی میدهد که میتوانید با افزودن حاشیهنویسیهای طول عمر آن را برطرف کنید.
طول عمرهای روی پارامترهای تابع یا متد طول عمر ورودی (input lifetimes) نامیده میشوند، و طول عمرهای روی مقادیر بازگشتی طول عمر خروجی (output lifetimes) نامیده میشوند.
کامپایلر از سه قانون برای تشخیص طول عمر مراجع استفاده میکند وقتی که حاشیهنویسیهای صریح وجود ندارند. قانون اول برای طول عمرهای ورودی اعمال میشود، و قانون دوم و سوم برای طول عمرهای خروجی. اگر کامپایلر به انتهای این سه قانون برسد و هنوز مراجع وجود داشته باشند که نتواند طول عمرهای آنها را تشخیص دهد، کامپایلر با یک خطا متوقف میشود. این قوانین به تعاریف fn
و همچنین بلوکهای impl
اعمال میشوند.
- قانون اول: کامپایلر یک پارامتر طول عمر به هر پارامتر که یک مرجع است اختصاص میدهد. به عبارت دیگر، یک تابع با یک پارامتر یک پارامتر طول عمر میگیرد:
fn foo<'a>(x: &'a i32)
؛ یک تابع با دو پارامتر دو پارامتر طول عمر جداگانه میگیرد:fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
؛ و به همین ترتیب. - قانون دوم: اگر دقیقاً یک پارامتر طول عمر ورودی وجود داشته باشد، آن طول عمر به تمام پارامترهای طول عمر خروجی اختصاص داده میشود:
fn foo<'a>(x: &'a i32) -> &'a i32
. - قانون سوم: اگر چندین پارامتر طول عمر ورودی وجود داشته باشد، اما یکی از آنها
&self
یا&mut self
باشد زیرا این یک متد است، طول عمرself
به تمام پارامترهای طول عمر خروجی اختصاص داده میشود. این قانون سوم خواندن و نوشتن متدها را بسیار آسانتر میکند زیرا نمادهای کمتری لازم است.
بیایید وانمود کنیم که ما کامپایلر هستیم. این قوانین را برای تشخیص طول عمر مراجع در امضای تابع first_word
در لیست ۱۰-۲۵ اعمال میکنیم. امضا بدون هیچ طول عمری که با مراجع مرتبط باشد شروع میشود:
fn first_word(s: &str) -> &str {
سپس کامپایلر قانون اول را اعمال میکند که مشخص میکند هر پارامتر طول عمر خاص خود را دریافت میکند. ما آن را طبق معمول 'a
مینامیم، بنابراین امضا اکنون به این صورت است:
fn first_word<'a>(s: &'a str) -> &str {
قانون دوم اعمال میشود زیرا دقیقاً یک طول عمر ورودی وجود دارد. قانون دوم مشخص میکند که طول عمر یک پارامتر ورودی به طول عمر خروجی اختصاص داده میشود، بنابراین امضا اکنون به این صورت است:
fn first_word<'a>(s: &'a str) -> &'a str {
حالا تمام مراجع در این امضای تابع طول عمر دارند و کامپایلر میتواند تحلیل خود را بدون نیاز به برنامهنویس برای حاشیهنویسی طول عمرها در این امضای تابع ادامه دهد.
بیایید به یک مثال دیگر نگاه کنیم، این بار با استفاده از تابع longest
که در ابتدا هیچ پارامتر طول عمری نداشت، همانطور که در لیست ۱۰-۲۰ کار خود را با آن شروع کردیم:
fn longest(x: &str, y: &str) -> &str {
بیایید قانون اول را اعمال کنیم: هر پارامتر طول عمر خاص خود را دریافت میکند. این بار دو پارامتر داریم، بنابراین دو طول عمر داریم:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
میبینید که قانون دوم اعمال نمیشود زیرا بیش از یک طول عمر ورودی وجود دارد. قانون سوم نیز اعمال نمیشود زیرا longest
یک تابع است و نه یک متد، بنابراین هیچ یک از پارامترها self
نیستند. پس از عبور از تمام سه قانون، هنوز طول عمر نوع بازگشتی را تعیین نکردهایم. به همین دلیل است که هنگام تلاش برای کامپایل کد در لیست ۱۰-۲۰ خطا گرفتیم: کامپایلر قوانین حذف طول عمر را مرور کرد اما همچنان نتوانست تمام طول عمرهای مراجع در امضا را تعیین کند.
از آنجا که قانون سوم واقعاً فقط در امضاهای متد اعمال میشود، به بررسی طول عمرها در آن زمینه میپردازیم تا ببینیم چرا قانون سوم باعث میشود که اغلب نیازی به حاشیهنویسی طول عمر در امضاهای متد نداشته باشیم.
حاشیهنویسی طول عمر در تعریف متدها
وقتی متدهایی را روی یک ساختار با طول عمرها پیادهسازی میکنیم، از همان نحوی که برای پارامترهای نوع جنریک در لیست ۱۰-۱۱ استفاده شده است، بهره میبریم. جایی که پارامترهای طول عمر را اعلام و استفاده میکنیم به این بستگی دارد که آیا آنها به فیلدهای ساختار یا به پارامترها و مقادیر بازگشتی متد مربوط هستند یا خیر.
نامهای طول عمر برای فیلدهای ساختار همیشه باید بعد از کلمه کلیدی impl
اعلام شوند و سپس بعد از نام ساختار استفاده شوند، زیرا این طول عمرها بخشی از نوع ساختار هستند.
در امضاهای متد در داخل بلوک impl
، مراجع ممکن است به طول عمر مراجع در فیلدهای ساختار مرتبط باشند، یا ممکن است مستقل باشند. علاوه بر این، قوانین حذف طول عمر اغلب باعث میشوند که حاشیهنویسی طول عمر در امضاهای متد ضروری نباشد. بیایید به چند مثال با استفاده از ساختار ImportantExcerpt
که در لیست ۱۰-۲۴ تعریف کردیم، نگاه کنیم.
ابتدا از متدی به نام level
استفاده میکنیم که تنها پارامتر آن مرجعی به self
است و مقدار بازگشتی آن یک i32
است که به چیزی اشاره نمیکند:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {announcement}"); self.part } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
اعلام پارامتر طول عمر بعد از impl
و استفاده از آن بعد از نام نوع الزامی است، اما ما نیازی به حاشیهنویسی طول عمر مرجع به self
نداریم زیرا قانون اول حذف اعمال میشود.
در اینجا مثالی است که قانون سوم حذف طول عمر اعمال میشود:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {announcement}"); self.part } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
دو طول عمر ورودی وجود دارد، بنابراین Rust قانون اول حذف طول عمر را اعمال میکند و طول عمرهای جداگانهای به &self
و announcement
میدهد. سپس، چون یکی از پارامترها &self
است، نوع بازگشتی طول عمر &self
را دریافت میکند، و تمام طول عمرها در نظر گرفته شدهاند.
طول عمر استاتیک
یک طول عمر خاص که باید درباره آن صحبت کنیم 'static
است، که نشان میدهد مرجع مورد نظر میتواند برای کل مدت اجرای برنامه زنده بماند. تمام رشتههای لیتری دارای طول عمر 'static
هستند، که میتوانیم آن را به این صورت حاشیهنویسی کنیم:
#![allow(unused)] fn main() { let s: &'static str = "I have a static lifetime."; }
متن این رشته مستقیماً در باینری برنامه ذخیره میشود، که همیشه در دسترس است. بنابراین، طول عمر تمام رشتههای لیتری 'static
است.
ممکن است در پیامهای خطا پیشنهادهایی برای استفاده از طول عمر 'static
ببینید. اما قبل از مشخص کردن طول عمر 'static
برای یک مرجع، فکر کنید که آیا مرجعی که دارید واقعاً برای کل مدت اجرای برنامه زنده است یا خیر، و آیا میخواهید چنین باشد. بیشتر اوقات، یک پیام خطا که طول عمر 'static
را پیشنهاد میدهد نتیجه تلاش برای ایجاد یک مرجع آویزان یا ناسازگاری طول عمرهای موجود است. در چنین مواردی، راه حل این است که این مشکلات را برطرف کنید، نه اینکه طول عمر 'static
را مشخص کنید.
پارامترهای نوع جنریک، محدودیت ویژگی، و طول عمرها با هم
بیایید به طور مختصر به نحو مشخص کردن پارامترهای نوع جنریک، محدودیت ویژگی، و طول عمرها در یک تابع نگاه کنیم!
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest_with_an_announcement( string1.as_str(), string2, "Today is someone's birthday!", ); println!("The longest string is {result}"); } use std::fmt::Display; fn longest_with_an_announcement<'a, T>( x: &'a str, y: &'a str, ann: T, ) -> &'a str where T: Display, { println!("Announcement! {ann}"); if x.len() > y.len() { x } else { y } }
این تابع longest
از لیست ۱۰-۲۱ است که طولانیترین قطعه رشته را بازمیگرداند. اما اکنون یک پارامتر اضافی به نام ann
دارد که از نوع جنریک T
است، که میتواند با هر نوعی که ویژگی Display
را پیادهسازی میکند، پر شود، همانطور که توسط بند where
مشخص شده است. این پارامتر اضافی با استفاده از {}
چاپ خواهد شد، به همین دلیل محدودیت ویژگی Display
ضروری است. از آنجا که طول عمرها نوعی جنریک هستند، اعلام طول عمر 'a
و پارامتر نوع جنریک T
در همان لیست داخل پرانتزهای زاویهای بعد از نام تابع قرار میگیرند.
خلاصه
در این فصل مطالب زیادی را پوشش دادیم! حالا که با پارامترهای نوع جنریک، ویژگیها و محدودیتهای ویژگی، و پارامترهای طول عمر جنریک آشنا شدید، آماده هستید تا کدی بدون تکرار بنویسید که در بسیاری از موقعیتهای مختلف کار کند. پارامترهای نوع جنریک به شما اجازه میدهند که کد را روی انواع مختلف اعمال کنید. ویژگیها و محدودیتهای ویژگی اطمینان حاصل میکنند که حتی با اینکه نوعها جنریک هستند، رفتار مورد نیاز کد را خواهند داشت. شما یاد گرفتید چگونه از حاشیهنویسی طول عمر استفاده کنید تا اطمینان حاصل شود که این کد انعطافپذیر هیچ مرجع آویزانی نخواهد داشت. و تمام این تحلیلها در زمان کامپایل انجام میشود، که بر عملکرد زمان اجرا تأثیری ندارد!
باور کنید یا نه، مطالب بیشتری برای یادگیری در مورد موضوعاتی که در این فصل بحث شد وجود دارد: فصل ۱۸ به اشیاء ویژگی (trait objects) میپردازد، که راه دیگری برای استفاده از ویژگیها است. همچنین سناریوهای پیچیدهتری وجود دارد که شامل حاشیهنویسی طول عمر هستند و فقط در سناریوهای بسیار پیشرفته به آنها نیاز خواهید داشت. برای این موارد، باید مرجع Rust را مطالعه کنید. اما بعد از این، یاد خواهید گرفت که چگونه تستهایی در Rust بنویسید تا مطمئن شوید کد شما همانطور که باید کار میکند.