null
إذا كنت قد استخدمت من قبل لغة برمجة أخرى (C/C++، Python، Java، Ruby، Lisp، وما إلى ذلك)، فمن المحتمل أنك صادفت null أو nil سابقًا.
استخدام null أو nil هو الطريقة التي تشير بها هذه اللغات إلى أن متغيرًا معيّنًا لا قيمة له.
لكن هذا يجعل استخدام متغير null عن غير قصد خطأً سهل الوقوع (وشائعًا).
وكما قد تتخيل، فإن محاولة استدعاء دالة غير موجودة، أو الوصول إلى قيمة غير موجودة، قد تؤدي إلى شتى أنواع الـbugs والانهيارات.
وقد ذهب مبتكر null إلى حد وصفه بأنه 'خطأ المليار دولار'.
Option
لتجنّب هذه المشكلات، يمكن لمتغيرات Cairo استخدام التعداد Option لتمثيل غياب القيمة بأمان.
يتكوّن هذا التعداد من صيغتين: None، وهي مكافئ null في Cairo؛ وSome(T)، حيث T قيمة من أي نوع.
وهو يبدو هكذا:
enum Option<T> {
None,
Some(T),
}
يمكنك أن تنظر إلى Option كطبقة أمان بينك وبين المشكلات التي قد تسبّبها مراجع null، مع الاحتفاظ بفائدتها المفاهيمية في الوقت نفسه.
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 أدوات أخرى غير match متاحة للتحقق من القيم الموجودة داخل Option والوصول إليها، لكن match ينبغي أن يكون مألوفًا لك الآن.
اعتبر هذا توضيحًا لسبب استخدام Cairo لـ Option بدلًا من مرجع null.
والنقطة الجوهرية هي أن عليك أن تتحقق بالضرورة مما إذا كان متغير Option يحمل Some (وفي هذه الحالة يمكنك المضي قدمًا واستخراج القيمة الموجودة داخله واستخدامها)، أو None.
وإلا فلن يُترجم برنامجك؛ فالمترجم يحميك من null.
أنت تعمل على تنفيذ لعبة تقمّص أدوار. تُمثَّل شخصية اللاعب بما يلي:
pub struct Player {
health: u32,
mana: Option<u32>,
level: u32,
}
يجب أن يصل اللاعبون في هذه اللعبة إلى المستوى 10 قبل أن يفتحوا مخزون مانا يستخدمونه لإلقاء التعاويذ.
وقبل هذه المرحلة، تكون مانا اللاعب Option::None.
أنت تعمل على جزأين من الوظائف في هذه اللعبة: آلية الإحياء وآلية إلقاء التعاويذ.
يجب أن تتحقق طريقة revive من أن اللاعب ميت فعلًا (أي أن صحته وصلت إلى 0)، وإذا كان كذلك، فيجب أن تُرجع الطريقة نسخة جديدة من اللاعب بصحة 100.
ويجب أن تحافظ طريقة revive على مستوى اللاعب.
إذا كان مستوى اللاعب 10 أو أكثر، فيجب إحياؤه بـ 100 مانا.
وإذا كان مستوى اللاعب أقل من 10، فيجب أن تكون مانا اللاعب 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 على لاعب صحته 1 أو أكثر، فيجب أن تُرجع الطريقة Option::None.
let alive_player = Player { health: 1, mana: Option::Some(15), level: 11 };
alive_player.revive()
// Returns Option::None
تأخذ طريقة cast_spell مرجعًا قابلًا للتغيير إلى اللاعب، إضافة إلى معامل mana_cost يبيّن مقدار المانا التي تكلّفها التعويذة.
وتُرجع مقدار الضرر الذي تُحدثه التعويذة عند إلقائها.
إذا لم يكن للاعب مخزون مانا، فإن محاولة إلقاء التعويذة يجب أن تُنقص صحته بمقدار تكلفة المانا للتعويذة. ويجب أن يكون الضرر المُرجَع 0.
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);
إذا كان للاعب مخزون مانا لكن المانا غير كافية، فيجب ألا تؤثر الطريقة على المخزون. ويجب أن يكون الضرر المُرجَع 0.
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));
استمتع باللعب!
سجّل في Exercism لتتعلّم وتتقن Cairo عبر 25 مفهومًا68 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.