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