null
اگر تا به حال از زبان برنامهنویسی دیگری استفاده کرده باشید (C/C++, Python, Java, Ruby, Lisp و غیره)، احتمالاً قبلاً با null یا nil روبهرو شدهاید.
استفاده از null یا nil روشی است که این زبانها با آن نشان میدهند یک متغیر خاص هیچ مقداری ندارد.
اما همین باعث میشود که استفادهی تصادفی از متغیری که به null اشاره میکند، اشتباهی آسان (و پرتکرار) باشد.
همانطور که میتوانید تصور کنید، تلاش برای فراخوانی تابعی که وجود ندارد، یا دسترسی به مقداری که وجود ندارد، میتواند به انواع و اقسام bug و کرش منجر شود.
سازندهی null تا آنجا پیش رفت که آن را «اشتباه میلیارد دلاری». خود نامید.
Option
برای پرهیز از این مشکلات، Rust از ارجاعهای null استفاده نمیکند.
با این حال، همچنان به روشی امن نیاز دارد تا نشان دهد یک متغیر خاص هیچ مقداری ندارد.
اینجاست که Option وارد میشود.
به جای اینکه متغیری داشته باشید که مقداری ندارد، متغیرهای Rust میتوانند از enum Option استفاده کنند.
این enum دو گونه دارد: None، معادل null در Rust؛ و Some(T) که در آن T مقداری از هر نوعی است.
به این شکل است:
enum Option<T> {
None,
Some(T),
}
میتوانید Option را لایهای از امنیت میان خودتان و مشکلاتی در نظر بگیرید که ارجاعهای null میتوانند به بار بیاورند، و در همان حال کاربرد مفهومی آنها را حفظ کنید.
Option
تنظیم یک متغیر روی None کاملاً سرراست است:
let nothing: Option<u32> = None; // Variable nothing is set to None
اما اگر میخواهید نوع Option مقداری را حمل کند، نمیتوانید این مقدار را مستقیماً تخصیص دهید.
یک متغیر از نوع Option و مثلاً یک متغیر از نوع i32 معادل هم نیستند.
باید از Some استفاده کنید:
let wrong_way: Option<i32> = -4; // This will not work
let right_way: Option<i32> = Some(-4); // This will work
let another_right_way = Some(-4); // Compiler infers that this is Option<i32>
به همین دلیل هم قطعهی زیر کار نمیکند:
let number = 47;
let option_number = Some(15);
let compile_error = number + option_number; // Cannot add an i32 and an Option<i32> - they are of different types
اگر میخواهید مقداری را که درون Some نگه داشته شده بهدست آورید، اول باید بررسی کنید که وجود دارد:
let mut some_words = Some("choose something to say"); // some_words set to something
match some_words {
Some(str) => println!("Here, we will {}", str),
None => println!("I've got nothing to say"),
} // Prints "Here, we will choose something to say"
some_words = None; // some_words now set to None
// exactly the same match block as above
match some_words {
Some(str) => println!("Here, we will {}", str),
None => println!("I've got nothing to say"),
} // Prints "I've got nothing to say"
علاوه بر match، ابزارهای دیگری هم در Rust برای بررسی و دسترسی به مقادیر درون Option وجود دارد، اما match باید تا حالا برایتان آشنا شده باشد.
بهعلاوه، این را نمایشی از دلیل استفادهی Rust از Option بهجای ارجاع null در نظر بگیرید.
نکته این است که باید حتماً بررسی کنید که متغیر Option برابر با Some است (که در آن صورت میتوانید پیش بروید و مقدار درون آن را استخراج و استفاده کنید) یا None.
در غیر این صورت، برنامهتان کامپایل نمیشود؛ کامپایلر شما را از null در امان نگه میدارد.
شما در حال پیادهسازی یک بازی نقشآفرینی هستید. شخصیت بازیکن با ساختار زیر نمایش داده میشود:
pub struct Player {
health: u32,
mana: Option<u32>,
level: u32,
}
بازیکنها در این بازی باید به سطح ۱۰ برسند تا «مخزن مانا» برایشان باز شود و بتوانند پرتاب طلسم را شروع کنند. پیش از آن، مانای Player برابر None است.
شما روی دو بخش از قابلیتهای این بازی کار میکنید: سازوکار احیا و سازوکار پرتاب طلسم.
متد revive باید بررسی کند که Player واقعاً مرده است (جانش به ۰ رسیده است) و اگر چنین بود، باید نمونهای تازه از Player با ۱۰۰ جان برگرداند.
اگر سطح Player برابر ۱۰ یا بیشتر باشد، باید با ۱۰۰ مانا احیا شود.
اگر سطح Player کمتر از ۱۰ باشد، مانای آن باید None باشد. متد revive باید سطح Player را حفظ کند.
let dead_player = Player { health: 0, mana: None, level: 2 };
dead_player.revive()
// Returns Player { health: 100, mana: None, level: 2 }
اگر متد revive روی یک Player فراخوانی شود که جانش ۱ یا بیشتر است، متد باید None را برگرداند.
let alive_player = Player { health: 1, mana: Some(15), level: 11 };
alive_player.revive()
// Returns None
متد cast_spell یک ارجاع تغییرپذیر به Player و همچنین پارامتر mana_cost را میگیرد که نشان میدهد طلسم چقدر مانا هزینه دارد. این متد مقدار آسیبی را برمیگرداند که طلسم پرتابشده وارد میکند؛ اگر طلسم با موفقیت پرتاب شود، این مقدار همیشه دو برابر هزینهی مانای طلسم خواهد بود.
اگر بازیکن به مخزن مانا دسترسی نداشته باشد، تلاش برای پرتاب طلسم باید جانش را به اندازهی هزینهی مانای طلسم کاهش دهد. مقدار آسیبی که برگردانده میشود باید ۰ باشد.
let not_a_wizard_yet = Player { health: 79, mana: None, level: 9 };
assert_eq!(not_a_wizard_yet.cast_spell(5), 0)
assert_eq!(not_a_wizard_yet.health, 74);
assert_eq!(not_a_wizard_yet.mana, None);
اگر بازیکن مخزن مانا داشته باشد اما مانای کافی نداشته باشد، متد نباید اثری روی مخزن بگذارد و بهجای آن باید ۰ برگرداند.
let low_mana_wizard = Player { health: 93, mana: Some(3), level: 12 };
assert_eq!(low_mana_wizard.cast_spell(10), 0);
assert_eq!(low_mana_wizard.health, 93);
assert_eq!(low_mana_wizard.mana, Some(3));
در غیر این صورت، mana_cost باید از مخزن مانای Player کسر شود و مقدار مناسب آسیب برگردانده شود.
let wizard = Player { health: 123, mana: Some(30), level: 18 };
assert_eq!(wizard.cast_spell(10), 20);
assert_eq!(wizard.health, 123);
assert_eq!(wizard.mana, Some(20));
خوش بگذرانید!