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