اذهب إلى المحتوى

Naser Dakhel

الأعضاء
  • المساهمات

    51
  • تاريخ الانضمام

  • تاريخ آخر زيارة

كل منشورات العضو Naser Dakhel

  1. تُعد قابلية التغيير الداخلي interior mutability نمط تصميم في رست يسمح لك بتغيير البيانات حتى في حالة وجود مراجع ثابتة immutable تشير لتلك البيانات، ولا تسمح قواعد الاستعارة بهذا الإجراء عادًة. يستخدم النمط شيفرة "unsafe" لتغيير البيانات وذلك داخل هيكل بيانات للتحايل على قواعد رست المعتادة التي تتحكم بقابلية التعديل والاستعارة، إذ تشير الشيفرة غير الآمنة للمصرّف أننا نتحقق من القواعد يدويًا عوضًا عن الاعتماد على المصرّف للتحقق منها لنا، وسنناقش الشيفرة البرمجية غير الآمنة بالتفصيل لاحقًا. يمكننا استخدام الأنواع التي تستخدم نمط قابلية التغيير الداخلي فقط عندما نتأكد أن قواعد الاستعارة تُتّبع في وقت التنفيذ، على الرغم من أن المصرّف لا يمكنه ضمان ذلك. تُغلّف wrap الشيفرة غير الآمنة "unsafe" ضمن واجهة برمجية API آمنة، بحيث يبقى النوع الخارجي ثابتًا. لنكتشف هذا المفهوم من خلال النظر إلى نوع <RefCell<T الذي يتبع نمط التغيير الداخلي. فرض قواعد الاستعارة عند وقت التنفيذ باستخدام <RefCell<T يمثل النمط <RefCell<T بعكس <Rc<T ملكيةً مفردةً للبيانات التي يحملها. إذًا ما الذي يجعل <RefCell<T مختلفًا عن نمط مثل <Box<T؟ تذكر قواعد الاستعارة التي تعلمناها سابقًا في المقال المراجع References والاستعارة Borrowing والشرائح Slices: يمكنك بأي وقت أن تمتلك إما مرجعًا متغيرًا واحدًا أو أي عدد من المراجع الثابتة ولكن ليس كليهما. يجب على المراجع أن تكون صالحة دومًا. تُفرض ثوابت قواعد الاستعارة borrowing rules’ invariants عند وقت التصريف مع المراجع و <Box<T، وتُفرض هذه الثوابت مع <RefCell<T في وقت التنفيذ. نحصل على خطأ تصريفي مع المراجع إذا خرقنا هذه القواعد، إلا أنه في حالة <RefCell<T سيُصاب البرنامج بالهلع ويتوقف إذا خرقت القواعد ذاتها. تتمثل إيجابيات التحقق من قواعد الاستعارة في وقت التصريف في أنه ستُكتشف الأخطاء مبكرًا في عملية التطوير ولا يوجد أي تأثير على الأداء وقت التنفيذ لأن جميع التحليلات قد اكتملت مسبقًا، لهذه الأسباب يُعد التحقق من قواعد الاستعارة في وقت التصريف هو الخيار الأفضل في معظم الحالات وهذا هو السبب في كونه الخيار الافتراضي في رست. تتمثل إيجابيات ميزة التحقق من قواعد الاستعارة في وقت التنفيذ بدلًا من ذلك، بأنه يُسمح بعد ذلك بسيناريوهات معينة خاصة بالذاكرة الآمنة إذ يجري منعها عادةً من خلال عمليات التحقق في وقت التصريف. يُعد التحليل الساكن static analysis صارمًا كما هو الحال في مصرّف رست. من المستحيل اكتشاف بعض خصائص الشيفرة البرمجية من خلال تحليلها والمثال الأكثر شهرة هو مشكلة التوقف halting problem التي تتجاوز نطاق موضوعنا هنا إلا أنه موضوع يستحق البحث عنه. قد ترفض رست البرنامج الصحيح إذا لم يكن مصرّف رست متأكدًا من أن الشيفرة البرمجية تتوافق مع قواعد الملكية، وذلك بفضل وجود عملية التحليل، وتوضّح هذه الحالة صرامة العملية، وإذا قبلت رست برنامجًا خاطئًا فلن يتمكن المستخدمون من الوثوق بالضمانات التي تقدمها رست، وعلى الجانب الآخر إذا رفضت رست برنامجًا صحيحًا فسيتسبب ذلك بإزعاج للمبرمج، ولكن لا يمكن أن يحدث أي شيء كارثي. يُعد النوع <RefCell<T مفيدًا عندما تكون متأكدًا من أن الشيفرة الخاصة بك تتبع قواعد الاستعارة ولكن المصرّف غير قادر على فهم وضمان ذلك. وكما في <Rc<T تُستخدم <RefCell<T فقط في السيناريوهات ذات الخيوط المفردة single-threaded وستعطيك خطأً وقت التنفيذ إذا حاولت استخدامه في سياق متعدد الخيوط multithreaded. سنتحدث عن كيفية الحصول على وظيفة <RefCell<T في برنامج متعدد الخيوط لاحقًا. فيما يلي ملخص لأسباب اختيار<Box<T أو <Rc<T أو <RefCell<T: يمكّن النوع <Rc<T وجود عدّة مالكين لنفس البيانات بينما يوجد للنوعين <RefCell<T و <Box<T مالك وحيد. يسمح النوع <Box<T بوجود استعارات متغيّرة أو ثابتة يُتحقَّق منها وقت التصريف، بينما يسمح النوع <RefCell<T باستعارات متغيّرة أو ثابتة وقت التنفيذ. بما أن النوع <RefCell<T يسمح باستعارات متغيّرة مُتحقق منها في وقت التنفيذ، يمكنك تغيير القيمة داخل <RefCell<T حتى عندما يكون النوع <RefCell<T ثابتًا. تغيير القيمة داخل قيمة ثابتة هو نمط التغيير الداخلي. لنلقي نظرةً على موقف يكون فيه نمط التغيير الداخلي مفيدًا ونفحص كيف يكون ذلك ممكنًا. التغيير الداخلي: استعارة متغيرة لقيمة ثابتة عندما يكون لديك قيمة ثابتة فإنك لا تستطيع استعارتها على أنها متغيرة وفقًا لقواعد الاستعارة، على سبيل المثال، لن تُصرَّف الشيفرة البرمجية التالية: fn main() { let x = 5; let y = &mut x; } إذا حاولت تصريف الشيفرة السابقة فسيظهر لك الخطأ التالي: $ cargo run Compiling borrowing v0.1.0 (file:///projects/borrowing) error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable --> src/main.rs:3:13 | 2 | let x = 5; | - help: consider changing this to be mutable: `mut x` 3 | let y = &mut x; | ^^^^^^ cannot borrow as mutable For more information about this error, try `rustc --explain E0596`. error: could not compile `borrowing` due to previous error على الرغم من ذلك هناك حالات قد يكون من المفيد فيها لقيمة ما أن تغير نفسها باستخدام توابعها methods الخاصة مع جعلها ثابتة بالنسبة للشيفرات الأخرى، بحيث لا تستطيع الشيفرة البرمجية خارج توابع القيمة تغيير القيمة. يُعد استخدام <RefCell<T إحدى الطرق للحصول على التغيير الداخلي إلا أن النوع <RefCell<T لا يتغلب على كامل قواعد الاستعارة، إذ يسمح مدقق الاستعارة في المصرّف بالتغيير الداخلي ويجري التحقق من قواعد الاستعارة في وقت التنفيذ بدلًا من ذلك. إذا خُرقت القواعد فسوف تحصل على حالة هلع panic!‎ بدلًا من خطأ تصريفي. لنعمل من خلال مثال عملي يمكّننا من استخدام <RefCell<T لتغيير قيمة ثابتة ومعرفة لماذا يُعد ذلك مفيدًا. حالة استخدام للتغيير الداخلي: الكائنات الزائفة Mock Objects يستخدم المبرمج نوعًا بدلًا من آخر في بعض الأحيان أثناء الاختبار من أجل مراقبة سلوك معين والتأكد من تنفيذه بصورةٍ صحيحة، ويُسمى هذا النوع من وضع القيمة المؤقتة placeholder بالاختبار المزدوج test double. فكِّر بهذا الأمر بسياق "دوبلير stunt double" في صناعة الأفلام، إذ يتدخل الشخص ويحل محل الممثل لإنجاز مشهد معين صعب. يُستخدم الاختبار المزدوج لأنواع أخرى عندما نجري الاختبارات. الكائنات الزائفة هي أنواع محددة من الاختبار المزدوج التي تسجل ما يحدث أثناء الاختبار حتى تتمكن من أن تتأكد أن الإجراءات الصحيحة قد أُنجزت. لا تحتوي رست على كائنات بنفس معنى الكائنات في لغات البرمجة الأخرى، ولا تحتوي رست على قدرة التعامل مع الكائنات الزائفة مُضمَّنة في المكتبة القياسية كما تفعل بعض اللغات الأخرى، ومع ذلك يمكنك بالتأكيد إنشاء هيكل يخدم الهدف من الكائنات الزائفة. إليك السيناريو الذي سنختبره: سننشئ مكتبةً تتعقب قيمةً مقابل قيمة قصوى وترسل رسائل بناءً على مدى قرب القيمة الحالية من القيمة القصوى، بحيث يمكن استخدام هذه المكتبة لتتبع حصة المستخدم لعدد استدعاءات الواجهة البرمجية المسموح بإجرائها على سبيل المثال. ستوفر مكتبتنا فقط وظيفة تتبع مدى قرب الحد الأعظمي للقيمة وما يجب أن تكون عليه الرسائل في أي وقت، ومن المتوقع أن توفّر التطبيقات التي تستخدم مكتبتنا آلية إرسال الرسائل، يمكن للتطبيق وضع رسالة في التطبيق، أو إرسال بريد إلكتروني، أو إرسال رسالة نصية، أو شيء آخر، إذ لا تحتاج المكتبة إلى معرفة هذا التفصيل، وكل ما تحتاجه هو شيء ينفِّذ سمة سنوفّرها تدعى Messenger. تظهر الشيفرة 20 الشيفرة البرمجية الخاصة بالمكتبة: اسم الملف: src/lib.rs pub trait Messenger { fn send(&self, msg: &str); } pub struct LimitTracker<'a, T: Messenger> { messenger: &'a T, value: usize, max: usize, } impl<'a, T> LimitTracker<'a, T> where T: Messenger, { pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> { LimitTracker { messenger, value: 0, max, } } pub fn set_value(&mut self, value: usize) { self.value = value; let percentage_of_max = self.value as f64 / self.max as f64; if percentage_of_max >= 1.0 { self.messenger.send("Error: You are over your quota!"); } else if percentage_of_max >= 0.9 { self.messenger .send("Urgent warning: You've used up over 90% of your quota!"); } else if percentage_of_max >= 0.75 { self.messenger .send("Warning: You've used up over 75% of your quota!"); } } } الشيفرة 20: مكتبة لتتبع مدى قرب قيمة من قيمة العظمى وإرسال تحذير عندما تصل القيمة لمقدار معيّن أحد الأجزاء المهمة من هذه الشيفرة هو أن سمة Messenger لها تابعٌ واحدٌ يدعى send يقبل مرجعًا ثابتًا يشير إلى self بالإضافة إلى نص الرسالة، وهذه السمة هي الواجهة التي يحتاج الكائن الزائف الخاص بنا إلى تنفيذها بحيث يمكن استخدام الكائن الزائف بنفس الطريقة التي يُستخدم بها الكائن الحقيقي؛ والجزء المهم الآخر هو أننا نريد اختبار سلوك التابع set_value على LimitTracker. يمكننا تغيير ما نمرّره لمعامل value، إلا أن set_value لا تُعيد لنا أي شيء لإجراء تأكيدات assertions عليه؛ إذ نريد أن نكون قادرين على القول أنه على المُرسل أن يرسل رسالة معيّنة إذا أنشأنا LimitTracker بشيء يطبّق السمة Messenger وقيمة معينة لـ max، عندما نمرر أرقامًا مختلفة مثل قيمة value. نحتاج هنا إلى كائن زائف لتتبُّع الرسائل التي يُطلب منه إرسالها عندما نستدعي send، وذلك بدلًا من إرسال بريد إلكتروني أو رسالة نصية. يمكننا إنشاء نسخة جديدة من كائن زائف وإنشاء LimitTracker يستخدم الكائن الزائف واستدعاء التابع set_value على LimitTracker ثم التحقق من أن الكائن الزائف يحتوي على الرسائل التي نتوقعها. تُظهر الشيفرة 21 محاولة تطبيق كائن زائف لفعل ذلك فقط إلا أن مدقق الاستعارة لن يسمح بذلك. اسم الملف: src/lib.rs #[cfg(test)] mod tests { use super::*; struct MockMessenger { sent_messages: Vec<String>, } impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_messages: vec![], } } } impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_messages.push(String::from(message)); } } #[test] fn it_sends_an_over_75_percent_warning_message() { let mock_messenger = MockMessenger::new(); let mut limit_tracker = LimitTracker::new(&mock_messenger, 100); limit_tracker.set_value(80); assert_eq!(mock_messenger.sent_messages.len(), 1); } } الشيفرة 21: محاولة لتطبيق MockMessenger غير المسموح به بواسطة مدقق الاستعارة تعرِّف شيفرة الاختبار السابقة هيكل MockMessenger يحتوي على حقل sent_messages مع قيم Vec من نوع سلسلة نصية String لتتبع الرسائل المطلوب إرسالها؛ كما نعرّف أيضًا دالة مرتبطة associated function بالاسم new لتسهيل إنشاء قيم MockMessenger الجديدة التي تبدأ بلائحة فارغة من الرسائل، ثم نطبّق السمة Messenger على MockMessenger حتى نستطيع إعطاء ملكية MockMessenger لنسخة LimitTracker. نأخذ الرسالة الممرّرة مثل معامل في تعريف التابع send ونخزّنها في قائمة MockMessenger الخاصة بالحقل sent_messages. نختبر في الاختبار ما يحدث عندما يُطلب من LimitTracker تعيين value لشيء يزيد عن 75 بالمائة من max، إذ نُنشئ أولًا نسخةً جديدةً من MockMessenger تبدأ بقائمة فارغة من الرسائل، ثم نُنشئ LimitTracker جديد ونمرّر له مرجعًا يشير إلى MockMessenger الجديد وقيمة max هي 100. نستدعي التابع set_value على LimitTracker بقيمة 80 والتي تزيد عن 75 بالمئة من 100 ثم نتأكد أن قائمة الرسائل التي يتتبعها MockMessenger يجب أن تحتوي الآن على رسالة واحدة. ومع ذلك هناك مشكلة واحدة في هذا الاختبار كما هو موضح هنا: $ cargo test Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker) error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference --> src/lib.rs:58:13 | 2 | fn send(&self, msg: &str); | ----- help: consider changing that to be a mutable reference: `&mut self` ... 58 | self.sent_messages.push(String::from(message)); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable For more information about this error, try `rustc --explain E0596`. error: could not compile `limit-tracker` due to previous error warning: build failed, waiting for other jobs to finish... لا يمكننا تعديل MockMessenger لتتبع الرسائل لأن التابع send يأخذ مرجعًا ثابتًا يشير إلى self، كما لا يمكننا أيضًا تنفيذ الاقتراح في نص الخطأ الذي يدلنا على استخدام ‎&mut self بدلًا من ذلك لأن بصمة التابع send لن تتطابق مع بصمة تعريف السمة Messenger (ننصحك بقراءة رسالة الخطأ بنفسك). هذه هي الحالة التي يُمكن أن يساعد فيها نمط التغيير الداخلي، إذ سنخزن sent_messages داخل <RefCell<T، ومن ثم سيتمكن التابع send من تعديل sent_messages لتخزين الرسائل التي رأيناها، وتظهر الشيفرة 22 كيف يبدو التغيير: اسم الملف: src/lib.rs #[cfg(test)] mod tests { use super::*; use std::cell::RefCell; struct MockMessenger { sent_messages: RefCell<Vec<String>>, } impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_messages: RefCell::new(vec![]), } } } impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_messages.borrow_mut().push(String::from(message)); } } #[test] fn it_sends_an_over_75_percent_warning_message() { // --snip-- assert_eq!(mock_messenger.sent_messages.borrow().len(), 1); } } الشيفرة 22: استخدام <RefCell<T لتغيير قيمة داخلية بينما تكون القيمة الخارجية ثابتة أصبح حقل sent_messages الآن من النوع <<RefCell<Vec<String بدلًا من <Vec<String. نُنشئ في الدالة new نسخةً جديدةً من <<RefCell<Vec<String حول الشعاع الفارغ. لا يزال المعامل الأول يمثّل استعارة self ثابتة تتطابق مع تعريف السمة، ولتطبيق التابع send نستدعي borrow_mut على <<RefCell<Vec<String في self.sent_messages للحصول على مرجع متغيّر للقيمة داخل <<RefCell<Vec<String التي تمثّل الشعاع، ومن ثم يمكننا أن نستدعي push على المرجع المتغيّر الذي يشير إلى الشعاع لتتبع الرسائل المرسلة أثناء الاختبار. التغيير الأخير الذي يتعين علينا إجراؤه هو ضمن التأكيد assertion لمعرفة عدد العناصر الموجودة في الشعاع الداخلي، ولتحقيق ذلك نستدعي borrow على <<RefCell<Vec<String للحصول على مرجع ثابت يشير إلى الشعاع. الآن بعد أن رأيت كيفية استخدام المؤشر <RefCell<T لنتعمق في كيفية عمله. تتبع عمليات الاستعارة وقت التنفيذ عن طريق <RefCell<T نستخدم عند إنشاء مراجع متغيّرة وثابتة الصيغة & و & mut على التوالي، بينما نستخدم في <RefCell<T التابعين borrow و borrow_mut اللذين يعدّان جزءًا من واجهة برمجية آمنة تنتمي إلى <RefCell<T. يُعيد التابع borrow نوع المؤشر الذكي <Ref<T ويُعيد التابع borrow_mut نوع المؤشر الذكي <RefMut<T، وينفّذ كلا النوعين السمة Deref لذلك يمكننا معاملتهما على أنهما مراجع نمطيّة regular references. يتعقّب المؤشر <RefCell<T عدد المؤشرات الذكية النشطة حاليًا من النوعين <Ref<T و <RefMut<T، وفي كل مرة نستدعي فيها التابع borrow يزيد المؤشر <RefCell<T من عدد عمليات الاستعارة النشطة الثابتة، وعندما تخرج قيمة من النوع <Ref<T عن النطاق، ينخفض عدد الاستعارات الثابتة بمقدار واحد. يتيح لنا المؤشر <RefCell<T الحصول على العديد من الاستعارات الثابتة أو استعارة واحدة متغيّرة في أي وقت تمامًا مثل قواعد الاستعارة وقت التصريف. إذا حاولنا انتهاك هذه القواعد، سيهلع تنفيذ <RefCell<T وقت التنفيذ بدلًا من الحصول على خطأ تصريفي كما اعتدنا حصوله مع المراجع. تظهر الشيفرة 23 تعديلًا في تطبيق الدالة send من الشيفرة 22، ونحاول هنا إنشاء استعارتين نشطتين متغيّرتين لنفس النطاق عمدًا لتوضيح أن <RefCell<T سيمنعنا من فعل ذلك وقت التنفيذ. اسم الملف: src/lib.rs impl Messenger for MockMessenger { fn send(&self, message: &str) { let mut one_borrow = self.sent_messages.borrow_mut(); let mut two_borrow = self.sent_messages.borrow_mut(); one_borrow.push(String::from(message)); two_borrow.push(String::from(message)); } } [الشيفرة 23: إنشاء مرجعين متغيّرين في النطاق ذاته لملاحظة هلع <RefCell<T>] نُنشئ متغير اسمه one_borrow للمؤشر الذكي <RefMut<T الذي أُعيد من borrow_mut، ثم نُنشئ استعارةً متغيّرة أخرى بنفس الطريقة في المتغير two_borrow، مما يؤدي إلى إنشاء مرجعين متغيّرين في النطاق ذاته وهو أمر غير مسموح به. عندما ننفذ الاختبارات لمكتبتنا، تُصرَّف الشيفرة البرمجية الموجودة في الشيفرة 23 دون أي أخطاء إلا أن الاختبار سيفشل: $ cargo test Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker) Finished test [unoptimized + debuginfo] target(s) in 0.91s Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde) running 1 test test tests::it_sends_an_over_75_percent_warning_message ... FAILED failures: ---- tests::it_sends_an_over_75_percent_warning_message stdout ---- thread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::it_sends_an_over_75_percent_warning_message test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` لاحظ أن الشيفرة هلعت مع الرسالة already borrowed: BorrowMutError، وهذه هي الطريقة التي يتعامل بها المؤشر <RefCell<T مع انتهاكات قواعد الاستعارة عند وقت التنفيذ. اختيار اكتشاف أخطاء الاستعارة وقت التنفيذ بدلًا من وقت التصريف كما فعلنا هنا يعني أنك من المحتمل أن تجد أخطاءً في الشيفرة الخاصة بك لاحقًا في عملية التطوير، وربما حتى عند نشر الشيفرة البرمجية الخاصة بك ووصولها لمرحلة الإنتاج. قد تتكبّد شيفرتك البرمجية أيضًا خسارةً صغيرةً في الأداء عند وقت التنفيذ وذلك نتيجة لتتبع الاستعارات عند وقت التشغيل بدلًا من وقت التصريف، ومع ذلك فإن استخدام <RefCell<T يجعل من الممكن كتابة كائن زائف يمكنه تعديل نفسه لتتبع الرسائل التي شاهدها أثناء استخدامه في سياق يسمح فقط بالقيم الثابتة. يمكنك استخدام <RefCell<T على الرغم من المقايضات للحصول على وظائف أكثر مما توفره المراجع العادية. وجود عدة مالكين للبيانات المتغيرة عن طريق استخدام كل من <Rc <T و <RefCell<T هناك طريقة شائعة لاستخدام <RefCell<T بالاشتراك مع <Rc<T، تذكر أن <Rc<T يتيح لك وجود عدة مالكين لبعض البيانات إلا أنه يمنحك وصولًا ثابتًا إلى تلك البيانات. إذا كان لديك <Rc<T يحتوي على <RefCell<T فيمكنك الحصول على قيمة يمكن أن يكون لها عدة مالكين ويمكنك تغييرها. على سبيل المثال تذكر مثال قائمة البنية في الشيفرة 18 في المقالة السابقة، إذ استخدمنا <Rc<T للسماح لقوائم متعددة بمشاركة ملكية قائمة أخرى، ونظرًا لأن <Rc<T يحتوي على قيم ثابتة فقط، فلا يمكننا تغيير أي من القيم الموجودة في القائمة بمجرد إنشائها. دعنا نضيف <RefCell<T لاكتساب القدرة على تغيير القيم في القوائم. تُظهر الشيفرة 24 أنه يمكننا تعديل القيم المخزّنة في جميع القوائم وذلك باستخدام <RefCell<T داخل تعريف Cons: اسم الملف: src/main.rs #[derive(Debug)] enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; fn main() { let value = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); *value.borrow_mut() += 10; println!("a after = {:?}", a); println!("b after = {:?}", b); println!("c after = {:?}", c); } الشيفرة 24: استخدام <<Rc<RefCell<i32 لإنشاء List يمكن تغييرها نُنشئ قيمة بحيث تكون نسخةً من النوع <<Rc<RefCell<i32 ونخزنها في متغير باسم value حتى نتمكن من الوصول إليها مباشرةً لاحقًا، ثم نُنشئ List في a مع متغاير Cons يحمل value، نحتاج هنا إلى استنساخ value بحيث يكون لكل من a و value ملكية للقيمة الداخلية 5 بدلًا من نقل الملكية من value إلى a أو استعارة a من value. نغلّف القائمة a داخل <Rc<T عند إنشاء القائمتين a و b بحيث يمكن لكلّ من القائمتين الرجوع إلى a وهو ما فعلناه في الشيفرة 18 سابقًا. نريد إضافة القيمة 10 إلى value بعد إنشاء القوائم في a و b و c، ونحقّق ذلك عن طريق استدعاء borrow_mut على value التي تستخدم ميزة التحصيل التلقائي automatic differencing التي ناقشناها سابقًا في المقال استخدام التوابع methods ضمن الهياكل structs ضمن فقرة بعنوان (أين العامل ‎->‎؟) لتحصيل <Rc<T إلى قيمة <RefCell<T الداخلية. يُعيد التابع borrow_mut المؤشر الذكي <RefMut<T، ومن ثم نستخدم عامل التحصيل عليه ونغير القيمة الداخلية. عندما ننفذ a و b و c، يمكننا أن نرى أن لجميعهم القيمة المعدلة 15 بدلًا من 5: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.63s Running `target/debug/cons-list` a after = Cons(RefCell { value: 15 }, Nil) b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil)) c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil)) يا لهذه الطريقة الجميلة، فقد أصبح لدينا قيمة List ثابتة خارجيًا باستخدام <RefCell<T>، إلا أنه يمكننا استخدام التوابع الموجودة في <RefCell<T التي توفر الوصول إلى قابلية التغيير الداخلية حتى نتمكن من تعديل بياناتنا عندما نحتاج إلى ذلك. تحمينا عمليات التحقق وقت التنفيذ لقواعد الاستعارة من حالات تسابق البيانات data race وفي بعض الأحيان يكون الأمر مستحقًا لمقايضة القليل من السرعة مقابل هذه المرونة في هياكل البيانات التي لدينا. لاحظ أن <RefCell<T لا يعمل مع الشيفرة متعددة الخيوط، ويعدّ <Mutex<T الإصدار الآمن من سلسلة <RefCell<T، وسنناقش <Mutex<T لاحقًا. ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: حلقات المرجع Reference Cycles وتسببها بتسريب الذاكرة Memory Leak في لغة رست المقال السابق: المؤشر Rc<T>‎ الذكي واستخدامه للإشارة إلى عدد المراجع في لغة رست Rust المؤشرات الذكية Smart Pointers في رست Rust الملكية Ownership في لغة رست البرمجة بلغة رست التحقق من المراجع References عبر دورات الحياة Lifetimes في لغة رست
  2. مبدأ الملكية ownership واضح في معظم الحالات، إذ تسمح لك الملكية بمعرفة أي متغير يملك قيمةً ما، ولكن هناك حالات تكون فيها القيمة الواحدة مملوكةً من أكثر من مالك، فمثلًا في شعبة هيكل البيانات graph data structure قد تؤشر العديد من الأضلع edges إلى العقدة node ذاتها، وبالتالي تمتلك هذه العقدة كل الأضلع التي تشير إليها. لا يجب تحرير العقدة من الذاكرة إلا في حال عدم وجود أي ضلع يشير إليها وبالتالي عدم امتلاكها من قبل أحد. يمكنك تمكين وجود عدة مالكين صراحةً باستخدام النوع Rc<T>‎ وهو اختصار لعدّ المرجع reference counting، إذ يُحصي النوع Rc<T>‎ الخاص برست عدد المراجع التي تشير إلى قيمة محددة لتحديد فيما إذا كانت تلك القيمة قيد الاستخدام أم لا، وإذا لم يكن هناك أي مراجع تشير للقيمة، عندها يمكن تحرير القيمة دون التسبب بجعل أي مرجع غير صالح. تخيل Rc<T>‎ مثل تلفاز في غرفة الجلوس، فعندما يدخل شخص الغرفة ليشاهد التلفاز يشغله، كما يمكن لآخرين القدوم للغرفة والمشاهدة أيضَا، وعندما يغادر آخر شخص الغرفة يطفئ التلفاز لأنه لم يعد يُستخدم، ولكن إذا أطفأ أحدهم التلفاز بينما يشاهده الآخرون فسيغضب الذين يشاهدون التلفاز. نستخدم النوع Rc<T>‎ عندما نريد وضع بعض البيانات في الكومة heap بحيث يقرؤها عدة أجزاءٌ مختلفة من برنامجنا ولا نستطيع معرفة أي الأجزاء سينتهي من استخدام هذه البيانات أخيرًا عند تصريف البرنامج. نستطيع جعل الجزء الأخير مالك البيانات إذا كنّا نعرفه وبذلك تُطبَّق قواعد الملكية الاعتيادية لرست عند وقت التصريف. لاحظ أن Rc<T>‎ موجود فقط للاستخدام في حالات استخدام الخيط الواحد single-threaded، وسنتحدث عن عدّ المراجع في البرامج ذات الخيوط المتعددة multithreaded لاحقًا عندما نتحدث عن التزامن concurrency. استخدام Rc‎ لمشاركة البيانات لنعاود النظر إلى قائمة البنية cons list في الشيفرة 5 من مقال المؤشرات الذكية السابق، تذكّر أننا عرفنا القائمة باستخدام Box<T‎>‎ إلا أننا سنُنشئ هذه المرة لائحتين يتشاركان ملكية قائمة ثالثة كما يوضح الشكل 3. الشكل 3: قائمة b وقائمة c يتشاركان ملكية قائمة ثالثة a ننشئ القائمة a التي تحتوي على 5 وبعدها 10، ومن ثم ننشئ قائمتين؛ قائمة b تبدأ بالقيمة 3 وقائمة c تبدأ بالقيمة 4، إذ ستستمر قيم كل من القائمتين b و c ضمن القائمة الثالثة a التي تحتوي على 5 و10، أي ستتشارك القائمتان القائمة الأولى التي تحتوي على 5 و10. لن تنجح محاولة تنفيذ هذه الحالة باستخدام تعريفنا للنوع List مع النوع Box<T>‎ كما هو موضح في الشيفرة 17: اسم الملف: src/main.rs enum List { Cons(i32, Box<List>), Nil, } use crate::List::{Cons, Nil}; fn main() { let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); let b = Cons(3, Box::new(a)); let c = Cons(4, Box::new(a)); } الشيفرة 17: شيفرة توضّح أنه ليس من المسموح استخدام قائمتين النوع Box<T>‎ مع مشاركة ملكيتهما لقائمة ثالثة عندما نصرّف الشيفرة السابقة نحصل على هذا الخطأ: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) error[E0382]: use of moved value: `a` --> src/main.rs:11:30 | 9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); | - move occurs because `a` has type `List`, which does not implement the `Copy` trait 10 | let b = Cons(3, Box::new(a)); | - value moved here 11 | let c = Cons(4, Box::new(a)); | ^ value used here after move For more information about this error, try `rustc --explain E0382`. error: could not compile `cons-list` due to previous error تمتلك متغايرات Cons البيانات التي تحتفظ بها، لذا عندما ننشئ القائمة b تنتقل القائمة a إلى القائمة b وتمتلك b القائمة a، ثم عندما نحاول استخدام a مجددًا عند إنشاء c لا يُسمح لنا، وذلك لأن القائمة a نُقلت. يمكننا تغيير تعريف Cons بحيث يخزّن المراجع عوضًا عن ذلك، ولكن علينا عندها تعريف معاملات دورة حياة lifetime parameters، إذ سيستطيع بذلك عنصر في القائمة أن يعيش مدّةً تساوي مدة حياة القائمة على الأقل، وهذه هي حالة العناصر الموجودة ضمن القائمة في الشيفرة 17 ولكن هذا لا ينطبق في كل حالة. عوضاً عن ذلك سنغير تعريف List لتستخدم Rc<T>‎ بدلاً من Box<T>‎ كما هو موضح في الشيفرة 18. يحفظ كل متغاير Cons قيمةً بالإضافة لمؤشر Rc<T>‎ يشير إلى List، عندما ننشئ b بدلًا من أخذ ملكية a، سننسخ Rc<List>‎ التي يحتفظ بها a وبذلك نزيد عدد المراجع من 1 إلى 2 ونجعل القائمة a والقائمة b تتشارك ملكية البيانات في Rc<List>‎، كما سننسخ أيضًا a عندما ننشئ c، وبذلك يزيد عدد المراجع من 2 إلى 3 مراجع، وفي كل مرة نستدعي Rc::clone سيزيد عدد المراجع للبيانات داخل Rc<List>‎ ولن تُحرَّر البيانات إلا إذا لم يكن هناك أي مرجع يشير إليها. اسم الملف: src/main.rs enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); let b = Cons(3, Rc::clone(&a)); let c = Cons(4, Rc::clone(&a)); } الشيفرة 18: تعريف List التي تستخدم Rc<T>‎ نحتاج لإضافة تعليمة use وذلك لإضافة Rc<T>‎ إلى النطاق لأنه غير موجود في البداية. أنشأنا في الدالة main قائمة تحتوي على 5 و10 وخزّناها في قيمة من نوع Rc<List>‎ جديدة ضمن a، ومن ثمّ استدعينا الدالة Rc::clone بعد أن أنشأنا b و c ومررنا مرجعًا مثل وسيط argument يشير إلى Rc<List>‎ في a. يمكننا استدعاء a.clone()‎ بدلًا من Rc::clone(&a)‎، إلا أن الطريقة الاصطلاحية في رست تستخدم Rc::clone في هذه الحالة. لا ينسخ تنفيذ Rc::clone نسخةً فعليةً deep copy للبيانات مثل باقي تنفيذات أنواع clone الأخرى، إذ يزيد استدعاء الدالة Rc::clone عدد المراجع وهو ما لا يأخذ وقتًا طويلًا، بينما يأخذ النسخ الفعلي للبيانات وقتًا طويلًا، إلا أنه يمكننا التمييز بصريًا بين النسخ الفعلي والنسخ الذي يزيد عدد المراجع باستخدام الدالة Rc::clone. نحتاج لأخذ موضوع النسخ الفعلية بالحسبان عندما نبحث عن مشاكل متعلقة بأداء الشيفرة البرمجية وذلك بإهمال استدعاءات Rc::clone. نسخ قيمة من النوع Rc‎ يزيد عدد المراجع لنغير مثالنا الموجود في الشيفرة 18 بحيث يمكننا رؤية تغيّر عدد المراجع عندما ننشئ ونحرّر مرجعًا إلى Rc<T>‎ في a. نغيّر من الدالة main في الشيفرة 19 بحيث تحتوي على نطاق داخلي حول القائمة c لكي نستطيع ملاحظة تغيّر قيمة عداد المراجع عندما تخرج c خارج النطاق. اسم الملف: src/main.rs enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!("count after creating a = {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!("count after creating b = {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!("count after creating c = {}", Rc::strong_count(&a)); } println!("count after c goes out of scope = {}", Rc::strong_count(&a)); } الشيفرة 19: طباعة عدد المراجع نطبع عدد المراجع في كل نقطة يتغير فيه قيمته ضمن البرنامج، ونحصل على تلك القيمة باستدعاء الدالة Rc::strong_count. نسمّي الدالة strong_count بدلًا من count، وذلك لأن النوع Rc<T>‎ يحتوي أيضًا على weak_count، وسنستخدم weak_count في مقال لاحق عندما نتحدث عن منع دورات المراجع وتحويل Rc<T>‎ إلى Weak<T>‎. تطبع الشيفرة السابقة ما يلي: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.45s Running `target/debug/cons-list` count after creating a = 1 count after creating b = 2 count after creating c = 3 count after c goes out of scope = 2 نلاحظ أن Rc<List>‎ في a تحتوي على عدد مراجع مبدئي يساوي إلى 1، ثم يزداد العدد كل مرة نستدعي فيها clone بمقدار 1، وعندما تخرج c خارج النطاق ينقص العدد بمقدار 1. لسنا بحاجة لاستدعاء الدالة لإنقاص عدد المراجع كما نفعل عند استدعاء Rc::clone بهدف زيادة عدد المراجع، إذ يُنقص تنفيذ سمة drop من عدد المراجع تلقائيًا عندما تخرج قيمة Rc<T>‎ من النطاق. ما لا نستطيع رؤيته في هذا المثال هو خروج a من النطاق بعد خروج b في نهاية الدالة main، ويُضبط عدد المراجع فيما بعد إلى القيمة 0، وعندها تصبح Rc<List>‎ محرَّرة تمامًا. يسمح استخدام Rc<T>‎ بأن يكون لقيمة واحدة أكثر من مالك، كما أن عدد المراجع يؤكد أن القيمة لا تزال صالحةً طالما لا يزال هناك مالك. يسمح Rc<T>‎ عن طريق المراجع الثابتة immutable مشاركة البيانات بين أقسام متعددة من البرنامج للقراءة فقط. قد تخرق بعض قوانين الاستعارة -التي ناقشناها سابقًا في مقال المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست- في حلقات المرجع Reference Cycles وتسببها بتسريب الذاكرة Memory Leak في لغة رست Rust إذا سمح Rc<T>‎ بوجود عدة مراجع متغيّرة أيضًا؛ إذ قد تسبب الاستعارات المتعددة المتغيّرة لنفس المكان حالة تعارض وتناقض للبيانات data race، إلا أن القدرة على تغيير البيانات مفيدة جدًا. سنناقش في القسم القادم النمط الداخلي المتغيّر interior mutability pattern، إضافةً إلى النوع RefCell<T>‎ الذي يمكن استخدامه مع Rc<T>‎ لتجاوز قيد الثبات هذا. ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: المؤشر الذكي Refcell<T>‎ ونمط قابلية التغيير الداخلي interior mutability في لغة رست المقال السابق: تنفيذ شيفرة برمجية عند تحرير الذاكرة cleanup باستخدام السمة Drop في لغة رست الملكية Ownership في لغة رست المؤشرات الذكية Smart Pointers في رست Rust التحقق من المراجع References عبر دورات الحياة Lifetimes في لغة رست البرمجة بلغة رست كيفية استخدام النوع HashMap لتخزين البيانات في رست Rust
  3. أنظمة التحكم في الإصدار version control هي أدوات تسجّل جميع تغيرات الشيفرة المصدرية وتجعل من السهل استرداد الإصدارات القديمة من الشيفرات البرمجية، ويمكنك النظر إلى هذه الأدوات بكونها أدوات متطورة للتراجع عن فعل ما؛ فعلى سبيل المثال، إذا استبدلت دالةً ثم قررت لاحقًا تفضيل الدالة القديمة، يمكنك استعادة شيفرتك إلى الإصدار الأصلي، أو في حال اكتشفت خطأً جديدًا، فيمكنك العودة إلى الإصدارات السابقة لتحديد وقت ظهوره لأول مرة وأي تغيير في الشيفرة تسبب بحدوثه. يدير نظام التحكم في الإصدار الملفات أثناء إجراء التغييرات عليها، ويشابه ذلك عمل نسخة من مجلد "مشروعي" وتسميته "نسخة من مشروعي"، إذ سيتعين عليك إذا واصلت إجراء التغييرات إنشاء نسخة أخرى تسميها "نسخة 2 من مشروعي"، ثم نسخة "نسخة 3 من مشروعي" ثم "نسخة 3b من مشروعي" ثم "نسخة من مشروعي يوم الثلاثاء" وما إلى ذلك وهلم جرًّا، وقد يكون نسخ المجلدات أمرًا بسيطًا، إلا أن هذه الطريقة تصبح أقل وأقل فاعلية مع تكرارها. بدلًا من ذلك، تعلّم استخدام نظام التحكم في الإصدار، وسيوفر لك الوقت والصداع على المدى الطويل. تعدّ كل من غيت Git و Mercurial وSubversion تطبيقات شائعة للتحكم في الإصدار، إلا أن غيت هو الأكثر شيوعًا. ستتعلم في هذه المقالة كيفية إعداد الملفات لمشاريع الشيفرات البرمجية واستخدام غيت لتتبع تغيراتها. إيداعات غيت Git يتيح لك غيت Git حفظ حالة ملفات مشروعك، المسماة لقطات snapshots أو إيداعات commits، أثناء إجراء التغييرات عليها. يمكنك بهذه الطريقة العودة إلى أي لقطة سابقة إذا احتجت إلى ذلك. قد يكون الإيداع اسمًا أو فعلًا بحسب السياق، إذ يودع المبرمجون (أو يحفظون) إيداعاتهم (أو لقطاتهم)، والمصطلح الآخر للإيداع هو تسجيل الوصول check-in إلا أنه أقل شيوعًا. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن تُسهّل أنظمة التحكم في الإصدار أيضًا على فريق مطوري البرامج أن يبقوا متزامنين مع بعضهم بعضًا أثناء إجراء تغييرات على الشيفرات المشروع المصدرية، إذ يمكن للمبرمجين عندما يُجري مبرمج آخر تغييرًا ما سحب هذه التحديثات إلى حواسيبهم. يتتبع نظام التحكم في الإصدار الإيداعات الحاصلة، ومن أجراها، ومتى أجراها، جنبًا إلى جنب مع تعليقات المطورين التي تصف التعديلات. يدير نظام التحكم في الإصدار الشيفرة المصدرية للمشروع في مجلد يسمى المستودع repository -أو اختصارًا repo- ويتوجب عليك عمومًا الاحتفاظ بمستودع غيت منفصل لكل مشروع تعمل عليه. تفترض هذه المقالة أنك تعمل غالبًا بمفردك ولا تحتاج إلى ميزات غيت المتقدمة، مثل التفرع والدمج، التي تساعد المبرمجين على التعاون، ولكن حتى لو كنت تعمل بمفردك، فليس هناك مشروع برمجة صغير جدًا للاستفادة من برنامج التحكم في الإصدار. استخدام أداة Cookiecutter لإنشاء مشاريع بايثون جديدة نسمي المجلد الذي يحتوي على الشيفرة المصدرية والوثائق والاختبارات والملفات الأخرى المتعلقة بالمشروع باسم "دليل العمل" أو "شجرة العمل working tree" في أداة غيت، أو ملف المشروع عمومًا. تسمى الملفات الموجودة في دليل العمل ككل بنسخة العمل. لننشئ قبل إنشاء مستودع غيت الملفات الخاصة بمشروع بايثون. كل مبرمج لديه طريقة مفضلة لإنشاء الملفات، ومع ذلك، تتبع مشاريع بايثون اصطلاحات أسماء المجلدات والتسلسلات الهرمية. قد تتكون برامجك الأبسط من ملف "‎.py" واحد، ولكن عندما تتعامل مع مشاريع أكثر تعقيدًا، ستبدأ بتضمين ملفات "‎.py" وملفات بيانات وتوثيق واختبارات للوحدات والمزيد. يحتوي عادةً جذر مجلد المشروع على مجلد باسم src لملفات التعليمات البرمجية المصدرية "‎.py" ومجلد اختبارات لاختبارات الوحدات ومجلد مستندات لأي وثائق، مثل تلك التي تُنشأ بواسطة أداة التوثيق سفينكس Sphinx، بينما تحتوي الملفات الأخرى على معلومات المشروع وأداة الضبط على النحو التالي: ملف README.md للحصول على معلومات عامة، وملف ‎.coveragerc لتغطية أداة ضبط الشيفرة، و LICENSE.txt لترخيص برنامج المشروع، وما إلى ذلك. هذه الأدوات والملفات خارج نطاق هذه السلسلة، لكنها جديرة بالبحث عنها والتعرف إليها. تصبح عملية إعادة إنشاء الملفات الأساسية السابقة ذاتها لمشاريع البرمجة الجديدة أمرًا شاقًا مع ممارستك للبرمجة لوقتٍ أطول، ويمكنك لتسريع عملية البرمجة استخدام وحدة cookiecutter الخاصة ببايثون لإنشاء هذه الملفات والمجلدات تلقائيًا، إليك التوثيق الكامل لكل من الوحدة وبرنامج سطر الأوامر Cookiecutter. لتثبيت Cookiecutter، نفذ الأمر التالي على ويندوز: pip install --user cookiecutter أو الأمر التالي على على ماك macOS ولينكس Linux: pip3 install --user cookiecutter يتضمن هذا التثبيت برنامج سطر أوامر Cookiecutter ووحدة cookiecutter الخاصة ببايثون. قد يحذرك الخرج من تثبيت برنامج سطر الأوامر في مجلد غير مدرج في متغير PATH كما يلي: Installing collected packages: cookiecutter WARNING: The script cookiecutter.exe is installed in 'C:\Users\Al\AppData\Roaming\Python\Python38\Scripts' which is not on PATH. Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location. تذكّر إضافة المجلد (C:\Users\Al...\Scripts في هذه الحالة كما في الشيفرة السابقة) إلى متغير بيئة PATH. وإلا فسيتعين عليك تنفيذ Cookiecutter مثل وحدة بايثون عن طريق إدخال ملف تعريف الارتباط python -m في ويندوز، أو ملف تعريف الارتباط python3 -m في نظامي ماك أوإس ولينكس بدلًا من الاكتفاء بكتابة cookiecutter. سنُنشئ في هذه المقالة مستودعًا لوحدة تسمى wizcoin، وهي وحدة تتعامل مع العملات المعدنية من نوع galleon، و sickle و knut وهي عملات خيالية لعالم سحري. تستخدم وحدة cookiecutter قوالب لإنشاء ملفات البداية لعدة أنواع مختلفة من المشاريع. يكون القالب غالبًا مجرد رابط لموقع غيت هب GitHub.com. على سبيل المثال، يمكنك من مجلد "C:\Users\Al" إدخال ما يلي في الطرفية Terminal لإنشاء مجلد "C:\Users\Al\wizcoin" مع الملفات الاعتيادية لمشروع بايثون الأساسي. تحمّل وحدة cookiecutter بدورها القالب من غيت هب GitHub وتسألك سلسلةً من الأسئلة حول المشروع الذي تريد إنشاءه: C:\Users\Al>‎‎‎cookiecutter gh:asweigart/cookiecutter-basicpythonproject project_name [Basic Python Project]: WizCoin module_name [basicpythonproject]: wizcoin author_name [Susie Softwaredeveloper]: Al Sweigart author_email [susie@example.com]: al@inventwithpython.com github_username [susieexample]: asweigart project_version [0.1.0]: project_short_description [A basic Python project.]: A Python module to represent the galleon, sickle, and knut coins of wizard currency. يمكنك أيضًا تنفيذ python -m cookiecutter إذا حصلت على خطأ بدلًا من cookiecutter، إذ يحمّل هذا الأمر نموذجًا مُنشأ من cookiecutter-basicpythonproject، وستجد قوالبًا للعديد من لغات البرمجة على الرابط، ونظرًا لاستضافة قوالب Cookiecutter غالبًا على غيت هب، فيمكنك أيضًا كتابة ":gh" اختصارًا للرابط https://github.com في سطر الأوامر. عندما يسألك Cookiecutter أسئلة، يمكنك إما إدخال إجابة أو ببساطة الضغط على مفتاح الإدخال ENTER لاستخدام الإجابة الافتراضية الموضحة بين قوسين مربعين. على سبيل المثال، يطلب منك project_name [Basic Python Project]:‎ تسمية مشروعك، فإذا لم تدخل شيئًا سيستخدم Cookiecutter النص ‎"Basic Python Project"‎ اسمًا للمشروع، كما تساعدك هذه الإعدادات الافتراضية أيضًا لمعرفة نوع الإجابة المتوقعة. يعرض ما يلي اسم المشروع بأحرف كبيرة يتضمن مسافات: project_name [Basic Python Project]:‎ بينما يوضح اسم الوحدة بأحرف صغيرة ولا تحتوي على مسافات: module_name [basicpythonproject]:‎ لم ندخل ردًا لموجّه project_version [0.1.0]:‎، لذا فإن الإستجابة الافتراضية هي "0.1.0". يُنشئ Cookiecutter بعد الإجابة على الأسئلة مجلد wizcoin في مجلد العمل الحالي مع الملفات الأساسية التي ستحتاجها لمشروع بايثون، كما هو موضح في الشكل 1. [الشكل 1: الملفات الموجودة في مجلد wizcoin الذي أُنشئ بواسطة Cookiecutter.] لا بأس إذا كنت لا تفهم الغرض من هذه الملفات. الشرح الكامل لكل منها خارج نطاق هذه السلسلة، ولكن يحتوي الرابط على روابط وشرح وصفي لمزيد من القراءة. الآن وبعد أن أصبح لدينا ملفات البداية، دعنا نتتبعها باستخدام غيت. تثبيت غيت git قد يكون غيت git مثبتًا فعلًا على حاسوبك، وللتيقن من ذلك، نفّذ git --version من سطر الأوامر، فإذا رأيت رسالةً، مثل git version 2.29.0.windows.1، فهذا يعني أن غيت مثبتًا فعلًا، أما إذا رأيت رسالة الخطأ "الأمر غير موجود"، فيجب عليك تثبيت غيت. إذا كنت تستخدم نظام ويندوز فانتقل إلى "https://git-scm.com/download"، ثم حمّل مثبّت غيت Git installer وشغّله، أما إذا كنت تستخدم نظام التشغيل ماك macOS Mavericks (10.9)‎ أو إصدارًا أحدث، فما عليك سوى تنفيذ git --version من الطرفية وستبدأ عملية تثبيت غيت في حال عدم وجوده، كما هو موضح في الشكل 2. أما إذا كنت تستخدم أوبنتو لينكس Ubuntu أو ديبيان لينكس Debian نفذ sudo apt install git-all من الطرفية Terminal، أو إذا كنت تستخدم ريدهات لينكس Red Hat، فنفذ sudo dnf install git-all من الطرفية، وإذا كنتَ تستخدم نظامًا آخر ابحث عن إرشادات لموزعين لينكس الآخرين على git-scm.com/download/linux، وتأكد من أن عملية التثبيت نجحت عن طريق تنفيذ git --version. [الشكل 2: سيُطلب منك في المرة الأولى التي تنفّذ فيها git --version على macOS 10.9 أو أحدث بتثبيت غيت.] ضبط اسم المستخدم والبريد الإلكتروني الخاصين بغيت ستحتاج إلى ضبط اسمك وبريدك الإلكتروني بعد تثبيت غيت بحيث تتضمن إيداعاتك معلومات المؤلف (أنت)، ولفعل ذلك، نفّذ الأمر git config من الطرفية وذلك باستخدام اسمك ومعلومات بريدك الإلكتروني كما يلي: C:\Users\Al>‎‎‎git config --global user.name "Al Sweigart" C:\Users\Al>‎‎‎git config --global user.email al@inventwithpython.com تُخزن هذه المعلومات في ملف "‎.gitconfig" في المجلد الرئيسي الخاص بك، مثل "C:\Users\Al" على حاسوبك الذي يعمل بنظام ويندوز. لن تحتاج أبدًا إلى تعديل هذا الملف النصي مباشرةً، بل بدلًا من ذلك، يمكنك تغييره عن طريق تنفيذ الأمر git config كما يمكنك إظهار إعدادات ضبط غيت الحالية باستخدام الأمر git config --list. ثبيت أدوات واجهة المستخدم الرسومية لغيت GUI Git تركز هذه المقالة على أداة سطر أوامر غيت، ولكن تثبيت البرنامج الذي يضيف واجهة المستخدم الرسومية لغيت يمكن أن يساعدك في المهام اليومية. يستخدم المبرمجون المحترفون الذين يعرفون سطر أوامر غيت CLI Git أدوات واجهة المستخدم الرسومية لغيت. تحتوي صفحة الويب https://git-scm.com/downloads/guis على العديد من هذه الأدوات، مثل TortoiseGit لنظام التشغيل ويندوز و GitHub Desktop لنظام التشغيل ماك و GitExtensions لنظام التشغيل لينكس. يوضح الشكل 3 كيف تضيف أداة TortoiseGit على ويندوز رموزًا إلى أيقونات مستعرض الملفات بناءً على حالتها؛ بحيث يشير الأخضر لملفات المستودع غير المعدلة، والأحمر لملفات المستودع المعدلة (أو المجلدات التي تحتوي على ملفات معدلة)، وتغيب الرموز عن الملفات التي لم يجري تعقبها. من المؤكد أن التحقق من هذه الرموز هو أكثر سهولة من إدخال الأوامر باستمرار في طرفية للحصول على هذه المعلومات، كما تضيف TortoiseGit أيضًا أوامر في القائمة المنسدلة لتنفيذ أوامر غيت كما هو موضح في الشكل 3. يعد استخدام أدوات واجهة المستخدم الرسومية لغيت أمرًا مريحًا، إلا أنه ليس بديلًا عن تعلم أوامر سطر الأوامر الواردة في هذه المقالة. ضع في الحسبان أنك قد تحتاج يومًا ما إلى استخدام غيت على حاسوب لم تُثَبّت أدوات واجهة المستخدم الرسومية عليه. [الشكل 3: تضيف TortoiseGit لنظام التشغيل ويندوز واجهة مستخدم رسومية لتنفيذ أوامر غيت من مستعرض الملفات.] سير عمل غيت يتضمن استخدام مستودع غيت الخطوات التالية: أولًا، إنشاء مستودع غيت عن طريق تنفيذ الأمر git init أو الأمر git clone. ثانيًا، إضافة ملفات باستخدام الأمر git add <filename>‎‎‎‎ لتتبع المستودع. ثالثًا، بمجرد إضافة الملفات، يمكنك إيداع التغييرات الحاصلة فيها باستخدام الأمر: git commit -am "<descriptive commit message>‎‎‎‎"‎ وبعد ذلك أنت جاهز لإجراء التغييرات على شفرتك مجددًا. يمكنك عرض ملف التعليمات لكل من هذه الأوامر عن طريق تنفيذ git help <command>‎‎‎‎، مثل git help init أو git help add. صفحات المساعدة هذه سهلة الاستخدام والرجوع إليها على الرغم من أنها مملة وتقنية لاستخدامها مثل وسيلة تعليمية، وستتعرف على مزيد من التفاصيل بخصوص كل من هذه الأوامر لاحقًا، ولكن أولًا، تحتاج إلى فهم بعض مفاهيم غيت لتسهيل استيعاب بقية هذه المقالة. كيفية تتبع غيت لحالة الملف الملفات الموجودة في مجلد المشروع هي ملفات مُتتبعة tracked من غيت أو غير متتبعة untracked والملفات المتتبعة هي الملفات التي تُضاف وتودع في المستودع، بينما يُصنّف أي ملف آخر على أنه ملف غير متتبع. قد لا تتواجد الملفات التي لم يجري تتبعها في مجلد المشروع في مستودع غيت، بينما توجد الملفات المتتبعة بإحدى الحالات الثلاث: حالة الإيداع committed state: هي عندما يكون الملف بنسخة مجلد المشروع مطابقًا لأحدث إيداع في المستودع، وتسمى هذه الحالة أحيانًا بالحالة غير المعدلة unmodified state أو بالحالة النظيفة clean state. الحالة المعدلة modified state: هي الحالة التي يكون عندها الملف في مجلد المشروع مختلفًا عن أحدث إيداع في المستودع. الحالة المُدرجة staged state: هي عندما يُعدّل الملف وتوضع علامة عليه ليُضمَّن في الإيداع التالي، ونقول عندها أن الملف مدرج أو في منطقة الإدراج، وتُعرف منطقة الإدراج أيضًا بالفهرس index أو ذاكرة التخزين المؤقتة cache. يحتوي الشكل 4 على رسم تخطيطي لكيفية تنقّل الملف بين هذه الحالات الأربع، ويمكنك إضافة ملف لم يُتتبَّع إلى مستودع غيت، وفي هذه الحالة يُتتبَّع ويُدرج، ومن ثم يمكنك إيداع ملفات مُدرجة لوضعها في حالة الإيداع. لا تحتاج إلى أي أمر غيت لوضع الملف في الحالة الُمعدّلة؛ فبمجرد إجراء تغييرات على ملف مودع، يُصنَّف تلقائيًا على أنه في الحالة المعدلة. [الشكل 4: الحالات المحتملة لملف في مستودع غيت والتنقل بينها.] نفذ git status في أي خطوة بعد إنشاء المستودع لعرض حالة المستودع الحالية وحالة ملفاته. ستُنفذ هذا الأمر بصورةٍ متكررة أثناء عملك مع غيت. في المثال التالي، أعددت ملفات في حالات مختلفة، لاحظ كيف تظهر هذه الملفات الأربعة في خرج git status: C:\Users\Al\ExampleRepo>‎‎‎git status On branch master Changes to be committed: (use "git restore --staged <file>‎‎‎..." to unstage) 1 new file: new_file.py 2 modified: staged_file.py Changes not staged for commit: (use "git add <file>‎‎‎..." to update what will be committed) (use "git restore <file>‎‎‎..." to discard changes in working directory) 3 modified: modified_file.py Untracked files: (use "git add <file>‎‎‎..." to include in what will be committed) 4 untracked_file.py في مشروعنا هذا، يوجد new_file.py (سطر 1) الذي أُضيفَ مؤخرًا إلى المستودع وبالتالي فهو في الحالة المُدرجة. هناك أيضًا ملفان متتبعان، وهما staged_file.py (سطر 2) و modified_file.py (سطر 3)، وهما في الحالة المُدرجة والمعدلة، على التوالي، ثم هناك ملف غير مُتتبع اسمه untracked_file.py (سطر 4). يحتوي خرج git status أيضًا على تذكيرات لأوامر غيت التي تنقل الملفات إلى حالات أخرى. ما هي الفائدة من وضع الملفات في الحالة المدرجة؟ قد تتساءل ما هو الهدف من الحالة المُدرجة، لمَ لا ننتقل فقط بين التعديل والإيداع دون الملفات المُدرجة؟ التعامل مع الحالة المُدرجة مليء بالحالات الخاصة الشائكة ومصدر كبير للارتباك للمبتدئين في غيت. على سبيل المثال، يمكن تعديل الملف بعد إدراجه، مما يؤدي إلى وجود ملفات في كل من الحالات المعدلة والمُدرجة، كما هو موضح في القسم السابق. تقنيًا، لا تحتوي المنطقة المُدرجة على ملفات بقدر ما تحتوي على تغييرات، لأنه يمكن لأجزاء من ملف واحد معدل أن تكون في الحالة المدرجة، بينما لا تنتمي أجزاء أخرى لهذه الحالة. هذه هي الحالات التي تسببت بشهرة غيت إذ أنها معقدة، وغالبًا ما تكون العديد من مصادر المعلومات حول كيفية عمل غيت غير دقيقة في أحسن الأحوال ومضللة في أسوأ الأحوال. إنشاء مستودع غيت على حاسوبك غيت هو نظام تحكم في الإصدار الموزع، مما يعني أنه يخزن جميع اللقطات وبيانات المستودع الوصفية محليًا على حاسبك في مجلد يسمى "‎.git". على عكس نظام التحكم في الإصدار المركزي، لا يحتاج غيت للاتصال بخادم عبر الإنترنت لإجراء الإيداعات، مما يجعل غيت سريعًا ومتاحًا للعمل معه عندما تكون غير متصل بالإنترنت. من الطرفية، نفذ الأوامر التالية لإنشاء مجلد "‎.git". ستحتاج في نظامي ماك أو إس ولينكس، إلى تنفيذ mkdir بدلًا من md. C:\Users\Al>‎‎‎md wizcoin C:\Users\Al>‎‎‎cd wizcoin C:\Users\Al\wizcoin>‎‎‎git init Initialized empty Git repository in C:/Users/Al/wizcoin/.git/ عند تحويل مجلد إلى مستودع غيت بتنفيذ git init، تبدأ جميع الملفات الموجودة فيه بدون تتبع. بالنسبة لمجلد wizcoin الخاص بنا، يُنشئ الأمر git init مجلدًا يدعى "wizcoin/.git"، الذي يحتوي بدوره على البيانات الوصفية لمستودع غيت. يؤدي وجود هذا المجلد "‎.git" إلى جعل المجلد مستودع غيت؛ وبدونه سيكون لديك ببساطة مجموعة من ملفات الشيفرة المصدرية في مجلد عادي. لن تضطر أبدًا إلى تعديل الملفات في "‎.git" مباشرةً، لذا تجاهل هذا المجلد. في الواقع، سُميَ المجلد "‎.git" بهذا الاسم لأن معظم أنظمة التشغيل تخفي تلقائيًا المجلدات والملفات التي تبدأ أسماؤها بنقطة. الآن لديك مستودع في المجلد "C:\Users\Al\wizcoin". يُعرف المستودع الموجود على حاسبك باسم المستودع المحلي local repo؛ يُعرف المستودع الموجود على حاسوب شخص آخر باسم المستودع البعيد remote repo. هذا التمييز مهم، لأنه سيتعين عليك غالبًا مشاركة الإيداعات بين المستودعات البعيدة والمحلية حتى تتمكن من العمل مع مطورين آخرين في المشروع ذاته. يمكنك الآن استخدام الأمر git لإضافة ملفات وتتبع التغييرات داخل مجلد المشروع، إذ سترى ما يلي إذا نفذت git status في المستودع الذي أنشأته حديثًا: C:\Users\Al\wizcoin>‎‎‎git status On branch master No commits yet nothing to commit (create/copy files and use "git add" to track) يخبرك خرج هذا الأمر أنه ليس لديك أي إيداعات حتى الآن في هذا المستودع. تنفيذ أمر git status مع أمر watch أثناء استخدام أداة سطر أوامر غيت، ستنفّذ غالبًا الأمر git status لمعرفة حالة المستودع الخاص بك، وبدلًا من إدخال هذا الأمر يدويًا، يمكنك استخدام الأمر watch لتنفيذه نيابةً عنك. ينفّذ الأمر watch أمرًا معينًا بصورةٍ متكررة كل ثانيتين، مع تحديث الشاشة بأحدث خرج لها. يمكنك الحصول على أمر watch عن طريق تنزيل InventWithPython في نظام التشغيل ويندوز، أو يمكنك الحصول على أمر watch عن طريق تنزيل InventWithPython ووضع هذا الملف في مجلد "PATH"، مثل "C:\Windows"، بينما يمكنك الانتقال إلى https://www.macports.org/ في نظام ماك، لتنزيل MacPorts وتثبيته، ثم نفذ sudo ports install watch. يحتوي نظام لينكس على هذا الأمر فعلًا، وبمجرد تثبيته، افتح موجه أوامر جديد أو نافذة طرفية جديدة، ونفذ cd لتغيير المجلد إلى مجلد مستودع غيت الخاص بك، ونفذ watch "git status"‎؛ إذ سيعمل الأمر watch على تنفيذ git status كل ثانيتين، ويعرض أحدث النتائج على الشاشة. يمكنك ترك هذه النافذة مفتوحة أثناء استخدام أداة سطر أوامر غيت في نافذة طرفية مختلفة لترى كيف تتغير حالة المستودع في الوقت الفعلي، كما يمكنك فتح نافذة طرفية أخرى وتنفيذ watch "git log -oneline"‎ لعرض ملخص الإيداعات التي تفعلها، والتي يجري تحديثها أيضًا في الوقت الفعلي. تساعد هذه المعلومات في إزالة الغموض المتعلق بما تفعله أوامر غيت التي تكتبها في المستودع الخاص بك. إضافة ملفات لغيت لتعقبها يمكن فقط إيداع أو التراجع عن أو التفاعل مع الملفات المتتبعة من خلال الأمر git. نفّذ git status لمعرفة حالة الملفات في مجلد المشروع: C:\Users\Al\wizcoin>git status On branch master No commits yet 1 Untracked files: (use "git add <file>..." to include in what will be committed) .coveragerc .gitignore LICENSE.txt README.md --snip-- tox.ini nothing added to commit but untracked files present (use "git add" to track) لم يحدث تعقب لأي من الملفات الموجودة في مجلد "wizcoin" حاليًا (سطر 1)، ويمكننا تتبعها عن طريق إجراء إيداع أولي لهذه الملفات، الذي يكون على خطوتين: تنفيذ git add لكل ملف يجري إيداعه، ثم تنفيذ git commit لإنشاء إيداع لكل هذه الملفات، ويتتبع غيت الملف بمجرّد إيداعه. ينقل الأمر git add الملفات من حالة عدم التتبع أو الحالة المعدلة إلى الحالة المُدرجة، إذ يمكننا تنفيذ git add لكل ملف نخطط لتعديله. على سبيل المثال، git add .coveragerc و git add .gitignore و git add LICENSE.txt وما إلى ذلك، لكن هذا أمر ممل. بدلًا من ذلك، دعنا نستخدم الرمز * لإضافة عدة ملفات مرة واحدة. على سبيل المثال، يضيف git add *.py جميع ملفات "‎.py" في مجلد العمل الحالي والمجلدات الفرعية الخاصة به. لإضافة كل ملف لم يجري تعقبه، استخدم نقطة واحدة (.) لإخبار غيت بمطابقة جميع الملفات: C:\Users\Al\wizcoin>‎‎‎git add . نفّذ git status لرؤية الملفات التي أدرجتها: C:\Users\Al\wizcoin>git status On branch master No commits yet 1 Changes to be committed: (use "git rm --cached <file>..." to unstage) 2 new file: .coveragerc new file: .gitignore --snip-- new file: tox.ini يخبرك خرج git status عن الملفات التي ستُنفَّذ على مراحل في المرة التالية التي تنفّذ فيها git commit (سطر 1)، ويخبرك أيضًا أن هذه ملفات جديدة أُضيفت إلى المستودع (سطر 2) بدلًا من الملفات المُعدّلة الموجودة في المستودع. بعد تنفيذ git add لتحديد الملفات المراد إضافتها إلى المستودع، نفذ الأمر التالي: git commit -m "Adding new files to the repo.” و git status مرةً أخرى لعرض حالة المستودع: C:\Users\Al\wizcoin>git commit -m "Adding new files to the repo." [master (root-commit) 65f3b4d] Adding new files to the repo. 15 files changed, 597 insertions(+) create mode 100644 .coveragerc create mode 100644 .gitignore --snip-- create mode 100644 tox.ini C:\Users\Al\wizcoin>git status On branch master nothing to commit, working tree clean لاحظ أن أي ملفات مدرجة في ملف "‎.gitignore" لن تُضاف إلى منطقة الإدراج، كما سيوضح القسم التالي. تجاهل الملفات في المستودع تظهر الملفات التي لم يجري تتبعها بواسطة غيت على أنها لم غير متتبعة عند تنفيذ git status، ولكن قد ترغب في استبعاد ملفات معينة من نظام التحكم في الإصدار تمامًا أثناء كتابة الشيفرة البرمجية، وذلك حتى لا تتبعها عن طريق الخطأ. وتشمل هذه الملفات: الملفات المؤقتة في مجلد المشروع. ملفات "‎.pyc" و "‎.pyo" و "‎.pyd" التي ينشئها مُفسر بايثون عند تنفيذ برامج "‎.py". ملفات "‎.tox" و "htmlcov" والمجلدات الأخرى التي تنشئها أدوات تطوير البرامج المختلفة docs/_build. أي ملفات أخرى مجمعة أو مُنشأة يمكن إعادة إنشائها (لأن المستودع مخصص للملفات المصدرية، وليس للمنتجات المُنشأة من الملفات المصدرية). ملفات الشيفرة المصدرية التي تحتوي على كلمات مرور قاعدة البيانات أو رموز المصادقة المميزة أو أرقام بطاقات الائتمان أو غيرها من المعلومات الحساسة. لتجنب تضمين هذه الملفات، أنشئ ملفًا نصيًا باسم "‎.gitignore" يحتوي على المجلدات والملفات التي يجب ألا يتتبعها غيت مطلقًا، وسيستثني غيت بدوره هذه الملفات والمجلدات تلقائيًا من أوامر git add أو git commit، ولن تظهر عند تنفيذ git status. يبدو ملف "‎.gitignore" الذي ينشئه قالب cookiecutter-basicpythonproject على النحو التالي: # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class --snip-- يستخدم ملف "‎.gitignore" الرمز * لأحرف البدل wildcards و # للتعليقات. يمكنك قراءة المزيد عنها على صفحة gitignore في التوثيق الرسمي. يجب عليك إضافة ملف "‎.gitignore" الفعلي إلى مستودع غيت حتى يتمكن المبرمجون الآخرون من الحصول عليه إذا استنسخوا clone مستودعك. إذا كنت تريد معرفة أي الملفات في دليل العمل قد جرى تجاهلها بناءً على الإعدادات في ‎.gitignore، فنفذ الأمر التالي: git ls-files --other --ignored --exclude-standard الخلاصة أنظمة التحكم في الإصدار منقذة المبرمجين، إذ يؤدي إيداع لقطات من الشيفرة البرمجية إلى تسهيل مراجعة تقدمك، وفي بعض الحالات، التراجع عن التغييرات التي لا تحتاج إليها. من المؤكد أن تعلُّم أساسيات نظام التحكم في الإصدار مثل غيت يوفر لك الوقت على المدى الطويل. تحتوي مشاريع بيثون عادةً على العديد من الملفات والمجلدات الافتراضية، وتساعدك وحدة cookiecutter في إنشاء نموذج أساسي للعديد من هذه الملفات. تشكل هذه الملفات الملفات الأولى التي تودعها في مستودع غيت المحلي الخاص بك. نسمي المجلد الذي يحتوي على كل هذا المحتوى مجلد العمل أو مجلد المشروع. ترجمة -وبتصرف- لقسم من الفصل ORGANIZING YOUR CODE PROJECTS WITH GIT من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: تلميحات النوع Type Hints في بايثون بدء العمل مع نظام إدارة الإصدارات جيت Git مبادئ Git الأساسية
  4. السمة الثانية المهمة لنمط المؤشرات الذكية smart pointer pattern هي السمة Drop، التي تسمح لك بتخصيص ماذا يحدث إذا كانت القيمة ستخرج من النطاق scope. يمكنك تأمين تنفيذ لسمة Drop على أي نوع، ويمكن استخدام هذه الشيفرة البرمجية لتحرير الموارد، مثل الملفات واتصالات الشبكة. قدّمنا السمة Drop في سياق المؤشرات الذكية، إذ تُستخدم وظيفة سمة Drop دائمًا عند تطبيق مؤشر ذكي، ومثال على ذلك: عندما يُحرَّر Box<T>‎ستحرّر المساحة المخصصة له في الكومة heap التي يشير إليها الصندوق box. يتوجب على المبرمج في بعض لغات البرمجة ولبعض الأنواع استدعاء الشيفرة البرمجية التي تحرّر مساحة تخزين أو موارد في كل مرة ينتهي من استخدام نسخة instance من هذه الأنواع، ومن الأمثلة على ذلك مقابض الملفات file handles والمقابس sockets أو الأقفال locks، وإذا نسي المبرمجون استدعاء تلك الشيفرة البرمجية (لتحرير مساحة التخزين والموارد)، سيزداد التحميل على النظام وسيتوقف النظام عن العمل بحلول نقطة معيّنة. يمكنك في لغة رست تحديد قسم معين من الشيفرة تُنفذ عندما تخرج قيمة ما من النطاق، إذ سيضيف المصرف هذه الشيفرة تلقائيًا ونتيجةً ذلك لا تحتاج أن تكون حذرًا بخصوص وضع شيفرة تحرير المساحة البرمجية cleanup code في كل مكان في البرنامج عندما تكون نسخة من نوع معين قد انتهت، أي لن يكون هناك أي هدر في الموارد. يمكنك تحديد الشيفرة البرمجية التي تريد تنفيذها عندما تخرج قيمة ما عن النطاق وذلك باستخدام تنفيذ سمة Drop، إذ تحتاج سمة Drop أن تطبّق تابع method واحد اسمه drop يأخذ مرجعًا متغيّرًا إلى self لينتظر استدعاء رست للدالة drop. دعنا ننفّذ drop مع تعليمات println!‎ في الوقت الحالي. توضّح الشيفرة 15 البنية CustomSmartPointer بوظيفة مخصّصة وحيدة هي طباعة Dropping CustomSmartPointer!‎ عندما تخرج النسخة عن النطاق لإظهار لحظة تنفيذ رست للخاصية drop. اسم الملف: src/main.rs struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("my stuff"), }; let d = CustomSmartPointer { data: String::from("other stuff"), }; println!("CustomSmartPointers created."); } [الشيفرة 15: بنية CustomSmartPointer التي تنفّذ السمة Drop عند مكان وضع شيفرة تحرير الذاكرة] السمة Drop مضمّنة في المقدمة لذا لا نحتاج لأن نضيفها إلى النطاق. ننفّذ سمة Drop على CustomSmartPointer ونقدّم تنفيذًا لتابع drop الذي يستدعي بدوره println!‎، ونضع في متن دالة drop أي منطق نريد تنفيذه عند تخرج نسخة من النوع خارج النطاق، كما أننا نطبع نصًا هنا لنوضح كيف تستدعي رست drop بصريًا. أنشأنا في main نسختين من CustomSmartPointer ومن ثم طبعنا CutsomSmartPointers created، سيخرج CustomSmartPointer بنهاية main خارج النطاق، مما يعني أن رست ستستدعي الشيفرة التي وضعناها في تابع drop مما سيتسبب بطباعة رسالتنا النهائية، مع ملاحظة بأننا لم نستدعي التابع drop صراحةً. نحصل على الخرج التالي عندما ننفذ هذا البرنامج: $ cargo run Compiling drop-example v0.1.0 (file:///projects/drop-example) Finished dev [unoptimized + debuginfo] target(s) in 0.60s Running `target/debug/drop-example` CustomSmartPointers created. Dropping CustomSmartPointer with data `other stuff`! Dropping CustomSmartPointer with data `my stuff`! استدعت رست drop تلقائيًا عندما خرجت نُسخنا عن النطاق واستدعى drop بدوره الشيفرة البرمجية التي حددناها. تُحرَّر المتغيرات عكس ترتيب انشائها لذا تُحرَّر d قبل c. يهدف هذا المثال إلى منحك دليلًا بصريًا مُلاحَظ لكيفية عمل التابع drop، إذ أنك تحدّد عادةً شيفرة تحرير الذاكرة التي تحتاجها بدلاً من طباعة رسالة. تحرير قيمة مبكرًا باستخدام دالة std::mem::drop لسوء الحظ، ليس من السهل تعطيل خاصية drop التلقائية، إلا أن تعطيل drop ليس ضروريًا عادةً، إذ أن الهدف من سمة Drop هي أنها تحدث تلقائيًا. نريد أحياناً تحرير قيمة ما مبكراً ومثال على ذلك هو استخدام المؤشرات الذكية لإدارة الأقفال، إذ قد تضطر لإجبار تابع drop على تحرير القفل لتستطيع الشيفرة البرمجية الموجودة في النطاق ذاته الحصول عليه. لا تتيح لك راست استدعاء تابع drop الخاص بسمة Drop يدويًا، بل يجب عليك بدلاً من ذلك استدعاء دالة std::mem::drop المضمّنة في المكتبة القياسية، إذا أردت تحرير قيمة قسريًا قبل أن تخرج عن نطاقها. نحصل على خطأ تصريفي إذا أردنا استدعاء التابع drop الخاص بسمة drop يدويًا وذلك بتعديل دالة main من الشيفرة 15 كما هو موضح: اسم الملف: src/main.rs struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("some data"), }; println!("CustomSmartPointer created."); c.drop(); println!("CustomSmartPointer dropped before the end of main."); } [الشيفرة 15: محاولة استدعاء تابع drop من السمة Drop يدوياً لتحرير الذاكرة المبكر] نحصل على الخطأ التالي عندما نحاول تصريف الشيفرة البرمجية السابقة: $ cargo run Compiling drop-example v0.1.0 (file:///projects/drop-example) error[E0040]: explicit use of destructor method --> src/main.rs:16:7 | 16 | c.drop(); | --^^^^-- | | | | | explicit destructor calls not allowed | help: consider using `drop` function: `drop(c)` For more information about this error, try `rustc --explain E0040`. error: could not compile `drop-example` due to previous error تبيّن لنا رسالة الخطأ هذه أنه من غير المسموح لنا استدعاء drop صراحةً. تستخدم رسالة الخطأ المصطلح مُفكّك destructor وهو مصطلح برمجي عام لدالة تنظف نسخة ما؛ والمفكّك هو مصطلح معاكس للباني constructor وهو الذي ينشئ نسخةً ما، ودالة drop في رست هي نوع من أنواع المفكّكات. لا تسمح لنا رست باستدعاء drop صراحةً لأن رست ستستدعي تلقائيًا التابع drop على القيمة في نهاية الدالة main مما سيسبب خطأ التحرير المزدوج double free لأن رست سيحاول تحرير القيمة ذاتها مرتين. لا يمكننا تعطيل إدخال drop التلقائي عندما تخرج قيمة ما عن النطاق، ولا يمكننا استدعاء التابع drop صراحةً، لذا نحن بحاجة لإجبار القيمة على أن تُنظف مبكرًا باستخدام الدالة std::mem::drop. تعمل دالة std::mem::drop بصورةٍ مختلفة عن التابع drop في سمة Drop، إذ نستدعيها بتمرير القيمة التي نريد تحريرها قسريًا مثل وسيط argument. الدالة مضمّنة في البداية لذا يمكننا تعديل الدالة main في الشيفرة 15 بحيث تستدعي الدالة drop كما في الشيفرة 16: اسم الملف: src/main.rs struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("some data"), }; println!("CustomSmartPointer created."); drop(c); println!("CustomSmartPointer dropped before the end of main."); } [الشيفرة 16: استدعاء std::mem::drop لتحرير القيمة صراحةً قبل الخروج من النطاق] ينتج الخرج الآتي عن تنفيذ الشيفرة السابقة: $ cargo run Compiling drop-example v0.1.0 (file:///projects/drop-example) Finished dev [unoptimized + debuginfo] target(s) in 0.73s Running `target/debug/drop-example` CustomSmartPointer created. Dropping CustomSmartPointer with data `some data`! CustomSmartPointer dropped before the end of main. يُطبع النص "Dropping CustomSmartPointer with data some data!‎" بين النصين ".CustomSmartpointer created" و "CustomSmartPointer dropped before the end of main"، ويدلّ ذلك إلى أن شيفرة التابع drop استُدعيت لتحرير c في تلك النقطة. يمكنك استخدام شيفرة برمجية مخصصة في تطبيق سمة drop بطرق عديدة وذلك بهدف جعل عملية تحرير الذاكرة سهلة ومريحة، إذ يمكنك مثلًا استخدامها لإنشاء مُخَصص ذاكرة memory allocator خاص بك. ولا داعي لتذكر عملية تحرير الذاكرة مع سمة Drop وفي نظام رست للملكية وذلك لأن رست تفعل ذلك تلقائيًا، ولا داعي أيضًا للقلق من المشاكل الناتجة في حال تحرير قيم لا تزال قيد الاستخدام، إذ يضمن نظام الملكية أن المراجع صحيحة وأن drop يُستدعى فقط عندما تكون القيمة غير مستخدمة بعد الآن. الآن بعد أن رأينا Box<T>‎ وبعض من خصائص المؤشرات الذكية، لنرى بعض المؤشرات الذكية الأخرى المُعرفة في المكتبة القياسية. ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: المؤشر Rc<T>‎ الذكي واستخدامه للإشارة إلى عدد المراجع في لغة رست Rust المقال السابق: معاملة المؤشرات الذكية Smart Pointers مثل مراجع نمطية Regular References باستخدام سمة Deref في لغة رست كتابة برنامج سطر أوامر Command Line بلغة رست Rust المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست
  5. تلميحات الأنواع هي موجّهات directives يمكن إضافتها في الشيفرة المصدرية لبايثون لتحديد أنواع البيانات للمتغيرات والمعاملات والقيم المُعادة، وهذا يسمح لأدوات تحليل الشيفرة الساكنة بالتأكد من أن الشيفرة الخاصة بك لا تُتسبب بأي استثناءات exceptions بسبب قيم ذات نوع خاطئ. ظهرت تلميحات الأنواع أول مرة في إصدار بايثون 3.5 ولكن بما أنها مبنية على التعليقات فيمكن استخدامها مع أي إصدار بايثون. ما هي تلميحات النوع؟ معظم اللغات البرمجية صارمة بضبط أنواع البيانات وتسمى ثابتة الأنواع static typing أي أنه يجب على المبرمجين تحديد أنواع البيانات صراحةً لكل من المتغيرات والمُعاملات والقيم المُعادة في الشيفرة المصدرية، مما يسمح للمفسّر أو المصرّف التحقق من أن الشيفرة تستخدم كل الكائنات بصورةٍ صحيحة قبل تنفيذ البرنامج. على الجانب الآخر، هنالك لغات برمجة -ومنها لغة بايثون- متساهلة في تحديد أنواع البيانات وتُسمى متغيرة أو ديناميكية الأنواع dynamic typing أي يُمكن أن تكون المتغيرات والمُعاملات والقيم المُعادة من أي نوع من البيانات ويمكن أن تغير نوعها عند تنفيذ البرنامج. تكون اللغات الديناميكية غالبًا أكثر سهولة للبرمجة عليها لأنها لا تحتاج كثيرًا إلى تحديد صريح لأنواع البيانات، إلا أنها تفتقد ميزات منع الأخطاء التي تكون موجودة في اللغات ثابتة الأنواع. عندما تكتب سطرًا من شيفرة بايثون مثل round('forty two')‎ لا تلاحظ أنك تمرر سلسلة نصية إلى دالة تقبل نوع البيانات عدد صحيح int أو عدد عشري float فقط حتى تُنفذ الشيفرة وتسبب خطأ، بينما تعطي اللغات ثابتة الأنواع تحذيرًا مسبقًا إذا أردت تعيين قيمة أو تمرير وسيط من النوع الخاطئ. تقدم تلميحات الأنواع الخاصة بلغة بايثون خيار كتابة الشيفرة بأنواع ثابتة، وتكون التلميحات بالخط الغامق كما في المثال التالي: def describeNumber(number: int) -> str: if number % 2 == 1: return 'An odd number. ' elif number == 42: return 'The answer. ' else: return 'Yes, that is a number. ' myLuckyNumber: int = 42 print(describeNumber(myLuckyNumber)) تستخدم تلميحات الأنواع النقطتين في المُعاملات والمتغيرات لفصل الاسم عن النوع ولكن بالنسبة للقيم المُرجعة يستخدم تلميح النوع السهم (‎->‎) للفصل بين أقواس تغليف تعبير def عن النوع. يلمّح نوع الدلة describeNumber()‎ أنها تأخذ قيمة عدد صحيح من أجل مُعامل number وتعيد قيمة سلسلة نصية. لا تحتاج إلى تطبيق تلميحات النوع إذا استخدمتها في مكان ما على كل جزء من البيانات في برنامجك، لكن استخدم بدلًا من ذلك طريقة كتابة تدريجية gradual typing وهو أسلوب كتابة وسطي يمنحك مرونة الكتابة الديناميكية وأمان الكتابة الساكنة وبهذا يمكننا إضافة تلميحات الأنواع لأنواع معينة من المتغيرات والمعاملات والقيم المُرجعة، إلا أنه كلما زادت تلميحات الأنواع في برنامجك كلما زادت المعلومات التي تأخذها أدوات تحليل الشيفرة الساكنة لمتابعة المشاكل المحتملة في البرنامج. لاحظ في المثال السابق أن أسماء الأنواع المحددة تطابق أسماء الدوال البانية int()‎ و str()‎، للصنف والنوع ونوع البيانات المعنى ذاته في بايثون، إذ تجد في أي نسخة مكونة من أصناف أن اسم الصنف مماثلٌ لاسم النوع: import datetime 1 noon: datetime.time = datetime.time(12, 0, 0) class CatTail: def __init__(self, length: int, color: str) -> None: self.length = length self.color = color 2 zophieTail: CatTail = CatTail(29, 'grey') لدى المتغير noon تلميح النوع datetime.time (سطر 1) لأنه كائن time (المعرف في وحدة datetime)، وللكائن zophieTail أيضًا نوع التلميح CatTail (سطر 2)، وذلك لأنه كائن لصنف CatTail الذي أنشأناه بتعليمة class. تُطبَّق تلميحات الأنواع تلقائيًا لكل الأصناف الفرعية للنوع المحدد، فمثلًا يمكن ضبط متغير تلميح النوع dict لأي قيمة مسار وأيضًا لأي قيم collection.OrderedDict و collection.defaultdict لأن هذه الأصناف هي أصناف فرعية للصنف dict. سنتكلم لاحقًا عن الأصناف الفرعية بتفصيل أكبر. لا تحتاج عادةً أدوات تحقق النوع الساكنة إلى تلميحات أنواع للمتغيرات، لأن أدوات التحقق من أنواع البيانات تستدلّ على النوع type inference من تعبير الإسناد الأول للمتغير. على سبيل المثال، يمكن لمتحقق النوع الاستدلال بأن spam في السطر spam = 42 يجب أن يحتوي تلميح نوع int، إلا أنه يُفضل ضبط تلميح نوع بكل الأحوال. أي تغيير مستقبلي للنوع float كما في spam = 42.0 سيسبب بتغيير في النوع المُستدلّ مما قد لا يكون متوافقًا مع رغبتك، إذ يُفضّل إجبار المبرمج ليغير تلميح النوع عند تغيير القيمة للتأكد أنه غيّر النوع متقصدًا بدلًا من تغيير عَرَضي خاطئ. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن استخدام محللات صارمة مع أنواع البيانات على الرغم من أن بايثون تدعم صياغة تلميحات الأنواع إلا أن مفسر بايثون يتجاهلها كليًّا؛ فإذا نفذت برنامج بايثون يمرر متغيرًا من نوع غير صالح للدالة، فستتصرف بايثون كأن تلميحات النوع غير موجودة. بمعنى آخر، لا تتسبب تلميحات النوع بأن يجري مفسر بايثون أي تحقق من الأنواع وقت التنفيذ، إذ أنها موجودةٌ فقط لتخدم أدوات التحقق من أنواع البيانات التي تحلل الشيفرة قبل تنفيذ البرنامج وليس أثناء تنفيذ البرنامج. نسمي هذه الأدوات بأدوات التحليل الثابتة static analyzers tools، لأنها تحلل الشيفرة المصدرية قبل تنفيذ البرنامج، بينما تحلّل أدوات التحليل وقت التنفيذ runtime analysis tools أو أدوات التحليل الديناميكي dynamic analysis tools البرامج وقت التنفيذ. قد تبدو لك الأمور مثيرة للحيرة الآن، إذ تشير الكلمتين ساكن وديناميكي إلى تنفيذ البرنامج بينما تشير الكتابة ثابتة الأنواع static typing ومتغيرة الأنواع dynamic typing إلى كيفية التصريح عن أنواع البيانات للمتغيرات والدوال. بايثون لغةٌ تُكتب ديناميكيًا ولديها أدوات تحليل ساكنة مثل Mypy مكتوبةٌ لأجلها. تثبيت وتشغيل Mypy على الرغم من أن بايثون لا تحتوي على أداة تحقق من الأنواع رسميًا، إلا أن Mypy تُعدّ أشهر أداة خارجية للتحقق من النوع، ويمكنك تثبيتها باستخدام pip عن طريق تنفيذ الأمر التالي: python -m pip install -user mypy شغل python3 بدلًا من python على ماك أو إس macOS ولينكس Linux. تتضمن أدوات التحقق من النوع الأُخرى المعروفة Pyright الخاص بمايكروسوفت و Pyre الخاص بفيسبوك و Pytype الخاص بجوجل. لتشغيل متحقق النوع، افتح سطر الأوامر أو نافذة طرفية ونفّذ أمر python -m mypy لتنفيذ الوحدة مثل تطبيق، ومرّر اسم ملف شيفرة بايثون ليتحقق منه. نتحقق في هذا المثال من الشيفرة لبرنامج مثال أُنشئ في ملف اسمه example.py: C:\Users\Al\Desktop>python –m mypy example.py Incompatible types in assignment (expression has type "float", variable has type "int") Found 1 error in 1 file (checked 1 source file) لا يخرِج متحقق النوع شيئًا إذا لم توجد أي مشكلة ويطبع رسائل خطأ عدا ذلك، ففي هذا المثال هناك مشكلة في ملف example.py في السطر 171 لأن المتغير المُسمى spam لديه تلميح نوع int ولكن تُسند إليه قيمة float، وقد يسبب هذا فشلًا ويجب التحقق منه. قد تكون بعض رسائل الخطأ صعبة الفهم للوهلة الأولى، ويمكن أن يعطي Mypy عددًا كبيرًا من الأخطاء المحتملة أكثر مما نستطيع ذكره هنا. أسهل طريقة لمعرفة ماذا يعني كل خطأ هو البحث عنه على الويب، ويمكنك البحث عن شيء شبيه بما يلي: " أنواع الإسناد غير المتوافقة في Mypy" أو Mypy incompatible types in assignment. يُعد تنفيذ Mypy من سطر الأوامر في كل مرة تغيّر فيها الشيفرة أمرًا غير فعال، وللحصول على استخدام أفضل من متحقق النوع يجب عليك ضبط بيئة التطوير المتكاملة IDE أو محرر النصوص الخاص بك ليعمل في الخلفية، فبهذه الطريقة سيبقى المحرر ينفّذ Mypy أثناء كتابة الشيفرة ويظهر أي أخطاء في المحرر. يبين الشكل 1 الأخطاء من المثال السابق في محرر النصوص Sublime Text. [الشكل 1: إظهار محرر النصوص Sublime Text للأخطاء من Mypy] تختلف خطوات ضبط محرر النصوص أو بيئة التطوير المتكاملة للعمل على Mypy اعتمادًا على أي محرر نصوص أو بيئة تطوير متكاملة مُستخدمة. يمكنك إيجاد الخطوات على الويب عن طريق البحث عن "ضبط Mypy على <اسم بيئة التطوير المتكاملة الخاصة بك>" أو "ضبط تلميحات النوع على <اسم بيئة التطوير المتكاملة الخاصة بك>" أو ما شابه، وإذا فشل كل شيء أخر يمكنك دائما تنفيذ Mypy من سطر الأوامر أو نافذة الطرفية. إعلام Mypy بتجاهل الشيفرة ربما تريد كتابة شيفرة ولا ترغب بأن ترى تحذيرات تلميح النوع لهذه الشيفرة، ولربما يظهر لأداة التحليل الساكنة أن السطر يستخدم النوع الخاطئ ولكنه يكون صالحًا وقت تنفيذ البرنامج. يمكنك تجاهل أي تحذير تلميح نوع بإضافة تعليق ‎# type: ignore في نهاية السطر، إليك مثالًا عن ذلك: def removeThreesAndFives(number: int) -> int: number = str(number) # type: ignore number = number.replace('3', '').replace('5', '') # type: ignore return int(number) يمكننا ضبط متغير العدد الصحيح إلى سلسلة نصية مؤقتًا لإزالة كل أرقام 3 و5 من العدد الصحيح المُمرر إلى removeThreesAndFives()‎، يتسبب ذلك بتحذير متحقق النوع من أول سطرين من الدالة لذا سنضيف تلميح النوع ‎# ‎type: ignore لهذه الأسطر لتجاهل تحذيرات متحقق النوع. استخدم ‎# type: ignore باعتدال، إذ يفتح تجاهل التحذيرات من متحقق النوع المجال للأخطاء بأن تتسلل إلى الشيفرة الخاصة بك، ويمكنك دائمًا إعادة كتابة الشيفرة الخاصة بك حتى لا تظهر هذه التحذيرات، فمثلًا إذا أنشأنا متغيرًا باستخدام numberAsStr = str(number)‎ أو بدلنا الأسطر الثلاثة باستخدام سطر شيفرة واحد كما يلي: return int(str(number.replace('3', '').replace('5', '')))‎ يمكننا تجنب استخدام المتغير number في عدة أنواع. لا نريد هنا تجاهل التحذيرات عن طريق تغيير تلميح النوع للمُعامل إلى union[int, str]‎ لأن هدف المعامل هو السماح بالأعداد الصحيحة فقط. ضبط تلميحات النوع لأنواع متعددة يمكن أن تحتوي متغيرات ومعاملات والقيم المُعادة الخاصة بلغة بايثون على العديد من أنواع البيانات، ولملائمة ذلك يمكن تحديد تلميحات الأنواع عن طريق استيراد Union من الوحدة المضمنة typing وتحديد مجال الأنواع داخل الأقواس المربعة بعد اسم الصنف Union. from typing import Union spam: Union[int, str, float] = 42 spam = 'hello' spam = 3.14 في هذا المثال، يحدد تلميح النوع Union[int, str, float]‎ أنه يمكن ضبط spam إلى عدد صحيح أو سلسلة نصية أو رقم عشري. لاحظ أنه من الأفضل استخدام الشكل from typing import x من تعبير import بدلًا من الشكل import typing ومن ثم استخدام الكتابة المطولة باستمرار من أجل تلميحات النوع في كل البرنامج. يمكن تحديد أنواع بيانات متعددة في المواقف التي يمكن أن تعيد فيها المتغيرات أو القيم المُعادة قيمة None بالإضافة إلى نوع آخر. ضع None داخل أقواس معقوفة بدلًا من NoneType لإضافة NoneType الذي هو نوع القيمة None في تلميح النوع. تقنيًا، ليس NoneType معرّفًا مبنيًا مسبقًا built-in identifier كما هو الحال مع int أو str. الأفضل من ذلك، بدلًا من استخدام Union[str, None]‎ على سبيل المثال، يمكنك استيراد Optional من الوحدة Optional[str]‎؛ يعني تلميح النوع هذا أن التابع أو الدالة قد تُعيدان None بدلًا من القيمة للنوع المتوقع، إليك مثالًا عن ذلك: from typing import Optional lastName: Optional[str] = None lastName = 'Sweigart' يمكن ضبط المتغير lastName في هذا المثال إلى قيمة None أو str، ولكن من الأفضل التقليل من استخدام Union و Optimal، إذ كلما قل عدد الأنواع التي تسمح بها المتغيرات والدوال كلما كانت الشيفرة أبسط، والشيفرة البسيطة أقل عرضةً للأخطاء من الشيفرة المعقدة. تذكر الحكمة الفضلى في بايثون أن البسيط أفضل من المعقد، فمن أجل الدوال التي تُعيد None للإشارة إلى خطأ ما، جرّب استخدام استثناء بدلًا من ذلك. يمكنك استخدام تلميح النوع Any (أيضًا من وحدة typing) لتحديد أن المتغير أو المعامل أو القيمة المُعادة يمكن أن تكون من أي نوع بيانات: from typing import Any import datetime spam: Any = 42 spam = datetime.date.today() spam = True يسمح تلميح النوع Any في هذا المثال بضبط المتغير spam إلى قيمة من أي نوع من البيانات، مثل int أو datetime.date أو bool، كما يمكنك أيضًا استخدام object مثل تلميح نوع لأن هذا هو الصنف الأساسي لكل أنواع البيانات في بايثون، ولكن Any هو تلميح مفهوم أكثر من object. استخدم Any باعتدال كما هو الأمر مع Union و Optional، إذ أنك ستفقد مزايا التحقق من النوع إذا ضبطت كل المتغيرات والمعاملات والقيم المرجعة الخاصة بك إلى نوع التلميح Any. الفرق بين تحديد تلميح النوع Any وعدم تحديد أي تلميح نوع هو أن Any توضح صراحةً أن المتغير أو التابع يقبل القيم من أي نوع، بينما يشير غياب تلميح النوع إلى أن المتغير أو التابع لم يُلمَّح لنوعه بعد. ضبط تلميحات النوع لكل من القوائم والقواميس وغيرها يمكن للقوائم lists والقواميس dictionaries والصفوف tuples والمجموعات sets وحاويات البيانات الأخرى أن تحتفظ بقيم أخرى، فإذا حددت list على أنها تلميح النوع للمتغير، يجب على هذا المتغير أن يحتوي على قائمة، إلا أنه من الممكن أن تحتوي تلك القائمة على قيمة من أي نوع. لا تسبب الشيفرة التالية أي اعتراضات من متحقق النوع: spam: list = [42, 'hello', 3.14, True] يجب استخدام تلميح النوع List الخاص بالوحدة typing للتصريح بالتحديد عن أنواع البيانات داخل القائمة. لاحظ أن List مكتوبةٌ بحرف كبير "L" لتمييزها عن نوع البيانات list: from typing import List, Union 1 catNames: List[str] = ['Zophie', 'Simon', 'Pooka', 'Theodore'] 2 numbers: List[Union[int, float]] = [42, 3.14, 99.9, 86] يحتوي المتغير catNames في هذا المثال على قائمة من السلاسل النصية، لذا نضبط بعد استيراد List من الوحدة typing تلميح النوع إلى List[str]‎ (السطر 1)، وينظر متحقق النوع إلى أي استدعاء للتابعين append()‎ أو insert()‎ أو أي شيفرة أخرى تضع قيمة غير السلسلة النصية في القائمة. يمكننا ضبط تلميح النوع باستخدام Union إذا احتوت القائمة على أنواع متعددة، إذ يمكن مثلًا للقائمة numbers أن تحتوي على قيم ذات عدد صحيح أو عدد عشري، لذا نضبط تلميح النوع إلى ‎List[Union[int, float]]‎ (السطر 2). لدى وحدة typing اسم بديل آخر لكل نوع حاوية، إليك قائمةً بأسماء الأنواع البديلة لأنواع الحاويات المعروفة في بايثون: List هي لنوع البيانات list. Tuple هي لنوع البيانات tuple. Dict هي لنوع البيانات القاموس dict. Set هي لنوع البيانات set. FrozenSet هي لنوع البيانات frozenset. Sequence هي لنوع البيانات list و tuple وأي نوع آخر من أنواع البيانات المتسلسلة. Mapping هي لنوع البيانات القاموس dict و set و frozenset وأي نوع آخر من أنواع بيانات الربط. ByteString هي لأنواع bytes و bytearray و memoryview. إليك قائمة كاملة للأنواع. النقل العكسي لتلميحات النوع باستخدام التعليقات النقل العكسي Backporting هي عملية أخذ الميزات من إصدار جديد من البرنامج ونقلها (أي تكييفها وإضافتها) إلى نسخة أقدم. ميزة تلميحات النوع لبايثون جديدةٌ للإصدار 3.5 ولكن يمكن لشيفرة بايثون التي تُنفذ بإصدار مفسر أقدم من 3.5 استخدام تلميحات النوع بوضع معلومات النوع في التعليقات. استخدم التعليق السطري بعد تعبير الإسناد من أجل المتغيرات، ومن أجل الدوال والتوابع اكتب تلميح النوع على السطر الذي يلي تعبير def. ابدأ بالتعليق باستخدام type متبوعًا بنوع البيانات. إليك مثالًا عن شيفرة تحتوي على تلميحات النوع في التعليقات: 1 from typing import List 2 spam = 42 # type: int def sayHello(): 3 # type: () -> None """The docstring comes after the type hint comment.""" print('Hello!') def addTwoNumbers(listOfNumbers, doubleTheSum): 4 # type: (List[float], bool) -> float total = listOfNumbers[0] + listOfNumbers[1] if doubleTheSum: total *= 2 return total لاحظ أنه حتى مع استخدامك لتنسيق التعليق الخاص بتلميح النوه فأنت لا تزال بحاجة لاستيراد الوحدة typing (سطر 1)، إضافةً لأي نوع اسم بديل تستخدمه في التعليقات. لا تحتوي الإصدارات الأقدم من 3.5 وحدة typing في مكتبتها الافتراضية، لذا عليك تثبيت typing بصورةٍ منفصلة عن طريق تنفيذ هذا الأمر: python -m pip install --user typing شغل python3 بدلًا من python في ماك أو إس ولينكس. لضبط المتغير spam إلى عدد صحيح نضيف ‎# ‎type: int مثل تعليق بنهاية السطر (سطر 2). من أجل الدوال يجب أن يضم التعليق قوسين مع فاصلة تفصل قتئمة تلميح الأنواع بنفس ترتيب المعاملات. يجب أن يكون للدوال التي تحتوي على صفر معامل قوسين فارغين (سطر 3)، كما يجب الفصل بين المعاملات المتعددة إذا وجدت داخل القوسين بفواصل (سطر 4). تنسيق التعليق ذي تلميح النوع أصعب للقراءة من التنسيق العادي، لذا استخدمه فقط من أجل الشيفرة التي تنفذها إصدارات بايثون أقدم من 3.5. ترجمة -وبتصرف- لقسم من الفصل COMMENTS, DOCSTRINGS, AND TYPE HINTS من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: التعليقات Comments وأنواعها في لغة بايثون أساسيات البرمجة بلغة بايثون الدليل السريع إلى لغة البرمجة بايثون
  6. يسمح لك تطبيق سمة Deref بتخصيص سلوك عامل التحصيل dereference operator * (انتبه عدم الخلط مع عامل عمليات الضرب أو عامل glob) يمكننا عند تطبيق deref بطريقة معينة تسمح بمعاملة المؤشرات الذكية smart pointers مثل مراجع نمطية، كتابة الشيفرة البرمجية بحيث تعمل على المراجع وتُستخدم بالمؤشرات الذكية أيضًا. لننظر أولًا إلى كيفية عمل عامل التحصيل مع المراجع النمطية regular references، ومن ثم سنحاول تعريف نوع مخصص يتصرف مثل Box<T>‎، وسنرى سبب عدم عمل عامل التحصيل مثل مرجع على النوع المخصص المعرف حديثاً. سنكتشف كيف يسمح تطبيق سمة Deref للمؤشرات الذكية بأن تعمل بطريقة مماثلة للمراجع، ثم سننظر إلى ميزة التحصيل القسري deref coercion في رست وكيف تسمح لنا بالعمل مع المراجع أو المؤشرات الذكية. ملاحظة: هناك فرق كبير بين النوع MyBox<T>‎ الذي سننشئه و Box<T>‎ الحقيقي: إذ لن يخزن إصدارنا منه البيانات على الكومة heap، وسنركز في مثالنا هذا على السمة deref، فمكان تخزين البيانات ليس مهمًا بقدر أهمية السلوك المشابه لسلوك المؤشر. تتبع المؤشر للوصول إلى القيمة المرجع النمطي هو نوع من المؤشرات، ويمكنك التفكير بالمؤشر على أنه سهم يشير إلى قيمة مخزنة في مكان آخر. أنشأنا في الشيفرة 6 مرجعًا إلى قيمة من النوع i32 ومن ثم استخدمنا عامل التحصيل لتتبع هذا المرجع وصولًا إلى القيمة. اسم الملف: src/main.rs fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); } [الشيفرة 6: استخدام عامل التحصيل لتتبع المرجع وصولًا إلى قيمة من النوع i32] يخزّن المتغير x قيمةً من النوع i32 هي 5. ضبطنا قيمة y بحيث تساوي مرجعًا إلى المتغير x، ويمكننا التأكد أن x تساوي 5، ولكن إذا أردنا التأكد من قيمة y علينا استخدام ‎*y لتتبع المرجع وصولًا للقيمة التي يدل عليها لتحصيلها (هذا هو السبب في حصول عملية التحصيل على اسمها)، وذلك لكي يستطيع المصرّف أن يقارنها مع القيمة الفعلية. نستطيع الحصول على قيمة العدد الصحيح y بعد تحصيل y وهي القيمة التي تشير على ما يمكن مقارنته مع 5. نحصل على الخطأ التالي عند التصريف إذا حاولنا كتابة assert_eq!(5,y);‎: $ cargo run Compiling deref-example v0.1.0 (file:///projects/deref-example) error[E0277]: can't compare `{integer}` with `&{integer}` --> src/main.rs:6:5 | 6 | assert_eq!(5, y); | ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}` | = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}` = help: the following other types implement trait `PartialEq<Rhs>`: f32 f64 i128 i16 i32 i64 i8 isize and 6 others = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0277`. error: could not compile `deref-example` due to previous error مقارنة مرجع لرقم مع رقم غير مسموح لأنهما من نوعين مختلفين ويجب علينا استخدام عامل التحصيل لتتبع المرجع وصولًا إلى القيمة التي يشير إليها. استخدام Box‎ مثل مرجع يمكننا إعادة كتابة الشيفرة البرمجية في الشيفرة 6 باسخدام Box<T>‎ بدلاً من مرجع، وذلك عن طريق استخدام عامل التحصيل الذي استخدمناه على Box<T>‎ كما هو موضح في الشيفرة 7 بطريقة مماثلة لعمل عامل التحصيل المُستخدم في الشيفرة 6: اسم الملف:src/main.rs fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); } [الشيفرة 7: استخدام عامل التحصيل على Box<i32>‎] الفرق الأساسي بين الشيفرة 7 والشيفرة 6 هو أننا حددنا y هنا لتكون نسخةً instance عن Box<T>‎ وتشير إلى قيمة منسوخة من x بدلًا من أن تكون مرجعًا يشير إلى قيمة x، ويمكننا في التوكيد assertion الأخير استخدام عامل التحصيل لتتبُّع مؤشر Box<T>‎ بالطريقة ذاتها التي اتبعناها عندما كان المرجع هو y. سنبحث تاليًا عن الشيء الذي يميّز Box<T>‎ ليسمح لنا باستخدام عامل التحصيل بتعريف نوع خاص بنا. تعريف المؤشر الذكي الخاص بنا دعنا نبني مؤشرًا ذكيًا خاصًا بنا بصورةٍ مماثلة للنوع Box<T>‎ الذي تزودنا به المكتبة القياسية لملاحظة كيف أن المؤشرات الذكية تتصرف على نحوٍ مختلف عن المراجع افتراضيًا، وسننظر بعدها إلى كيفية إضافة قدرة استخدام عامل التحصيل. النوع Box<T>‎ معرفٌ مثل هيكل صف tuple struct بعنصر واحد، لذا نعرّف في الشيفرة 8 نوع MyBox<T>‎ بالطريقة ذاتها، كما نعرف أيضاً دالة new لتطابق الدالة new المعرفة في Box<T>‎. اسم الملف: src/main.rs struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() {} [الشيفرة 8: تعريف النوع MyBox<T>‎] نعرف بنية بالاسم MyBox ونعرف معاملًا مُعمّمًا ‎‏generic ‏ ‎T‏ لأننا نريد لنوعنا أن يحتفظ بكل أنواع القيم. نوع MyBox هو صف tuple بعنصر واحد من النوع T. تأخذ دالة MyBox::new معاملًا واحدًا من النوع T وتُعيد نسخةً من MyBox تحتفظ بالقيمة المُمرّرة. لنجرب إضافة دالة main الموجودة في الشيفرة 7 إلى الشيفرة 8 ونعدلّها بحيث تستخدم النوع MyBox<T>‎ الذي عرفناه بدلًا من Box<T>‎. لن تُصّرف الشيفرة 9 لأن رست لا تعرف كيفية تحصيل قيمة MyBox. اسم الملف: src/main.rs struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); } [الشيفرة 9: محاولة استخدام Mybox<T>‎ بطريقة استخدام المراجع وBox<T>‎] إليك الخطأ التصريفي الناتج عن الشيفرة السابقة: $ cargo run Compiling deref-example v0.1.0 (file:///projects/deref-example) error[E0614]: type `MyBox<{integer}>` cannot be dereferenced --> src/main.rs:14:19 | 14 | assert_eq!(5, *y); | ^^ For more information about this error, try `rustc --explain E0614`. error: could not compile `deref-example` due to previous error لا يمكن تحصيل النوع MyBox<T>‎ الخاص بنا لأننا لم نطبق هذه الميزة على نوعنا، ولتطبيق التحصيل باستخدام العامل *، نطبّق السمة Deref. معاملة النوع مثل مرجع بتطبيق السمة Deref لتطبيق السمة نحن بحاجة تأمين تطبيقات لتوابع السمة المطلوبة كما ذكرنا سابقًا، إذ تحتاج السمة Deref الموجودة في المكتبة القياسية لتطبيق تابع واحد اسمه deref يستعير self ويعيد مرجعًا للبيانات الداخلية. تحتوي الشيفرة 10 على تطبيق Deref لإضافة تعريف MyBox: اسم الملف: src/main.rs use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); } [الشيفرة 10: تطبيق Deref على MyBox<T>‎] نعرّف في السطر type Target = T;‎ نوعًا مرتبط associated type لسمة Deref لتستخدمه، وتختلف الأنواع المرتبطة قليلًا في تعريف المعاملات المعمّمة، ولكن لا داعي للقلق بخصوصها حاليَا إذ سنتطرق لهذا الموضوع لاحقًا. نكتب في متن التابع deref المرجع ‎&self.0 بحيث يُعيد deref مرجعًا للقيمة التي نريد الوصول إليها باستخدام العامل * (تذكر سابقًا أن ‎.0 تصل إلى القيمة الأولى في هيكل الصف). تستطيع الدالة main في الشيفرة 9 التي تستدعي * على القيمة MyBox<T>‎ أن تُصرّف الآن مع نجاح التأكيدات. يستطيع المصرّف أن يحصّل المراجع & فقط بدون سمة Deref، إذ يعطي تابع deref المصرف القدرة على أن يأخذ القيم من أي نوع يطبق Deref وأن يستدعي التابع deref للحصول على مرجع & يعرف كيفية تحصيله. عندما أدخلنا ‎*y في الشيفرة 9، نفذت رست الشيفرة التالية خلف الكواليس: *(y.deref()) تستبدل رست العامل * باستدعاء للتابع deref ومن ثم إلى تحصيل عادي حتى لا نحتاج إلى التفكير فيما إذا كنا نريد استدعاء تابع deref، وتسمح لنا ميزة رست هذه بكتابة شيفرة برمجية تعمل بالطريقة نفسها سواءً أكان لدينا مرجع عادي أو نوع يطبق Deref. يعود السبب في إعادة تابع deref المرجع إلى قيمة وأن التحصيل العادي خارج القوسين في ‎*(y.deref())‎ لا يزال ضروريًا إلى نظام الملكية؛ فإذا أعاد التابع deref القيمة مباشرة بدلاً من مرجع للقيمة فإن القيمة ستنتقل خارج self، ولا نريد أن نأخذ ملكية القيمة الداخلية في MyBox<T>‎ في هذه الحالة وفي معظم الحالات التي نستخدم فيها معامل التحصيل. لاحظ أن المعامل * يُستبدل باستدعاءٍ للتابع deref ثم استدعاء للمعامل * مرةً واحدةً وذلك في كل مرة نستخدم * في شيفرتنا البرمجية. ينتهي بنا المطاف بقيمة من النوع i32 تُطابق "5" في assert_eq!‎ في الشيفرة 9 وذلك لأن عملية استبدال المعامل * لا تُنفّذ إلى ما لا نهاية. التحصيل القسري الضمني مع الدالات والتوابع يحوّل التحصيل القسري المرجع من نوع يطبق السمة Deref إلى مرجع من نوع آخر، فمثلاً يحوّل التحصيل القسري ‎&String إلى ‎&str لأن String يطبق السمة Deref بطريقة تُعيد ‎&str. التحصيل القسري هو عملية ملائمة في رست تُجرى على وسطاء arguments الدوال والتوابع وتعمل فقط على الأنواع التي تطبق السمة Deref، وتحصل هذه العملية تلقائيًا عندما نمرر مرجعًا لقيمة ذات نوع معين مثل وسيط لدالة أو تابع لا يطابق نوع المعامل في تعريف الدالة أو التابع. تحوِّل سلسلةً من الاستدعاءات إلى التابع deref النوع المُقدم إلى نوع يحتاجه المعامل. أُضيف التحصيل القسري إلى رست بحيث لا يضطر المبرمجون الذين يكتبون استدعاءات لدالات وتوابع إلى إضافة العديد من المراجع الصريحة والتحصيلات باستخدام & و *، كما تسمح لنا خاصية التحصيل القسري أيضاً بكتابة شيفرة برمجية تعمل لكل من المراجع أو المؤشرات الذكية في الوقت ذاته. لمشاهدة عمل التحصيل القسري عمليًا، نستخدم النوع MyBox<T>‎ الذي عرفناه في الشيفرة 8 بالإضافة إلى تطبيق Deref الذي أضفناه في الشيفرة 10. توضح الشيفرة 11 تعريف دالة تحتوي على معامل شريحة سلسلة نصية string slice: اسم الملف: src/main.rs fn hello(name: &str) { println!("Hello, {name}!"); } fn main() {} [الشيفرة 11: الدالة hello التي تحتوي على معامل name من النوع ‎&str] بإمكاننا استدعاء الدالة hello باستخدام شريحة سلسلة نصية بمثابة وسيط مثل hello(“Rust”);‎. يجعل التحصيل القسري استدعاء hello مع مرجع للقيمة MyBox<String>‎ ممكنًا كما هو موضح في الشيفرة 12: اسم الملف: src/main.rs use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hello, {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&m); } [الشيفرة 12: استدعاء hello باستخدام مرجع إلى القيمة MyBox<String>‎ ويمكن تنفيذ ذلك بفضل التحصيل القسري] نستدعي هنا الدالة hello مع الوسيط ‎&m الذي يمثل مرجعًا إلى القيمة MyBox<String>‎، وتستطيع رست تحويل ‎&MyBox<String>‎ إلى ‎&String باستدعاء deref وذلك لأننا طبقنا السمة Deref على MyBox<T>‎ كما هو موضح في الشيفرة 10. تقّدم المكتبة القياسية تطبيقًا للسمة Deref على النوع String الذي يعيد لنا شريحة سلسلة نصية ويمكنك العثور على هذه التفاصيل في توثيق الواجهة البرمجية الخاصة بالسمة Deref. تستدعي رست التابع deref مجددًا لتحويل ‎&String إلى ‎&str الذي يطابق تعريف دالة hello. إذا لم تطبق رست التحصيل القسري فسيتوجب علينا كتابة الشيفرة 13 بدلًا من الشيفرة 12 لاستدعاء hello مع قيمة من النوع ‎&MyBox<String>‎. اسم الملف: src/main.rs use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hello, {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&(*m)[..]); } [الشيفرة 13: الشيفرة التي يجب علينا كتابتها إذا لم تحتوي رست على ميزة التحصيل القسري] يحصّل (m*) النوع MyBox<String>‎ إلى String ومن ثم إلى & وتأخذ [..] شريحة سلسلة نصية String تساوي قيمة السلسلة النصية كاملةً وذلك لمطابقة بصمة الدالة hello. ستكون هذه الشيفرة البرمجية صعبة القراءة والفهم بدون التحصيل القسري وذلك مع كل الرموز اللازمة، إذ يسمح التحصيل القسري للغة رست بمعالجة هذه التحويلات تلقائياً نيابةً عنّا. عندما تُعرَّف سمة Deref للأنواع المستخدمة، ستحلّل رست هذه الأنواع وتستخدم Deref::deref بعدد المرات اللازم لتحصل على مرجع يطابق نوع المعامل، ويُعرَّف عدد مرات إضافة Deref::deref اللازمة عند وقت التصريف لذا لا يوجد أي عبء إضافي عند وقت التشغيل runtime مع التحصيل القسري. كيفية تعامل التحصيل القسري مع قابلية التغيير يمكننا استخدام السمة DerefMut لتجاوز عمل العامل * على المراجع المتغيّرة mutable references بصورةٍ مشابهة لاستخدامنا لسمة Deref لتجاوز عمل العامل * على المراجع الثابتة immutable. تنفّذ رست عملية التحصيل القسري عندما تجد تطبيقات لأنواع وسمات في ثلاث حالات معيّنة: من ‎&T إلى ‎&U عندما T: Deref<Target=U>‎. من ‎&mut T إلى ‎&mut U عندما T: DerefMut<Target=U>‎. من ‎&mut T إلى ‎&U عندما T: Deref<Target=U>‎. الحالتان الأولى والثانية متماثلتان مع فرق أن الثانية هي تطبيق لحالة متغيّرة، بينما تنصّ الحالة الأولى أنه إذا كان لديك ‎&T وتطبّق T سمة Deref لنوع ما من U، يمكن الحصول على ‎&U بوضوح، والحالة الثانية تشير إلى أن عملية التحصيل القسري ذاتها تحدث للمراجع المتغيّرة. تعدّ الحالة الثالثة أكثر تعقيدًا؛ إذ تجبر رست تحويل مرجع متغيّر إلى مرجع ثابت، إلا أن العكس غير ممكن، فالمراجع الثابتة لن تُحوّل قسريًا إلى مراجع متغيرة، وذلك بسبب قواعد الاستعارة، وإذا كان لديك مرجعًا متغيّرًا فإن هذا المرجع سيكون المرجع الوحيد لتلك البيانات (وإلا فلن يمكنك تصريف البرنامج). لن يكسرتحويل مرجع متغيّرإلى مرجع ثابت قواعد الاستعارة الافتراضية. تتطلب عملية تحويل المرجع الثابت إلى مرجع متغيّر أن يكون المرجع الثابت الابتدائي هو المرجع الوحيد الثابت للبيانات الخاصة به، إلا أن قوانين الاستعارة لا تضمن لك ذلك، وبالتالي لا يمكن لرست الافتراض بأن تحويل مرجع ثابت إلى مرجع متغيّر هي شيء ممكن. ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: تنفيذ شيفرة برمجية عند تحرير الذاكرة cleanup باستخدام السمة Drop في لغة رست المقال السابق: المؤشرات الذكية Smart Pointers في رست Rust المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست التحقق من المراجع References عبر دورات الحياة Lifetimes في لغة رست
  7. إن التعليقات comments والتوثيق documentation في الشيفرة المصدرية هي بأهمية الشيفرة البرمجية ذاتها، سبب ذلك هو أن عملية تطوير البرمجيات لا تنتهي أبدًا وتحتاج دائمًا للقيام بتغييرات، وذلك إما لإضافة ميّزات جديدة أو لإصلاح المشاكل، ولكن لا تستطيع تغيير الشيفرة إذا لم تفهمها لذا يجب ابقاءها بحالة صالحة للقراءة، كما كتب علماء الحاسوب هارولد ابلسون Harold Abelson وجيرالد جاي Gerald Jay وجولي سوسمان Julie Sussman: تسمح لك التعليقات -بالإضافة إلى سلاسل التوثيق النصية docstrings وتلميح الأنواع type hints التي سنناقشها لاحقًا- بالمحافظة على وضوح الشيفرة، إذ أن التعليقات هي شروحات بلغة بشرية قصيرة وبسيطة تُكتب مباشرة في الشيفرة المصدرية ويتجاهلها الحاسوب، تقدم التعليقات ملاحظات مساعدة وتحذيرات ورسائل تذكير للآخرين الذين لم يكتبوا الشيفرة، أو أحيانًا لمبرمجين الشيفرة في المستقبل. سأل معظم المبرمجون أنفسهم في وقتٍ ما "من كتب هذه الفوضى الغير مقروءة؟" فقط ليأتي الجواب ضمن التعليقات "أنا!". تركز هذه المقالة على التعليقات لتضمين التوثيق داخل الشيفرة الخاصة بك لجعلها أكثر ملائمة للقراءة، إن التوثيقات الخارجية مثل دليل المستخدم والدروس التعليمية على الشبكة والمراجع مهمة أيضًا إلا أنها خارج نطاق موضوعنا، إذا أردت أن تعرف أكثر بخصوص التوثيقات الخارجية راجع مولد توثيق سفينكس Sphinx. ستتحدث المقالة أيضًا عن سلاسل التوثيق النصية docstrings هي نوع توثيق خاص بلغة بايثون للدوال functions والتوابع methods وأيضًا الوحدات modules الخاصة بك. عندما تحدد تعليقات بصيغة سلاسل التوثيق النصية، ستسهّل الأدوات الآلية automated tools، مثل مولدات التوثيق، أو وحدة help()‎ المبنية مسبقًا في بايثون على المطورين إيجاد المعلومات عن الشيفرة الخاصة بك. التعليقات Comments تدعم بايثون تعليقات السطر الواحد والأسطر المتعددة مثل كل لغات البرمجة، أي نص يأتي بعد إشارة # ونهاية السطر هو تعليق سطر واحد، وعلى الرغم من أنه ليس لبايثون صياغة مخصصة لتعليقات الأسطر المتعددة يمكن استخدام السلاسل النصية المتعددة الأسطر بثلاث مزدوجات بدلًا عن ذلك، حيث لا تتسبب قيمة السلسلة النصية هذه بأن يقوم مفسر بايثون بأي شيء، انظر إلى هذا المثال: # هذا تعليق سطري """هذه سلسلة نصية متعددة الأسطر وتعمل كتعليق متعدد الأسطر أيضًا """ إذا استمر التعليق الخاص بك لأكثر من سطر فمن الأفضل استخدام تعليق واحد متعدد الأسطر بدلًا من عدة تعليقات سطر واحد متتالية التي ستكون قراءتها أصعب كما نرى هنا: """هذه طريقة جيدة لكتابة تعليق يمتد لعدة أسطر """ # هذه ليست طريقة جيدة # لكتابة تعليق # يمتد لعدة أسطر. تعدّ التعليقات والتوثيقات فكرة ثانوية في عملية البرمجة ويعتقد البعض بأنها تتسبب بالضرر أكثر من الإفادة، التعليقات ليست اختيارية إذا أردت كتابة شيفرة احترافية ومقروءة، سنكتب في هذا القسم تعليقات مفيدة تزيد معرفة القارئ دون أن تؤثر على قابلية قراءة البرنامج. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن تنسيق التعليق لننظر إلى بعض التعليقات التي تتّبع ممارسات تنسيق جيّدة: 1 # هنا تعليق بخصوص الشيفرة البرمجية التالية: someCode() 2 # هنا كتلة تعليق أطول تمتد إلى عدة أسطر # باستخدام عدّة تعليقات سطر واحد على التوالي 3 # # تُعرف هذه الكتل بكتل التعليقات if someCondition: 4 # هنا تعليق بخصوص الشيفرة البرمجية التالية 5 someOtherCode() # هنا تعليق سطري يجب أن تكون التعليقات في سطرها الخاص بدلًا من نهاية سطر الشيفرة، يجب أن تكون التعليقات في معظم الوقت جمل كاملة سليمة الكتابة مع علامات الترقيم بدلًا من عبارات أو كلمات وحيدة (تعليق 1). الاستثناء هو أن التعليقات يجب أن تتبع لحدود طول السطر الواحد الخاص بالشيفرة المصدرية ذاته، ويمكن للتعليقات التي تمتد لأسطر متعددة أن تستخدم تعليقات سطر واحد متعددة على التوالي وذلك يُعرف بكتلة التعليقات block comments (تعليق 2). يمكننا فصل الفقرات في كتل تعليقات باستخدام فراغ وتعليق سطر واحد (تعليق 3). يجب للتعليقات أن تكون بمستوى مماثل للمسافة البادئة للشيفرة التي تُعلق عليها (تعليق 4). تُدعى التعليقات التي تتبع سطر الشيفرة بالتعليقات السطرية inline comments (تعليق 5) ويجب على الأقل ترك فراغين بين الشيفرة والتعليق. يجب أن تحتوي تعليقات السطر الأحادي على فراغ واحد بعد إشارة #: #لا تكتب التعليقات مباشرةً بعد إشارة التعليق يمكن أن تحتوي التعليقات على رابط URL مع المعلومات المتعلقة به، إلا أنه لا يجب استبدال التعليقات بالروابط لأن المحتوى المتعلق بالرابط يمكن أن يختفي من الشبكة في أي وقت: # إليك شرحًا مفصلًا عن بعض الجوانب في هذه الشيفرة البرمجية # والموجودة على الرابط التالي، لمزيد من المعلومات اذهب إلى ‫https://example.com تصف الاصطلاحات المذكورة سابقًا النمط أكثر من وصفها للمحتوى، إلا أنها تُسهِّل من قراءة التعليق، كلما كان التعليق أسهل للقراءة، زاد اهتمام المبرمجين فيها، وتكون التعليقات مفيدة فقط عندما يستطيع المبرمجون قراءتها. التعليقات السطرية تأتي التعليقات السطرية inline comments في نهاية سطر الشيفرة كما في الحالة التالية: while True: # استمرّ بسؤال اللاعب لحين إدخاله لحركة صالحة تكون التعليقات السطرية مختصرة، لذا يمكن أن تتسع ضمن حدود طول السطر الموضوعة في دليل تنسيق البرنامج، هذا يعني أنها في أغلب الأحوال تكون قصيرة لحد عدم إعطاء معلومات كافية. إذا كنت تريد استخدام التعليقات السطرية، فانتبه لجعل التعليق يشرح السطر الذي يسبقه فقط، يجب وضع التعليق السطري في سطره الخاص إذا كان يحتاج لمساحة أكثر أو كان يشرح أكثر من سطر من الشيفرة. أحد الاستخدامات الشائعة المناسبة للتعليقات السطرية هو شرح هدف متغير ما أو إعطاء بعض من السياق بخصوصه، وتُكتب هذه التعليقات السطرية على تعبير الإسناد الذي يُنشئ المتغير: TOTAL_DISKS = 5 # المزيد من الأقراص سيزيد صعوبة الأحجية استخدام ثاني شائع للتعليقات السطرية هو إضافة سياق لقيم المتغيرات عندما تريد انشائها: month = 2 # تتراوح قيمة الأشهر من 0 (أي يناير) إلى 11 (أي ديسمبر)‏ catWeight = 4.9 # الوزن بوحدة الكيلوجرام website = 'inventwithpython.com' # لا تُضمّن‫ "https://‎" في بداية القيمة لا يجب على التعليقات السطرية أن تحدد نوع بيانات المتغير، لأن هذا واضح من تعبير الإسناد إلا أذا كان ذلك يشكل تعليقًا لتلميح النوع type hints وهو ما سنتحدث عنه لاحقًا. التعليقات التوضيحية يجب أن تشرح التعليقات بالعادة لماذا تكون الشيفرة مكتوبة بهذا الشكل بدلًا من طريقة عمل الشيفرة أو كيفية عملها، حتى مع تنسيق الشيفرة المناسب واصطلاحات التسميات المفيدة التي تحدثنا عنها سابقًا فلا يمكن للشيفرة شرح نوايا المبرمج، فقد تنسى تفاصيل الشيفرة التي كتبتها بعد أسابيع، بالتالي يجب عليك في الوقت الحاضر كتابة تعليقات شيفرة توضيحية لمنع نفسك المستقبلي من لعن نفسك الماضي. لدينا هنا مثال عن تعليق غير مفيد يشرح ماذا تفعل الشيفرة بدلًا من تلميح سبب كتابة الشيفرة، إذ يوضح هذا التعليق أمرًا مفهومًا: >>> currentWeekWages *= 1.5 # ضرب أجور الأسبوع الحالي بمقدار 1.5 إن هذا التعليق أكثر من غير مفيد، فمن الواضح من الشيفرة أن المتغير currentWeekWages مضروب بـالقيمة 1.5 لذا إهمال هذا التعليق بالكامل من شأنه تبسيط الشيفرة الخاصة بك. إن التعليق التالي سيكون أفضل بكثير: >>> currentWeekWages *= 1.5 # أخذ نسبة الأجرة ونصف بالحسبان يشرح هذا التعليق النيّة من هذه الشيفرة بدلًا من تكرار كيفية عمل الشيفرة، إذ يعطي سياقًا لا تستطيع حتى الشيفرة المكتوبة بشكل جيد تقديمها. تعليقات الخلاصة شرح نيّة المبرمج ليست الطريقة الوحيدة التي تكون فيها التعليقات مفيدة، تسمح التعليقات المختصرة التي تلخص عدة أسطر من الشيفرة للقارئ أن ينظر إلى الشيفرة بشكل سريع ويحصل على فكرة عامة عما تفعله. يستخدم المبرمجون عادة فراغ فارغ لفصل "فقرات" الشيفرة عن بعضها وتشغل تعليقات التلخيص summary comments سطرًا واحد في بداية هذه الفقرات، وعلى عكس التعليقات السطرية التي تشرح سطرًا واحدًا من الشيفرة، تصف تعليقات التلخيص ما تفعله الشيفرة على درجة أعلى من التجريد. يمكن في هذا المثال أن نعرف من قراءة السطور الأربعة من الشيفرة أنها تضبط متغير playerTurn لقيمة تمثل اللاعب الضد، ولكن تعليق السطر الواحد القصير يريح القارئ من قراءة الشيفرة ومعرفة هدف القيام بذلك: # تبديل الدور إلى اللاعب الآخر if playerTurn == PLAYER_X: playerTurn = PLAYER_O elif playerTurn == PLAYER_O: playerTurn = PLAYER_X توزيع تعليقات خلاصة بالشكل هذا في البرنامج الخاص بك يُسهل علينا النظر إليه بشكل سريع، يمكن للمبرمج أن يتفحص الشيفرة بشكل دقيق بخصوص أي نقطة تهمه، وتمنع تعليقات الخلاصة المبرمجين من أخذ أفكار خاطئة عما تفعله الشيفرة، إذ يمكن لتعليق تلخيص قصير أن يؤكد أن المطور فهم بشكل مناسب كيفية عمل الشيفرة. تعليقات "الدروس المستفادة" طُلب مني عندما كنت أعمل في شركة برمجيات أن أُكيّف مكتبة رسوم بيانية لكي تتعامل مع تحديثات في الوقت الفعلي لملايين نقاط البيانات في المخطط. المكتبة التي كنا نستخدمها تستطيع إما تحديث المخطط في الوقت الفعلي أو تدعم مخططات لملايين نقاط البيانات ولكن ليس الأمرين معًا؛ توقعت أن أُنهي المهمة خلال بضعة أيام وفي الأسبوع الثالث مازلت اعتقد انني أستطيع ان انتهي خلال بضعة أيام، كل يوم كان الحل قريبًا جدًا وفي خلال الأسبوع الخامس كان لدي نموذج أولي يعمل. خلال كل تلك الفترة تعلمت الكثير من التفاصيل عن كيفية عمل مكتبة الرسوم البيانية وما هي قدراتها وحدودها، ثم قضيت بضع ساعات اكتب هذه التفاصيل في تعليق بطول صفحة ووضعته في الشيفرة المصدرية، عرفت أن أي شخص يحتاج للتعديل على الشيفرة الخاصة بي لاحقًا سيواجه هذه المشاكل البسيطة نفسها التي واجهتها، وهذا التوثيق الذي كتبته سيوفر عليهم أسابيع من الجهد. ربما يمتد تعليق "الدروس المستفادة" -كما اسميهم- لعدة فقرات مما يجعله يبدوا أنّه لا ينتمي إلى الشيفرة المصدرية ولكن المعلومات المحتواة فيه هي كنز لمن يحتاج المحافظة على هذه الشيفرة، لا تخف من كتابة تعليقات طويلة ومفصلة في الشيفرة المصدرية الخاصة بك لشرح كيفية عمل شيء ما، بالنسبة للمبرمجين الآخرين العديد من هذه التفاصيل ستكون غير معروفة أو غير مفهومة أو مهملة. يمكن لمطوري البرمجيات الذين لا يحتاجون لهذه التفاصيل بأن يتجاهلوها ببساطة، إلا أن المطورين الذين يحتاجونهم سيكونون ممتنين لوجودهم. تذكّر أن تعليق الدروس المستفادة هو ليس مثل توثيقات الوحدات أو الدوال (التي تتعامل معها سلاسل التوثيق النصية)، وهو ليس درس تعليمي لمستخدمي البرنامج، بل أن تعليقات الدروس المستفادة هي للمطورين الذين يقرؤون الشيفرة المصدرية. لأن تعليق الدروس المستفادة الخاص بي يتعلق بمكتبة رسوم بيانية ذات مصدر مفتوح ويمكن أن يفيد الآخرين فقد نشرته في موقع السؤال والجواب العام Stackoverflow.org بحيث يتسنّى للآخرين الذين هم في نفس حالتي إيجاده. التعليقات القانونية لدى بعض الشركات البرمجية أو المشاريع المفتوحة المصدر سياسات تضم حقوق النشر وترخيص البرمجيات ومعلومات عن المؤلف في التعليقات في أعلى كل ملف شيفرة مصدرية لأسباب قانونية، يجب أن تتألف هذه التوصيفات من عدة أسطر على الأكثر وتشبه التالي: """Cat Herder 3.0 Copyright (C) 2021 Al Sweigart. All rights reserved. See license.txt for the full text.""" راجع إذا أمكن ملفات خارجية أو مواقع تحوي النص الكامل للرخصة بدلًا من ضم كل الرخصة فوق كل ملف شيفرة مصدرية، إنه من المتعب المرور عبر عدة صفحات من النصوص كلما تفتح ملف شيفرة مصدرية، وضم الرخصة كاملةً لا يضيف أي حماية قانونية إضافية. تعليقات مهنية أخبرني زميل أكبر مني أحترمه كثيرًا في عملي البرمجي الأول أننا إذا أرفقنا الشيفرة المصدرية لمنتجنا للعملاء أحيانًا فإنه من المهم أن تكون للتعليقات نبرة مهنية. يبدو وقتها أنني كتبت “يا إلهي ما هذا!” في أحد التعليقات لجزء مثير للإحباط من الشيفرة، شعرت عندئذٍ بالحرج واعتذرت مباشرةً وعدلت التعليق، ومنذ تلك اللحظة حافظت على مستوى المهنيّة الخاص بشيفرتي البرمجية حتى في المشاريع الشخصية. ربما يُغريك كتابة مزحة أو التنفيس عن غضبك في تعليقات برنامجك، ولكن اعتد تجنّب القيام بذلك، فأنت لا تعرف من سيقرأ الشيفرة الخاص بك في المستقبل ومن السهل ألّا تُفهم نبرة النص، كما وضحنا سابقًا، السياسة الفُضلى هي كتابة تعليقاتك بطريقة مهذبة ومباشرة بدون مزاح. تعليقات وسوم الشيفرة البرمجية والأشياء التي يجب القيام بها يترك المبرمجين أحيانًا تعليقات قصيرة لتذكيرهم بخصوص المهام التي يجب القيام بها، ويأخذ ذلك شكل وسوم الشيفرة البرمجية codetags وهو تعليق بحروف كبيرة مثل TODO متبوع بشرح قصير، يجب استخدام أدوات إدارة المشروع في الحالة المثالية لمتابعة هذه المشاكل بدلًا من دفنها في الشيفرة المصدرية الخاصة بك، ولكن يمكن استخدام هذه التعليقات للمشاريع الشخصية الصغيرة التي لا تستخدم العديد من الأدوات، إذ يمكن لتعليق TODO أن يخدم كتذكير مفيد، كمثال على ذلك: _chargeIonFluxStream() # TODO: النظر إلى سبب فشل هذه الدالة كل يوم ثلاثاء بإمكانك استخدام عدد من وسوم الشيفرة لهذه التنبيهات كما يلي: TODO يمثّل تذكيرًا عام عن عمل يجب القيام به FIXME يمثّل تذكيرًا بخصوص عدم عمل جزء من الشيفرة بشكل كامل HACK يمثّل تذكيرًا بأن هذا الجزء من الشيفرة يعمل ولكن بصعوبة ويجب تطوير هذه الشيفرة XXX يمثّل تنبيهًا عامًا وعادةً ما يكون ذو ثقل كبير يجب إلحاق هذه التسميات ذات الحروف الكبيرة بشرح مفصل عن المهمة أو المشكلة، وبعدها يمكن البحث في الشيفرة المصدرية عن هذه التسميات لإيجاد الشيفرة التي بحاجة إصلاح، السيئة من هذه التعليقات أنه يمكن نسيان هذه التذكرات إن لم تكن تقرأ هذا القسم من الشيفرة الخاصة بك الموجودة فيها تلك الوسوم. لا يجب أن تستبدل وسوم الشيفرة أدوات متابعة المشاكل أو تقارير الأخطاء الرسمية. إذا أردت استخدام وسوم الشيفرة في الشيفرة الخاصة بك أنصح بإبقائها بسيطة باستخدام TODO وتجاهل الباقي. التعليقات السحرية وترميز الملف المصدري لربما لمحت ملف مصدري .py مع شيء يشبه هذه الأسطر في أعلى الملف: 1 #!/usr/bin/env python3 2 # -*- coding: utf-8 -*- تقدم هذه التعليقات السحرية magic comments التي تظهر دائمًا في أعلى الملف معلومات بخصوص المفسر أو الترميز، إذ يخبر سطر شيبانج shebang الأول المبدوء بإشارتي !# نظام التشغيل بالمفسر الذي يجب أن يُستخدم لتنفيذ التعليمات في الملف. التعليق السحري الثاني هو تعريف للترميز، ويُعرّف في هذه الحال الترميز UTF-8 كنمط ترميز يونيكود لاستخدامه في الملف المصدري. لا تحتاج على الإطلاق عمومًا لضم هذا السطر لأن معظم المحرّرات وبيئات التطوير المتكاملة IDEs تحفظ ملفات الشيفرة المصدرية باستخدام الترميز UTF-8، وتعامل إصدارات بايثون بدءًا من 3.0 الترميز UTF-8 كترميز الافتراضي. يمكن أن تحتوي الملفات المرمزة باستخدام UTF-8 على أي مَحرف لذا يبقى ملف المصدر .py صالحًا حتى لو كان يحتوي على حروف انكليزية أو صينية أو عربية. لمقدمة عن اليونيكود وترميز السلاسل النصية أرشح منشور مدوّنة نيد باتشيلدر Ned Batchelder "اليونيكود العملي". سلاسل التوثيق النصية Docstrings سلاسل التوثيق النصية docstrings هي تعليقات بأسطر متعددة تظهر في أعلى ملف الوحدة المصدري "‏‎.py‎‎"،‏ أو مباشرةً بعد تعليمة class، أو def، وتقدم توثيقات عن الوحدة module، أو الصنف class أو الدالة funtion) أو التوابع المعرّفة. تُستخدم أدوات توليد التوثيقات الآلية سلاسل التوثيق النصية هذه لإنشاء ملفات توثيق خارجية، مثل ملفات المساعدة، أو صفحات الويب. يجب أن تستخدم سلاسل التوثيق النصية تعليقات متعددة الأسطر مع ثلاث علامات تنصيص مزودجة بدلًا من التعليقات أحادية السطر التي تبدأ بإشارة المربع #، كما ينبغي أن تستخدم سلاسل التوثيق النصية ثلاث علامات تنصيص مزودجة من أجل سلاسل ثلاثية الاقتباس بدلًا من ثلاث علامات تنصيص أحادية. إليك مثالًا عن ملف sessions.py في الوحدة الشهيرة requests: 1 # -*- coding: utf-8 -*- 2 """ requests.session ~~~~~~~~~~~~~~~~ This module provides a Session object to manage and persist settings across requests (cookies, auth, proxies). """ import os import sys --snip— class Session(SessionRedirectMixin): 3 """A Requests session. Provides cookie persistence, connection-pooling, and configuration. Basic Usage:: >>> import requests >>> s = requests.Session() >>> s.get('https://httpbin.org/get') <Response [200]> --snip-- def get(self, url, **kwargs): 4 r"""Sends a GET request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. :param \*\*kwargs: Optional arguments that ``request`` takes. :rtype: requests.Response """ --snip-- يحتوي request الخاص بملف sessions.py على سلاسل توثيق نصية من أجل الوحدة (تعليق 2) والصنف Session (تعليق 3) وتابع get()‎ الخاص بصنف Session (تعليق 4). لاحظ أنه على الرغم من أن سلسلة التوثيق النصية للوحدة يجب أن تكون أول سلسلة نصية تظهر في الوحدة، إلا أنها يجب أن تأتي بعد أي تعليق سحري، مثل سطر شيبانج shebang المبدوء بإشارة # أو تعريف الترميز (تعليق 1). يمكن استرجاع سلاسل التوثيق النصية لاحقًا من أجل وحدة أو صنف أو تابع عن طريق التحقق من سمة الكائن __doc__ الخاصة به، إذ يمكننا هنا مثلًا فحص سلسلة التوثيق النصية لمعرفة المزيد عن وحدة sessions وصنف Session والتابع get()‎: >>> from requests import sessions >>> sessions.__doc__ '\nrequests.session\n~~~~~~~~~~~~~~~~\n\nThis module provides a Session object to manage and persist settings across\nrequests (cookies, auth, proxies).\n' >>> sessions.Session.__doc__ "A Requests session.\n\n Provides cookie persistence, connection-pooling, and configuration.\n\n Basic Usage::\n\n >>> import requests\n --snip-- >>> sessions.Session.get.__doc__ 'Sends a GET request. Returns :class:`Response` object.\n\n :param url: URL for the new :class:`Request` object.\n :param \\*\\*kwargs: --snip-- يمكن أن تستخدم أدوات التوثيقات الآلية سلاسل التوثيق النصية لتأمين معلومات مناسبة للسياق، ومن هذه الأدوات هي دالة help()‎ المبنية مسبقًا في بايثون، والتي تعرض سلسلة التوثيق النصية للكائن الممرر بطريقة أسهل للقراءة من سلاسل __doc__ النصية المباشرة الخام، وهذا يفيد عند التعامل مع الصدفة التفاعلية interactive shell، لأننا نريد الحصول على المعلومات عن أي وحدة أو صنف أو دالة نريد استخدامها. >> from requests import sessions >>> help(sessions) Help on module requests.sessions in requests: NAME requests.sessions DESCRIPTION requests.session ~~~~~~~~~~~~~~~~ This module provides a Session object to manage and persist settings -- More -- إذا كانت سلسة التوثيق النصية أكبر من أن تتسع على الشاشة تعرض بايثون --More-- في أسفل النافذة، ويمكنك الضغط على المفتاح الإدخال ENTER للوصول إلى السطر التالي أو ضغط على مفتاح spacebar للوصول إلى الصفحة التالية أو الضغط على مفتاح Q للخروج من مشاهدة سلسلة التوثيق النصية. ينبغي أن تحتوي سلسلة التوثيق النصية عمومًا على سطر واحد يلخص الوحدة أو الصنف أو الدالة متبوعًا بسطر فارغ ومعلومات مفصلة أكثر، أما بالنسبة للتوابع والدوال، فيمكن أن تحتوي على معلومات عن المُعاملات والقيم المُعادة والآثار الجانبية الخاصة بها. نحن نكتب سلاسل التوثيقات النصية للمبرمجين الآخرين وليس لمستخدمي البرنامج، لذا يجب أن تحتوي معلومات تقنية وليس على دروسًا تعليمية. تقدم سلاسل التوثيق النصية فائدةً ثانيةً مهمة، لأنها تضمّن التوثيقات في الشيفرة المصدرية، فعندما تكتب التوثيقات بصورةٍ منفصلة عن الشيفرة يمكن أن تنساها كاملة، وبدلًا عن ذلك عندما تكتب سلاسل التوثيق النصية في أعلى الوحدات والأصناف والدوال تبقى المعلومات سهلة المراجعة والتحديث. ربما لا تكون قادرًا على كتابة سلاسل التوثيق النصية إذا ما زلت تعمل على الشيفرة التي تريد وصفها، ففي تلك الحالة أضف تعليق TODO في سلسلة التوثيق النصية بمثابة تذكير لإكمال التفاصيل المتبقية. مثلًا لدى الدالة الخيالية reverseCatPolarity()‎ سلسلة توثيق نصية ضعيفة توضّح ما هو واضح أصلًا: def reverseCatPolarity(catId, catQuantumPhase, catVoltage): """عكس قطبية قطّة TODO أنهِ سلسلة التوثيق النصية""" --snip-- ربما تغريك كتابة توثيقات قصيرة والمضي قدمًا لأن كل صنف ودالة وتابع يجب أن يكون لديه سلسلة توثيق نصية، إلا أنه من السهل نسيان أن سلسلة التوثيق النصية هذه ستحتاج لإعادة كتابة دون تعليق TODO. يحتوي PEP257 على توثيق مفصل عن سلاسل التوثيق النصية. ترجمة -وبتصرف- لقسم من الفصل COMMENTS, DOCSTRINGS, AND TYPE HINTS من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: البرمجة الوظيفية Functional Programming وتطبيقها في بايثون كيفية كتابة التعليقات في بايثون أساسيات البرمجة بلغة بايثون
  8. يُعد المؤشر pointer مفهومًا عامًا لمتغيرٍ يحتوي على عنوان في الذاكرة، ويشير هذا العنوان أو "يؤشر إلى" بعض البيانات الأخرى. أكثر أنواع المؤشرات شيوعًا في رست هو المرجع reference، الذي تعلمناه سابقًا. يُحدّد المرجع بالرمز "&" وتُستعار القيمة التي يشير إليها، ولا يوجد للمؤشرات أي قدرات خاصة عدا الإشارة إلى البيانات، ولا يتطلّب استخدامها أي حِمل إضافي overhead. من جهة أخرى، تُعدّ المؤشرات الذكية smart pointers هياكل بيانات تعمل مثل مؤشر ولكن لها أيضًا بيانات وصفية metadata وقدرات إضافية، إذ لا يقتصر مفهوم المؤشرات الذكية على رست، فهي نشأت في لغة سي بلس بلس C++‎ وتوجد بلغات أخرى أيضًا. تحتوي رست على مجموعة متنوعة من المؤشرات الذكية المعرَّفة في المكتبة القياسية التي تقدم وظائف أكثر من تلك التي توفرها المراجع، وللتعرف على المفهوم العام، سنلقي نظرةً على بعض الأمثلة المختلفة للمؤشرات الذكية، بما في ذلك نوع مؤشر ذكي لعدّ المراجع reference counting. يمكّنك هذا المؤشر من السماح بوجود عدّة مالكين owners للبيانات من خلال تتبع عددهم، ويُحرّر البيانات في حال لم يتبقَّ أي مالكين. يوجد بمفهوم رست للملكية والاستعارة فرقٌ إضافي بين المراجع والمؤشرات الذكية؛ إذ بينما تستعير المراجع البيانات فقط، تمتلك المؤشرات الذكية في كثير من الحالات البيانات التي تشير إليها. صادفنا مسبقًا بعض المؤشرات الذكية -على الرغم من أننا لم ندعوها على هذا النحو في ذلك الوقت- بما في ذلك String و <Vec<T، وكلا النوعين مؤشرات ذكية لأنهما يمتلكان بعض الذاكرة و تسمحان لك بالتلاعب بها، إضافةً لوجود بيانات وصفية وإمكانيات أو ضمانات إضافية. تخزّن String على سبيل المثال سعتها على أنها بيانات وصفية ولديها قدرة إضافية لتضمن أن تكون بياناتها دائمًا بترميز UTF-8 صالح. تُطبَّق عادةً المؤشرات الذكية باستخدام الهياكل، وتنفّذ المؤشرات الذكية على عكس البنية العادية Deref و Drop، إذ تسمح سمة Deref لنسخة instance من هيكل المؤشر الذكي بالتصرف بمثابة مرجع حتى تتمكن من كتابة شيفرتك البرمجية للعمل مع المراجع أو المؤشرات الذكية، بينما تسمح لك سمة Drop بتخصيص الشيفرة التي تُنفَّذ عندما تخرج نسخة المؤشر الذكي عن النطاق، وسنناقش هنا كلًا من السمات traits ونوضح سبب أهميتها للمؤشرات الذكية. لن يغطي هذا المقال كل مؤشر ذكي موجود بما أن نمط المؤشر الذكي smart pointer pattern هو نمط تصميم عام يستخدم بصورةٍ متكررة في رست. تمتلك العديد من المكتبات مؤشراتها الذكية الخاصة بها، ويمكنك حتى كتابة المؤشرات الخاصة بك. سنغطي المؤشرات الذكية الأكثر شيوعًا في المكتبة القياسية: <Box<T لحجز مساحة خاصة بالقيم على الكومة heap. <Rc<T نوع عدّ مرجع يمكّن الملكية المتعددة. <Ref<T و <RefMut<T اللذين يمكن الوصول إليهما عن طريق <RefCell<T، وهو نمط يفرض قواعد الاستعارة وقت التنفيذ runtime بدلًا من وقت التصريف compile time. سنغطي بالإضافة إلى ذلك نمط قابلية التغيير الداخلي interior mutability pattern، إذ يعرّض النوع الثابت immutable واجهة برمجية لتعديل قيمة داخلية، كما سنناقش أيضًا دورات المرجع reference cycles، وسنرى كيف بإمكانها تسريب leak الذاكرة وكيفية منعها من ذلك. دعنا نبدأ. استخدام المؤشر Box‎ للإشارة إلى البيانات المخزنة على الكومة يُعد الصندوق "Box" واحدًا من أكثر المؤشرات الذكية وضوحًا وبساطةً، ويُكتب نوعه بالشكل <Box<T. تسمح لك الصناديق أن تخزن البيانات على الكومة بدلًا من المكدس stack، إذ يبقى المؤشر على المكدس الذي يشير بدوره للبيانات الموجودة على الكومة. عد إلى الفصل المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست لمراجعة الفرق بين الكومة والمكدس. لا تملك الصناديق أي أفضلية في الأداء عدا أنها تخزن بياناتها على الكومة عوضًا عن المكدس، ولا تملك الكثير من الإمكانيات الإضافية. سنستخدمها غالبًا في أحد هذه الحالات: عندما يكون لديك نوع بحجم غير معروف وقت التصريف وتريد أن تستخدم قيمةً لهذا النوع في سياق يتطلب حجمه المحدد. عندما يكون لديك حجم كبير من البيانات وتريد أن تنقل ملكيتها ولكنك تريد التيقن أن البيانات لن تُنسَخ عندما تفعل هذا. عندما تريد أن تملك قيمةً ما وتهتم فقط أنها من نوع يناسب سمة محددة بدلًا عن كونها من نوع محدد. سنستعرض الحالة الأولى في فقرة "تمكين الأنواع التعاودية باستخدام الصناديق"، أما في الحالة الثانية فيمكن أن يأخذ نقل ملكية كبيرة من البيانات وقتًا طويلًا وذلك لأن البيانات نُسخت على المكدس، ويمكننا تخزين الكمية الكبيرة من البيانات على الكومة في صندوق لتحسين الأداء في هذه الحالة، وبذلك تُنسخ كميةٌ صغيرةٌ من بيانات المؤشر على المكدس، بينما تبقى البيانات التي تشير إليها في مكان واحد على الكومة. تُعرف الحالة الثالثة باسم "سمة الكائن" وسنتكلم عنها لاحقًا، إذ أنك ستطبق ما تعلمته هنا لاحقًا. استخدام Box‎ لتخزين البيانات على الكومة سنتكلم عن طريقة كتابة Box<T>‎ وكيفية تفاعل هذا النوع مع القيم المخزنة داخله قبل أن نناقش حالة استخدام تخزين الكومة للنوع Box<T>‎. توضح الشيفرة 1 كيفية استخدام صندوق لتخزين قيمة i32 على الكومة: اسم الملف: src/main.rs fn main() { let b = Box::new(5); println!("b = {}", b); } [الشيفرة 1: تخزين قيمة من النوع i32 على الكومة باستعمال صندوق box] نعرّف المتغير b ليملك القيمة Box التي تشير إلى القيمة "5" المخزنة على الكومة. سيطبع هذا البرنامج "b = 5" وفي هذه الحالة سنصل للبيانات الموجودة في الصندوق بطريقة مشابهة في حال كانت البيانات مخزنة على المكدس. ستُحرَّر القيمة deallocated كما في أي قيمة ممتلكة، عندما يخرج صندوق عن النطاق كما تفعل b في نهاية main، وتحدث عملية التحرير لكل من الصندوق (المخزن على المكدس) والبيانات التي يشير إليها (المخزنة على الكومة). وضع قيمة وحيدة على الكومة غير مفيد، لأنك لن تستخدم الصناديق بحد ذاتها كثيرًا، ووجود قيم مثل قيمة وحيدة من النوع i32 على المكدس -إذ تُخزن افتراضيًا هناك- مناسبٌ أكثر في أغلب الحالات. لننظر إلى حالة تسمح لنا الصناديق أن نعرّف أنواع لن يُسمح لنا بتعريفها إن لم يكن لدينا صناديق. تمكين الأنواع التعاودية باستخدام الصناديق يمكن للقيمة من نوع تعاودي recursive type أن تملك قيمةً أخرى من النوع ذاته مثل جزء من نفسها. تمثّل الأنواع التعاودية مشكلة إذ أن رست تحتاج لمعرفة المساحة التي يحتلها نوع ما وقت التصريف، ويمكن لتداخل قيم الأنواع التعاودية نظريًا أن يستمر إلى ما لا نهاية، لهذا لا يمكن أن تعرف رست كم تحتاج القيمة من مساحة، إلا أنه يمكننا استخدام الأنواع التعاودية بإدخال صندوق في تعريف النوع التعاودي نظرًا لأن الصناديق لها حجم معروف. لنكتشف قائمة البنية "cons list" مثالًا على نوع تعاودي، إذ أن نوع البيانات هذا موجود كثيرًا في لغات البرمجة الوظيفية. يُعَدّ نوع قائمة البنية بسيطًا وواضحًا باستثناء نقطة التعاود فيه، وبالتالي ستكون المفاهيم في الأمثلة التي سنعمل عليها مفيدةً في أي وقت ستصادف فيه حالات أكثر تعقيدًا من ضمنها الأنواع التعاودية. المزيد من المعلومات عن قائمة البنية تُعد قائمة البنية هيكل بيانات أتى من لغة البرمجة ليسب Lisp وشبيهاتها، وتتألف من أزواج متداخلة، وهي نسخة ليسب من القائمة المترابطة linked list، ويأتي اسم هيكل البيانات هذا من الدالة cons (اختصارًا لدالة البنية construct function) في ليسب التي تبني بدورها زوجًا جديدًا من وسيطين arguments. يمكننا بناء قوائم بنية مؤلفة من أزواج تعاودية عن طريق استدعاء cons على زوج يحتوي على قيمة وزوج آخر. إليك المثال التوضيحي pseudocode لقائمة بنية تحتوي على القائمة 1، 2، 3 مع وجود كل زوج داخل قوسين: (1, (2, (3, Nil))) يحتوي كل عنصر في قائمة البنية على عنصرين: القيمة للعنصر الحالي والعنصر التالي، إلا أن العنصر الأخير في القائمة يحتوي فقط على قيمة تُدعى Nil دون عنصر تالي. يمكن إنشاء قائمة البنية عن طريق استدعاء دالة cons بصورة تعاودية، والاسم المتعارف عليه للدلالة على الحالة الأساسية base case للتعاودية هو Nil، مع العلم أنه ليس خاضعًا لنفس مبدأ المصطلحين "null" أو "nil" الذين ناقشناهما سابقًا، فهما يمثلان مؤشرًا على قيمة غير موجودة أو غير صالحة. لا تعد قائمة بينة من هياكل البيانات المُستخدمة بكثرة في رست، إذ يُعد النوع <Vec<T خيارًا أفضل للاستعمال في معظم الوقت عندما تملك قائمة عناصر في رست، كما يوجد أنواع أخرى لبيانات تعاودية مفيدة في حالات متعددة، لكن من خلال البدء بقائمة البنية هنا، سنتعرف كيف تمكّننا للصناديق من تعريف نوع بيانات تعاودية دون ارتباك. تتضمن الشيفرة 2 على تعريف لمعدّد enum لقائمة بنية. لاحظ أن هذه الشيفرة لن تُصرَّف بعد لأن النوع List لا يملك حجمًا محددًا وهذا ما سنوضحه لاحقًا. اسم الملف: src/main.rs enum List { Cons(i32, List), Nil, } [الشيفرة 2: المحاولة الأولى لتعريف معدّد لتمثيل هيكل البيانات قائمة البنية لقيم من النوع i32] ملاحظة: نطبّق قائمة البنية التي تحمل فقط قيم من النوع i32 بهدف التوضيح، إذ يمكننا تنفيذها باستعمال الأنواع المعمّمة generics كما ناقشنا سابقًا وذلك لتعريف نوع قائمة بنية يخزّن قيمًا من أي نوع. يبدو استعمال النوع List لتخزين القائمة "1‎, 2, 3" كما توضح الشيفرة 3: اسم الملف: src/main.rs use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Cons(2, Cons(3, Nil))); } [الشيفرة 3: استعمال المعدّد List لتخزين القائمة "1‎, 2, 3"] تحمل قيمة Cons الأولى على "1" وقيمة List أخرى، قيمة List هذه هي قيمة Cons أخرى تحتوي على "2" وقيمة List أخرى، قيمة List هذه هي قيمة Cons أخرى تحتوي على "3" وقيمة List التي هي في النهاية Nil ألا وهو المتغاير variant غير التعاودي الذي يشير إلى نهاية القائمة. إذا حاولنا تصريف الشيفرة البرمجية الموجودة في الشيفرة 3، فسنحصل على الخطأ الموضح في الشيفرة 4: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) error[E0072]: recursive type `List` has infinite size --> src/main.rs:1:1 | 1 | enum List { | ^^^^^^^^^ recursive type has infinite size 2 | Cons(i32, List), | ---- recursive without indirection | help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable | 2 | Cons(i32, Box<List>), | ++++ + For more information about this error, try `rustc --explain E0072`. error: could not compile `cons-list` due to previous error [الشيفرة 4: الخطأ الذي نحصل عليه عندما نحاول تعريف معدّد تعاودي] يُظهِر الخطأ أن هذا النمط "له حجم لا نهائي"، والسبب هو تعريفنا للنوع List بمتغير تعاودي، أي أنه يحمل قيمةً أخرى لنفسه مباشرًة، ونتيجة لذلك، لا تستطيع رست معرفة مقدار المساحة التي يحتاجها لتخزين قيمة List. دعونا نوضح لماذا نحصل على هذا الخطأ. أولًا، سننظر إلى كيفية تحديد رست لمقدار المساحة التي يحتاجها لتخزين قيمة لنوع غير تعاودي. حساب حجم نوع غير تعاودي تذكر معدّد Message الذي عرّفناه سابقًا (الشيفرة 2 من الفصل التعدادات enums في لغة رست Rust)عندما ناقشنا تعريفات المعدّد: enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() {} تمر رست عبر كل من المتغيرات لمعرفة المتغير الذي يحتاج إلى أكبر مساحة وذلك لتحديد مقدار المساحة المراد تخصيصها لقيمة Message. ترى رست أن Message::Quit لا تحتاج إلى أي مساحة، بينما تحتاج Message::Move إلى مساحة كافية لتخزين قيمتين من نوع i32، وهكذا دواليك، ونظرًا لاستخدام متغير واحد فقط فإن أكبر مساحة تحتاجها قيمة Message هي المساحة التي ستأخذها لتخزين أكبر متغيراتها. قارن هذا مع ما يحدث عندما تحاول رست تحديد مقدار المساحة التي يحتاجها نوع تعاودي مثل المعدد List في الشيفرة 2، إذ يبدأ المصرّف بالنظر إلى المتغاير Cons الذي يحمل قيمةً من النوع i32 وقيمةً من النوع List، لذلك يحتاج Cons إلى مساحة مساوية لحجم النوع i32 إضافةً إلى حجم النوع List. لمعرفة مقدار الذاكرة التي يحتاجها النوع List ينظر المصرّف إلى المتغايرات بدءًا من المتغاير Cons، الذي يحمل قيمةً من النوع i32 وقيمةً من النوع List، وتستمر هذه العملية لما لا نهاية، كما هو موضح في الشكل 1. [الشكل 1: List لانهائية مؤلفة من متغايرات Cons لانهائية] استخدام <Box<T للحصول على نوع تعاودي بحجم معروف يعطينا المصرّف الخطأ التالي لأن رست لا يمكنها معرفة مقدار المساحة المراد تخصيصها لأنواع معرّفة بصورةٍ تعاودية مرفقًا مع هذا الاقتراح المفيد: help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable | 2 | Cons(i32, Box<List>), | ++++ + يعني "التحصيل indirection" -في هذا الاقتراح- أنه بدلًا من تخزين قيمة مباشرةً، يجب علينا تغيير هيكل البيانات المُستخدَم لتخزين القيمة بصورةٍ غير مباشرة عن طريق تخزين مؤشر يشير إلى القيمة عوضًا عن ذلك. نظرًا لأن <Box<T هو مؤشر فإن رست تعرف دائمًا مقدار المساحة التي يحتاجها <Box<T، إذ أن حجم المؤشر لا يتغير بناءً على كمية البيانات التي يشير إليها، وهذا يعني أنه يمكننا وضع <Box<T داخل المتغاير Cons بدلًا من قيمة List أخرى مباشرةً. سيشير <Box<T إلى قيمة List التالية التي ستكون على الكومة بدلًا من داخل المتغاير Cons. نظريًا، لا يزال لدينا قائمة أنشئت باستخدام قوائم تحتوي على قوائم أخرى، ولكن هذا التطبيق الآن أشبه بوضع العناصر بجانب بعضها بدلًا من وضع بعضها داخل الأخرى. يمكننا تغيير تعريف معدّد List في الشيفرة 2 باستخدام List في الشيفرة 3 كما هو موضح في الشيفرة 5 التي ستصرَّف بنجاح: اسم الملف: src/main.rs enum List { Cons(i32, Box<List>), Nil, } use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); } [الشيفرة 5: تعريف List التي تستخدم <Box<T للحصول على حجم معروف] يحتاج المتغاير Cons إلى حجمi32 بالإضافة إلى مساحة لتخزين بيانات مؤشر الصندوق، وبما أن المتغاير Nil لا يخزن أي قيم فهو يحتاج إلى مساحة أقل من المتغاير Cons. نعلم الآن أن أي قيمة List ستشغل حجم i32 إضافةً إلى حجم بيانات مؤشر الصندوق. كسرنا السلسلة اللانهائية التعاودية باستخدام الصندوق، بحيث يمكن للمصرف الآن معرفة الحجم الذي يحتاجه لتخزين قيمة List. يوضح الشكل 2 ما يبدو عليه متغاير Cons الآن. [الشكل 2: List ذات حجم محدد لأن Cons يحمل Box] توفر الصناديق التحصيل وتخصيص الكومة فقط، إذ لا تتوافر على أي إمكانيات خاصة أخرى مثل تلك التي سنراها مع أنواع المؤشرات الذكية الأخرى لاحقًا، كما أنها لا تتمتع بالأداء العام الذي تتحمله هذه الإمكانيات الخاصة لتكون مفيدةً في حالات مثل قائمة البنية، إذ تكون ميزة التحصيل هي الميزة الوحيدة التي نحتاجها. سنلقي نظرةً على المزيد من حالات استعمال الصناديق لاحقًا. النوع <Box<T هو مؤشر ذكي لأنه يطبق السمة Deref التي تسمح لقيم <Box<T أن تُعامَل بمثابة مراجع. عندما تخرج قيمة <Box<T عن النطاق، تُمسَح بيانات الكومة التي يشير إليها الصندوق بسبب تطبيق السمة Drop. ستبرز أهمية هاتان السمتان أكثر عندما نناقش أنواع المؤشرات الذكية الأخرى لاحقًا. لنكتشف هاتين السمتين بتفاصيل أكثر. ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: معاملة المؤشرات الذكية Smart Pointers كمراجع نمطية باستخدام سمة Deref في لغة رست المقال السابق: مساحة عمل كارجو Cargo Workspaces في لغة رست وتحميل حزمة من crates.io أنواع البيانات Data Types في لغة رست Rust المؤشرات الذكية (Smart Pointers) في Cpp
  9. بنينا سابقًا حزمة تتضمن وحدة تنفيذية مصرفة Binary Crate ووحدة مكتبة مصرفة Library Crate، وقد تجد مع تطور مشروعك أن وحدة المكتبة المصرفة تزداد حجمًا وستحتاج إلى تقسيم حزمتك إلى عدد من وحدات مكتبة مصرفة. يقدّم كارجو Cargo ميزة تدعى مساحات العمل Workspaces التي تساعد على إدارة حزم متعددة مرتبطة تُطوَّر بالترادف tandem أي واحدًا بعد الآخر. إنشاء مساحة عمل مساحة العمل هي مجموعة من الحزم التي تتشارك ملف Cargo.lock ومجلد الخرج ذاتهما. سنستخدم شيفرة برمجية بسيطة لإنشاء مشروع باستخدام مساحة العمل، بهدف التركيز على بُنية مساحة العمل أكثر. هناك الكثير من الطرق لبناء مساحة العمل ولذا سنعمل وفق الطريقة الشائعة. سيكون لدينا مساحة عمل تحتوي على وحدة ثنائية أو تنفيذية واحدة ومكتبتين؛ إذ ستؤمن الوحدة الثنائية الوظيفة الأساسية، وستعتمد بدورها على مكتبتين: مكتبة تؤمن دالة add_one، والثانية ستؤمن دالة add_two. ستكون الوحدات المصرفة الثلاثة في مساحة العمل ذاتها. نبدأ بعمل مسار جديد لمساحة العمل: $ mkdir add $ cd add ننشئ بعد ذلك في مجلد الخرج add الملف Cargo.toml الذي سيضبط مساحة العمل كاملةً. لن يكون لهذا الملف قسم [package]. بل سيبدأ بقسم [workspace] الذي سيسمح لنا بإضافة أعضاء إلى مساحة العمل عن طريق تحديد المسار للحزمة باستخدام الوحدة الثنائية أو التنفيذية المصرفة، وفي هذه الحالة المسار هو "adder": اسم الملف: Cargo.toml [workspace] members = [ "adder", ] ننشئ وحدة ثنائية مصرفة [adder] عن طريق تنفيذ cargo new في المجلد add: $ cargo new adder Created binary (application) `adder` package يمكننا الآن بناء مساحة العمل عن طريق تشغيل cargo build. الملفات في مجلد add يجب أن تكون على النحو التالي: ├── Cargo.lock ├── Cargo.toml ├── adder │ ├── Cargo.toml │ └── src │ └── main.rs └── target لمساحة العمل مجلد "target" واحد في بداية المستوى الذي ستوضع فيه أدوات التخطيط artifacts المصرفة، ولا تحتوي حزمة adder على مجلد "target". حتى لو نفّذنا cargo build داخل مجلد "adder"، ستكون أدوات التخطيط المصرفة في "add/target" بدلاً من "add/adder/target". يهيّئ كارجو المجلد target بالشكل هذا لأن الحزم المصرفة في مساحة العمل مهيئة لتعتمد على بعضها بعضًا. إذا كان لكل حزمة مصرفة مجلد "target" خاص بها، فهذا يعني أن كل حزمة مصرفة ستُعيد تصريف باقي الحزم المصرفة في مساحة العمل لوضع أدوات التخطيط في مجلد "target" الخاص بها، إلا أن الحزم تتجنب عملية إعادة البناء غير الضرورية بمشاركة مجلد "target" واحد. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن إنشاء الحزمة الثانية في مساحة العمل دعنا ننشئ حزمة عضو ثانية في مساحة العمل ونسميها add_one. غيِّر ملف Cargo.toml الموجود في المستوى العلوي ليحدد المسار add_one في القائمة members: اسم الملف: Cargo.toml [workspace] members = [ "adder", "add_one", ] أنشئ بعد ذلك حزمة مكتبة مصرفة اسمها add_one: $ cargo new add_one --lib Created library `add_one` package يجب أن يحتوي مجلد "add" الآن على المجلدات والملفات التالية: ├── Cargo.lock ├── Cargo.toml ├── add_one │ ├── Cargo.toml │ └── src │ └── lib.rs ├── adder │ ├── Cargo.toml │ └── src │ └── main.rs └── target نضيف في الملف add_one/scr/lib.rs الدالة add_one: اسم الملف: add_one/src/lib.rs pub fn add_one(x: i32) -> i32 { x + 1 } الآن بإمكاننا أن نجعل كلًا من الحزمة adder والوحدة الثنائية المصرفة تعتمدان على حزمة add_one التي تحتوي مكتبتنا. أولاً، نضيف اعتمادية مسار add_one إلى الملف adder/Cargo.toml. [dependencies] add_one = { path = "../add_one" } لا يفترض كارجو أن الحزم المصرفة تعتمد على بعضها في مساحة العمل، لذا نحتاج إلى توضيح علاقات الاعتمادية. لنستخدم بعدها دالة add_one (من الحزمة المصرفة add_one) في الحزمة المصرفة adder. افتح الملف adder/scr/main.rs وأضف سطر use في الأعلى لإضافة حزمة المكتبة المصرفة add_one الجديدة إلى النطاق. ثم عدِّل الدالة main بحيث تستدعي الدالة add_one كما في الشيفرة 7. اسم الملف: adder/src/main.rs use add_one; fn main() { let num = 10; println!("Hello, world! {num} plus one is {}!", add_one::add_one(num)); } [الشيفرة 7: استخدام حزمة المكتبة المصرفة add_one من الحزمة adder] دعنا نبني مساحة العمل بتنفيذ cargo build في مجلد "add" العلوي. $ cargo build Compiling add_one v0.1.0 (file:///projects/add/add_one) Compiling adder v0.1.0 (file:///projects/add/adder) Finished dev [unoptimized + debuginfo] target(s) in 0.68s يمكننا تحديد أي حزمة نريد تشغيلها في مساحة العمل باستخدام الوسيط ‎-p واسم الحزمة مع cargo run لتشغيل الحزمة الثنائية المصرفة من المجلد "add": $ cargo run -p adder Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/adder` Hello, world! 10 plus one is 11! يشغل هذا الأمر الشيفرة الموجودة في الملف "adder/scr/main.rs"، والتي تعتمد على الحزمة المصرفة add_one. الاعتماد على حزمة خارجية في مساحة العمل نلاحظ أن مساحة العمل تحتوي على ملف Cargo.lock واحد في المستوى الأعلى، بدلاً من أن يكون هناك ملف Cargo.lock في كل مسار حزمة مصرفة. يضمن ذلك أن كل حزمة مصرفة تستخدم الإصدار ذاته لكل الاعتماديات. إذا أضفنا حزمة rand للملفين "adder/Cargo.toml" و "add_one/Cargo.toml"، سيحوِّل كارجو كلاهما إلى إصدار واحد من rand، ثم سيسجل ذلك في Cargo.lock. جعل كل حزم المصرفة تستخدم نفس الاعتمادية يعني أن كل الحزم المصرفة ستكون متوافقة مع بعضها. دعنا نضيف الحزمة المصرفة rand إلى قسم [dependencies] في ملف add_one/Cargo.toml لكي نستخدم الحزمة المصرفة rand في الحزمة المصرفة add_one: اسم الملف: add_one/Cargo.toml [dependencies] rand = "0.8.5" يمكننا الآن إضافة use rand;‎ إلى الملف add_one/src/lib.rs، وبناء كامل مساحة العمل عن طريق تنفيذ cargo build في المجلد "add" الذي سيجلب ويصرِّف الحزمة المصرفة rand. نحصل على تحذير واحد لأننا لم نُشر إلى حزمة rand التي أضفناها إلى النطاق: $ cargo build Updating crates.io index Downloaded rand v0.8.5 --snip-- Compiling rand v0.8.5 Compiling add_one v0.1.0 (file:///projects/add/add_one) warning: unused import: `rand` --> add_one/src/lib.rs:1:5 | 1 | use rand; | ^^^^ | = note: `#[warn(unused_imports)]` on by default warning: `add_one` (lib) generated 1 warning Compiling adder v0.1.0 (file:///projects/add/adder) Finished dev [unoptimized + debuginfo] target(s) in 10.18s يحتوي ملف Cargo.lock في المستوى العلوي على معلومات عن الاعتمادية لكل من add_one و rand، ولكن وعلى الرغم من أننا نستخدم rand في مكان ما ضمن مساحة العمل إلا أننا لا نستطيع استخدامها في الحزم المصرفة الأخرى إلا إذا اضفنا rand إلى ملف Cargo.toml الخاص بها أيضاً. على سبيل المثال إذا أضفنا use rand;‎ إلى ملف adder/scr/main.rs من أجل الحزمة adder سنحصل على خطأ: $ cargo build --snip-- Compiling adder v0.1.0 (file:///projects/add/adder) error[E0432]: unresolved import `rand` --> adder/src/main.rs:2:5 | 2 | use rand; | ^^^^ no external crate `rand` لحل هذه المشكلة، عدِّل ملف Cargo.toml لحزمة adder وأشِر إلى أن rand هي اعتمادية لها أيضاً. بناء الحزمة adder سيضيف rand إلى لائحة اعتماديات adder في ملف cargo.lock، ولكن لن يجري أي تنزيل لنسخ إضافية من rand. يضمن كارجو أن كل حزمة مصرفة في كل حزمة في مساحة العمل تستخدم نفس الإصدار من الحزمة rand، وبالتالي ستقل المساحة التخزينية المستخدمة وسنضمن أن كل الحزم المصرفة في مساحة العمل ستكون متوافقة مع بعضها بعضًا. إضافة اختبار إلى مساحة العمل سنضيف اختبارًا للدالة add_one::add_one داخل الحزمة المصرفة add_one للمزيد من التحسينات: اسم الملف: add_one/src/lib.rs pub fn add_one(x: i32) -> i32 { x + 1 } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { assert_eq!(3, add_one(2)); } } نفّذ الأمر cargo test ضمن مجلد "add" العلوي، إذ سيؤدي تنفيذ cargo test في مساحة عمل مهيكلة بهذا الشكل إلى تنفيذ الاختبارات الخاصة بالحزم المصرفة في مساحة العمل: $ cargo test Compiling add_one v0.1.0 (file:///projects/add/add_one) Compiling adder v0.1.0 (file:///projects/add/adder) Finished test [unoptimized + debuginfo] target(s) in 0.27s Running unittests src/lib.rs (target/debug/deps/add_one-f0253159197f7841) running 1 test test tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/adder-49979ff40686fa8e) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests add_one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s يُظهر أول قسم في الخرج نجاح اختبار it_works في الحزمة المصرفة add_one، بينما يظهر القسم الثاني أنه لم يُعثر على أي اختبار في الحزمة المصرفة adder، ويظهر القسم الأخير عدم العثور على اختبارات توثيق documentation tests في الحزمة المصرفة add_one. يمكن أيضاّ تنفيذ اختبارات لحزمة مصرفة محددة في مساحة عمل من المجلد العلوي باستخدام الراية ‎-p وتحديد اسم الحزمة المصرفة المراد اختبارها: $ cargo test -p add_one Finished test [unoptimized + debuginfo] target(s) in 0.00s Running unittests src/lib.rs (target/debug/deps/add_one-b3235fea9a156f74) running 1 test test tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests add_one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s يظهر الخرج أن cargo test نفّذ فقط الاختبارات الموجودة في الحزمة المصرفة add_one ولم ينفّذ الاختبارات الموجودة في الحزمة المصرفة adder. إذا أردت نشر الحزم المصرفة في مساحة العمل على crates.io، فيجب على كل حزمة مصرفة في مساحة العمل أن تُنشر على حدة. نستطيع نشر حزمة مصرفة معينة في مساحة العمل باستخدام الراية ‎-p وتحديد اسم الحزمة المصرفة المراد نشرها بصورةٍ مماثلة للأمر cargo test. للتدرّب على العملية بصورةٍ أفضل، ضِف الحزمة المصرفة add_two لمساحة العمل هذه بنفس طريقة الحزمة المصرفة add_one. ضع في الحسبان استخدام مساحة العمل كلما كبر مشروعك، فمن الأسهل فهم مكونات صغيرة ومنفردة على كتلة كبيرة من الشيفرة البرمجية. إضافةً إلى ذلك، إبقاء الحزم المصرفة في مساحة عمل واحدة يجعل التنسيق بين الحزم المصرفة أسهل إذا كانت تُعدَّل باستمرار في نفس الوقت. تثبيت الملفات الثنائية binaries باستخدام cargo install يسمح لك أمر cargo install بتثبيت واستخدام الوحدات الثنائية المصرفة محليًا، وليس المقصود من ذلك استبدال حزم النظام، إذ أن الأمر موجود ليكون بمثابة طريقة ملائمة لمطوري رست لتثبيت الأدوات التي شاركها الآخرون على crates.io. لاحظ أنه يمكنك فقط تثبيت الحزم التي تحتوي أهداف ثنائية binary targets، والهدف الثنائي هو برنامج قابل للتشغيل يُنشأ إذا كانت الحزمة المصرفة تحتوي على ملف src/main.rs أو ملف آخر محدد على أنه ملف تنفيذي، على عكس هدف المكتبة library target الذي لا يمكن تشغيله لوحده، فهو موجود لضمِّه داخل برامج أخرى. تحتوي الحزم المصرفة عادةً على معلومات في ملف README وتدل هذه المعلومات فيما إذا كانت الوحدة المصرفة مكتبية أو تحتوي هدفًا ثنائيًا أو كلاهما. تُخزَّن كل الوحدات الثنائية المصرفة المثبتة عند تنفيذ cargo install في مجلد التثبيت الجذر الذي يدعى "bin". إذا ثبتّت رست باستخدام "rustup.rs" ولم يكن لديك أي إعدادات افتراضية فإن المجلد سيكون ‎$HOME/.cargo/bin. تأكد أن هذا المجلد في ‎$PATH الخاص بك لتكون قادراً على تشغيل البرامج التي ثبتتها باستخدام cargo install. ذكرنا سابقًا أن هناك تنفيذ لأداة grep بلغة رست اسمه ripgrep للبحث عن الملفات، ولتثبيت ripgrep يمكنك تنفيذ الأمر التالي: $ cargo install ripgrep Updating crates.io index Downloaded ripgrep v13.0.0 Downloaded 1 crate (243.3 KB) in 0.88s Installing ripgrep v13.0.0 --snip-- Compiling ripgrep v13.0.0 Finished release [optimized + debuginfo] target(s) in 3m 10s Installing ~/.cargo/bin/rg Installed package `ripgrep v13.0.0` (executable `rg`) يظهر السطر الثاني قبل الأخير من المخرجات مكان واسم الثنائية المثبتة، وهي rg في حالة ripgrep. إذا كان مجلد التثبيت موجودًا في ‎$PATH الخاص بك، فيمكنك تشغيل rg --help والبدء باستخدام أداة أسرع مكتوبة بلغة رست للبحث عن الملفات. توسيع استخدامات كارجو عن طريق أوامر مخصصة كارجو مصمم بحيث يمكن توسيع استخداماته بأوامر فرعية دون الحاجة لتعديله. إذا كان هناك وحدة ثنائية binary ضمن ‎$PATH اسمها cargo-something، فهذا يعني أنه يمكنك تشغيلها كما لو كانت أمر فرعي لكارجو عن طريق تنفيذ cargo something. تستطيع استعراض الأوامر المخصصة بتنفيذ cargo --list. قدرتك على استخدام cargo install لتثبيت الإضافات وتشغيلها كما في أدوات الكارجو المضمّنة built-in هي ميزة ملائمة جداً بتصميم كارجو. ترجمة -وبتصرف- لقسم من الفصل More About Cargo and Crates.io من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: المؤشرات الذكية Smart Pointers في رست Rust المقال السابق: تخصيص نسخ مشروع بلغة رست ونشرها على crates.io الأخطاء والتعامل معها في لغة رست الملكية Ownership في لغة رست
  10. سنتعرّف هنا على كيفية تخصيص نُسَخ المشروع builds المختلفة باستخدام ما يُدعى حسابات الإصدار release profiles، ثمّ سنستعرض كيفيّة إضافة مشاريعك بلغة رست على موقع crates.io. تخصيص نسخ مشروع مع حسابات الإصدار وضبطها في كارجو cargo حسابات الإصدار في رست هي حسابات قابلة للتعديل customizable ومعرّفة مسبقًا بالعديد من الإعدادات المختلفة التي تسمح للمبرمج بأن يمتلك تحكمًا أكبر على العديد من الخيارات لتصريف الشيفرة البرمجية، إذ أن كل حساب مضبوط بصورةٍ مستقلة عن الآخر. لدى كارجو حسابين أساسيين، هما: حساب dev وهو حساب يستخدمه كارجو عندما تنفّذ cargo build وحساب release يستخدمه كارجو عندما تنفّذ cargo build --release. حساب dev معرَّف بإعدادات تصلح لعملية التطوير، وحساب release مُعرَّف بإعدادت تصلح لإطلاق نسخ جديدة. قد تكون أسماء الحسابات هذه ضمن خرج نسخ مشروعك مألوفة: $ cargo build Finished dev [unoptimized + debuginfo] target(s) in 0.0s $ cargo build --release Finished release [optimized] target(s) in 0.0s حسابات dev و release هي الحسابات التي يستخدمها المصرّف. يمتلك كارجو إعدادات افتراضية لكلٍ من الحسابات التي تُطبَّق عندما لا تُحدد بوضوح بإضافة أية أقسام [profile.*‎] إلى ملف cargo.toml الخاص بالمشروع، إذ عند إضافة قسم [profile.*‎] لأي حساب تريد التعديل عليه، فأنت تُعيد الكتابة على أي من الإعدادات الفرعية الخاصة بالإعدادات الافتراضية. مثلاً، هذه هي القيم الأساسية لإعدادات opt-level لكل من الحسابين dev و release. اسم الملف: cargo.toml [profile.dev] opt-level = 0 [profile.release] opt-level = 3 تتحكم إعدادات opt-level بعدد التحسينات التي ستطبقها رست على شيفرتك البرمجية، بمجال من 0 إلى 3، مع الانتباه إلى أن تطبيق أي تحسينات إضافية سيزيد من وقت التصريف، لذا إذا كنت في مرحلة التطوير وتصرّف شيفرتك البرمجية بصورةٍ متكررة، فستحتاج إلى تحسينات أقل لتُصرف أسرع حتى لو كانت الشيفرة البرمجية الناتجة تعمل أبطأ. ستكون القيمة لكلٍ من opt-level و dev افتراضيًا هي 0، وعندما تكون جاهزاً لإصدار شيفرتك البرمجية، فمن الأفضل أن تقضي وقتاً أكثر بالتصريف إذ أنك ستصرّف الشيفرة البرمجية لمرة واحدة فقط في وضع الإصدار، لكنك ستشغّل البرنامج عدّة مرات، إذًا يُقايض وضع الإصدار وقت التصريف الطويل مقابل شيفرة برمجية تعمل على نحوٍ أسرع، وهذا هو السبب في كون القيمة الأساسية opt-level للحساب release هي 3. يمكنك تجاوز القيمة الافتراضية بإضافة قيمة مختلفة لها في Cargo.toml، فإذا أردنا مثلًا أن يكون مستوى التحسين هو 1 في حساب التطوير، يمكننا إضافة هذين السطرين في ملف Cargo.toml الخاص بالمشروع: اسم الملف: Cargo.toml [profile.dev] opt-level =1 تعيد هذه الشيفرة تعريف الإعداد الافتراضي 0، وعندما ننفّذ cargo build، سيستخدم كارجو الإعدادات الافتراضية لحساب dev إضافةً إلى التعديلات التي أجريناها على opt-level، ولأننا ضبطنا opt-level إلى القيمة 1، سيطبّق كارجو تحسينات أكثر من التحسينات التي يطبقها في الوضع الافتراضي، ولكن ليس أكثر من التحسينات الموجودة في إصدار البناء. للحصول على لائحة كاملة من خيارات الضبط والقيم الافتراضية لكل حساب راجع توثيق كارجو. نشر وحدة مصرفة crate على crates.io استخدمنا حزم من crates.io مثل اعتماديات dependencies لمشاريعنا، لكن يمكنك أيضًا أن تشارك شيفرتك مع أشخاص آخرين عن طريق نشر حزمتك الخاصة. يوزِّع تسجيل الوحدة المُصرَّفة crates في crates.io الشيفرة المصدرية للحزم الخاصة بك، بحيث تكون الشيفرة المُستضافة مفتوحة المصدر. لدى رست وكارجو ميزات تجعل حزمتك المنشورة أسهل إيجادًا من قبل الأشخاص واستخدامها وسنتحدث عن بعض هذه المزايا ثم سنشرح كيف تنشر حزمة. كتابة تعليقات توثيق مفيدة سيساعد توثيق حزمتك بدقة المستخدمين الآخرين على معرفة كيف ومتى يستخدمونها، لذا يُعد استثمار الوقت في كتابة التوثيق أمرًا مجديًا. ناقشنا سابقًا كيف تعلّق شيفرة رست باستخدام خطين مائلين "//". لدى رست أيضًا نوع محدد من التعليقات للتوثيق تعرف بتعليق التوثيق documentation comment والذي سيولّد بدوره توثيق HTML. يعرض الملف المكتوب باستخدام HTML محتويات تعليقات التوثيق لعناصر الواجهة البرمجية API العامة والموجهة للمبرمجين المهتمين بمعرفة كيفية استخدام وحدة مصرفة بغض النظر عن كيفية عملها وراء الكواليس. تستخدم تعليقات التوثيق ثلاثة خطوط مائلة "///" بدلاً من خطّين، وتدعم صيغة ماركداون Markdown لتنسيق النص، إذ يكفي وضع تعليقات النص مباشرةً قبل العنصر الذي يوَثّق. تُظهر الشيفرة 1 تعليقات التوثيق لدالة add_one في وحدة مصرفة تدعى my_crate. اسم الملف: src/lib.rs /// أضف واحد إلى الرقم المُعطى /// /// # أمثلة /// /// ``` /// let arg = 5; /// let answer = my_crate::add_one(arg); /// /// assert_eq!(6, answer); /// ``` pub fn add_one(x: i32) -> i32 { x + 1 } [الشيفرة 1- تعليق التوثيق لدالة] نقدم هنا وصفًا عما تفعله دالة add_one، ابدأ القسم بعنوان Examples ثم ضع شيفرة تعبّر عن كيفية استخدام الدالة add_one، ويمكننا توليد توثيق HTML من تعليق التوثيق هذا عن طريق تنفيذ cargo doc، إذ يشغّل هذا الأمر أداة rustdoc الموزّعة مع رست وتضع توثيق HTML المولّد في المجلد "target/doc". سيبني تنفيذ cargo doc --open ملف HTML للتوثيق الحالي لوحدتك المصرفة (وأيضًا توثيق كل ما يعتمد على وحدتك المصرفة) ويفتح النتيجة في متصفح ويب للسهولة. انتقل إلى الدالة add_one وسترى كيف يتحول النص في تعليقات التوثيق، كما يظهر في الشكل 1: [الشكل 1: توثيق HTML لدالة add_one] الأقسام شائعة الاستخدام استخدمنا عنوان ماركداون ‎# Examples في الشيفرة 1 لإنشاء قسم في ملف HTML بالعنوان "Examples" وهذه بعض الأجزاء الشائعة الأخرى التي يستخدمها مؤلفو الوحدات المصرفة في توثيقهم: الهلع Panics: السيناريوهات التي تتوقف بها الدوال المُضمنة بالتوثيق، ويجب على مستخدمي الدالة الذين لا يريدون لبرامجهم أن تتوقف ألا يستدعوا الدالة في هذه الحالات. الأخطاء Errors: إذا أعادت الدالة القيمة Result واصفةً أنواع الأخطاء التي قد حصلت للشيفرة البرمجية المُستدعاة والظروف التي قد تسببت بحدوث الأخطاء التي تعيدها، تكون هذه المعلومات مفيدة للمستخدمين ويمكنهم بهذا أن يكتبوا شيفرة تستطيع التعامل مع الأنواع المختلفة من الأخطاء بعدة طرق. الأمان Safety: إذا استُدعيَت الدالة unsafe (نناقش عدم الأمان Unsafety لاحقًا)، يجب أن يكون هناك قسمٌ يشرح سبب عدم أمان الدالة ويغطي الأنواع اللا متغايرة invariants التي تتوقعها الدالة من المستخدمين. لا تحتاج معظم تعليقات التوثيق لكل هذه الأقسام، لكن هذه لائحة جيدة تذكرك بالجوانب التي سيهتم مستخدمو شيفرتك البرمجية بمعرفتها. استخدام تعليقات التوثيق مثل اختبارات يساعد إضافة كُتَل من الشيفرات البرمجية على أنها مثال ضمن تعليقات التوثيق فهم كيفية استخدام مكتبتك، ولفعل هذا الأمر مزايا إضافية: إذ أن تنفيذ cargo test سينفذ بدوره أمثلة الشيفرة البرمجية في توثيقك مثل اختبارات. لا شيء أفضل من توثيق يحتوي على أمثلة، لكن لا شيء أسوأ من أمثلة لا تعمل لأن الشيفرة البرمجية قد تغيرت منذ وقت كتابة التوثيق. نحصل على قسم في نتائج الاختبارات إذا نفّذنا الأمر cargo test مع توثيق دالة add_one من الشيفرة 1 على النحو التالي: Doc-tests my_crate running 1 test test src/lib.rs - add_one (line 5) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s الآن إذا غيرنا إما الدالة أو المثال بحيث يهلع الماكرو assert_eq!‎ في المثال وينفّذ cargo test مرةً أخرى، سنرى أن اختبارات التوثيق تلاحظ أن شيفرة المثال والشيفرة البرمجية لا يتزامنان مع بعضهما بعضًا. تعليق العناصر المحتواة يضيف أسلوب التعليق "!//" التوثيق إلى العنصر الذي يحتوي على التعليقات بدلاً من العناصر التي تلي التعليقات، ونستخدم عادةً تعليقات المستند هذه داخل ملف الوحدة المصرفة الجذر (src/lib.rs اصطلاحًا)، أو داخل وحدة ما لتوثيق الوحدة المصرفة، أو الوحدة ككُل. على سبيل المثال، لإضافة التوثيق التي يصف الغرض من الوحدة المصرّفة my_crate التي تحتوي على الدالة add_one، نضيف تعليقات التوثيق التي تبدأ بـ "!//" إلى بداية الملف src/lib.rs، كما هو موضح في الشيفرة 2: اسم الملف: src/lib.rs //! # My Crate //! //! `my_crate` is a collection of utilities to make performing certain //! calculations more convenient. /// Adds one to the number given. // --snip-- [الشيفرة 2: توثيق الوحدة المصرفة my_crate ككل] لاحظ عدم وجود أي شيفرة برمجية بعد آخر سطر يبدأ بـ "!//"، وذلك لأننا بدأنا التعليقات بـ "!//" بدلاً من "///". نوثّق العنصر الذي يحتوي على هذا التعليق بدلاً من العنصر الذي يتبع هذا التعليق، وفي هذه الحالة، يكون هذا العنصر هو ملف src/lib.rs، وهو الوحدة المصرفة الجذر، إذ تصف هذه التعليقات كامل الوحدة المصرفة. عندما ننفّذ cargo doc --open، تظهر هذه التعليقات على الصفحة الأولى من توثيق my_crate أعلى قائمة العناصر العامة في الوحدة المصرفة، كما هو موضح في الشكل 2: [الشكل 2: التوثيق المولَّد للوحدة المصرّفة my_crate متضمنًا التعليق الذي يصف كل الوحدة المصرفة] تُعد تعليقات التوثيق داخل العناصر مفيدةً لوصف الوحدات المصرفة والوحدات خصوصًا. استخدمها لشرح الغرض العام من الحاوية container لمساعدة المستخدمين على فهم تنظيم الوحدة المصرفة. تصدير واجهة برمجية عامة Public API ملائمة باستخدام pub use يُعد هيكل الواجهة البرمجية العامة أحد النقاط المهمة عند نشر وحدة مصرفة، إذ يكون الأشخاص الذين يستخدمون الوحدة المصرفة الخاصة بك أقل دراية منك بهيكلية الوحدة، وقد يواجهون صعوبةً في إيجاد الأجزاء التي يريدون استخدامها إذا كانت الوحدة المصرفة الخاصة بك تحتوي على تسلسل هرمي كبير. تناولنا سابقًا كيفية جعل العناصر عامة باستخدام الكلمة المفتاحية pub، وإضافة العناصر إلى نطاق باستخدام الكلمة المفتاحية use. ومع ذلك، قد لا تكون الهيكلية المنطقية بالنسبة لك أثناء تطوير الوحدة المصرفة مناسبةً للمستخدمين، إذ قد ترغب في تنظيم الهياكل الخاصة بك ضمن تسلسل هرمي يحتوي على مستويات متعددة، ولكن قد يواجه بعض الأشخاص مشاكلًا بخصوص وجود نوع ما عرّفته في مكان عميق ضمن التسلسل الهرمي وذلك عندما يرغبون باستخدامه، كما قد يسبب الاضطرار إلى إدخال المسار التالي بعض الازعاج: use my_crate::some_module::another_module::UsefulType;‎ بدلًا من استخدام: use my_crate::UsefulType;‎ الخبر السار هو أنه إذا لم يكن الهيكل مناسبًا للآخرين لاستخدامه من مكتبة أخرى، فلن تضطر إلى إعادة ترتيب التنظيم الداخلي، إذ يمكنك إعادة تصدير العناصر بدلًا من ذلك لإنشاء هيكل عام مختلف عن هيكلتك الخاصة باستخدام pub use. تأخذ عملية إعادة التصدير عنصرًا عامًا من مكان ما وتجعله عامًا في مكان آخر، كما لو جرى تعريفه في موقع آخر عوضًا عن ذلك. على سبيل المثال، لنفترض أننا أنشأنا مكتبة باسم art لنمذجة المفاهيم الفنية، بحيث يوجد داخل هذه المكتبة وحدتان: وحدة kinds تحتوي على معدّدَين enums باسم PrimaryColor و SecondaryColor ووحدة utils تحتوي على دالة تدعى mix، كما هو موضح في الشيفرة 3: اسم الملف: src/lib.rs //! # Art //! //! مكتبة لنمذجة المفاهيم الفنية pub mod kinds { /// الألوان الأساسية طبقًا لنموذج‪ ‪‪‪RYB pub enum PrimaryColor { Red, Yellow, Blue, } /// الألوان الثانوية طبقًا لنموذج‪ RYB pub enum SecondaryColor { Orange, Green, Purple, } } pub mod utils { use crate::kinds::*; /// دمج لونين أساسيين بقيم متساوية لإنشاء لون ثانوي pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor { // --snip-- } } [الشيفرة 3: مكتبة art التي تحتوي على عناصر منظمة ضمن الوحدتين kinds و utils] يوضح الشكل 3 كيف ستبدو الصفحة الأولى لتوثيق الوحدة المصرفة التي أُنشئت بواسطة cargo doc: [الشكل 3: الصفحة الأولى لتوثيق art الذي توضّح الوحدتين kinds و utils] لاحظ أن النوعين PrimaryColor و SecondaryColor غير مُدرجين في الصفحة الأولى وكذلك دالة mix، إذ يجب علينا النقر على kinds و utils لرؤيتهما. ستحتاج وحدة مصرفة أخرى تعتمد على هذه المكتبة إلى عبارات use، لتجلب العناصر الموجودة في art إلى النطاق، وبالتالي تحديد هيكل الوحدة المعرّفة حاليًا. تُظهر الشيفرة 4 مثالاً على الوحدة المصرفة التي تستخدم عناصر PrimaryColor و mix من الوحدة المصرفة art: اسم الملف: src/main.rs use art::kinds::PrimaryColor; use art::utils::mix; fn main() { let red = PrimaryColor::Red; let yellow = PrimaryColor::Yellow; mix(red, yellow); } [الشيفرة 4: وحدة مصرفة تستخدم عناصر الوحدة المصرفة art مع تصدير هيكلها الداخلي] ينبغي على مؤلف الشيفرة في الشيفرة 4 التي تستخدم الوحدة المصرفة art أن يعرّف أن PrimaryColor موجود في الوحدة kinds وأن mix موجودة في الوحدة utils. هيكل الوحدة المصرفة art مناسب أكثر للمطورين العاملين على الوحدة المصرفة art مقارنةً بمن سيستخدمها، إذ لا يحتوي الهيكل الداخلي على أي معلومات مفيدة لشخص يحاول فهم كيفية استخدام الوحدة المصرفة art، بل يتسبب الهيكل الداخلي باللّبس لأن المطورين الذين يستخدمونها يجب أن يعرفوا أيّ المسارات التي يجب عليهم الذهاب إليها كما يجب عليهم تحديد أسماء الوحدات في عبارات use. لإزالة التنظيم الداخلي من الواجهة البرمجية العامة يمكننا تعديل شيفرة الوحدة المصرفة art في الشيفرة 3 لإضافة تعليمات pub use لإعادة تصدير العناصر للمستوى العلوي كما هو موضح في الشيفرة 5: اسم الملف: src/lib.rs //! # Art //! //! مكتبة لنمذجة المفاهيم الفنية pub use self::kinds::PrimaryColor; pub use self::kinds::SecondaryColor; pub use self::utils::mix; pub mod kinds { // --snip-- } pub mod utils { // --snip-- } [الشيفرة 5: إضافة تعليمات pub use لإعادة تصدير العناصر] سيُدرِج توثيق الواجهة البرمجية التي يولدها الأمر cargp doc لهذه الوحدة المصرفة ويعيد تصديرها على الصفحة الأولى كما هو موضح في الشكل 4 جاعلًا النوعَين PrimaryColor و SecondaryColor ودالة mix أسهل للإيجاد. [الشكل 4: الصفحة الأولى لتوثيق art التي تعرض عمليات إعادة التصدير] يمكن لمستخدمي الوحدة المصرفة art أن يروا ويستخدموا الهيكلة الداخلية من الشيفرة 3 كما هو موضح في الشيفرة 4 أو يمكنهم استخدام هيكل أكثر سهولة للاستخدام في الشيفرة 5 كما هو موضح في الشيفرة 6: اسم الملف: src/main.rs use art::mix; use art::PrimaryColor; fn main() { // --snip-- } [الشيفرة 6: برنامج يستخدم العناصر المعاد تصديرها من الوحدة المصرفة art] يمكن -في الحالات التي يوجد فيها العديد من الوحدات المتداخلة nested modules- أن تحدث عملية إعادة تصدير الأنواع في المستوى العلوي باستخدام pub use فرقًا واضحًا على تجربة الأشخاص في استخدام الوحدة المصرّفة. الاستخدام الشائع الآخر للتعليمة pub use هو إعادة تصدير تعريفات الاعتمادية في الوحدة المصرفة الحالية لجعل تعريفات تلك الوحدة المصرفة جزءًا من الواجهة البرمجية العامة لوحدتك المصرفة. يُعد إنشاء بنية واجهة برمجية عامة مفيدة فنًا أكثر من كونه علمًا، ويمكنك تكرار المحاولة حتى تعثر على واجهة برمجية تعمل بصورةٍ أفضل لمستخدميها، ويمنحك اختيار pub use مرونةً في كيفية هيكلة وحدتك المصرفة داخليًا وفصل هذه الهيكلة الداخلية عما تقدمه للمستخدمين. ألقِ نظرةً على الشيفرات البرمجية الخاصة ببعض الوحدات المصرفة التي ثبّتتها لمعرفة ما إذا كانت هيكلتها الداخلية مختلفة عن الواجهة البرمجية العامة. إنشاء حساب Crates.io قبل أن تتمكن من نشر أي وحدات مصرفة، تحتاج إلى إنشاء حساب على crates.io والحصول على رمز واجهة برمجية مميز API token، ولفعل ذلك، انقر على زر الصفحة الرئيسية على crates.io وسجّل الدخول عبر حساب غيت هب GitHub، إذ يُعد حساب غيت هب أحد المتطلبات حاليًا، ولكن قد يدعم الموقع طرقًا أخرى لإنشاء حساب في المستقبل. بمجرد تسجيل الدخول، اذهب إلى إعدادات حسابك على https://crates.io/me واسترجع مفتاح API. ثم نفّذ الأمر cargo login باستخدام مفتاح API الخاص بك، كما يلي: $ cargo login abcdefghijklmnopqrstuvwxyz012345 يُعلم هذا الأمر كارجو برمز API الخاص بك وتخزينه محليًا في "‎~/.cargo/credentials". لاحظ أن هذا الرمز هو سر، فلا تشاركه مع أي شخص آخر، وإذا شاركته مع أي شخص لأي سبب من الأسباب، فيجب عليك إبطاله وإنشاء رمز مميز جديد على crates.io. إضافة بيانات وصفية لوحدة مصرفة جديدة لنفترض أن لديك وحدة مصرفة تريد نشرها، ستحتاج قبل النشر إلى إضافة بعض البيانات الوصفية في قسم [package] داخل ملف Cargo.toml الخاص بالوحدة المصرفة. ستحتاج وحدتك المصرفة إلى اسم مميز، إذ يُمكنك تسمية الوحدة المصرفة أثناء عملك على وحدة مصرفة محليًا كما تريد، ومع ذلك، تُخصَّص أسماء الوحدات المصرفة على crates.io على أساس من يأتي أولًا يُخدم أولًا first-come, first-served. بمجرد اختيار اسم لوحدة مصرفة ما، لا يمكن لأي شخص آخر نشر وحدة مصرفة بهذا الاسم. قبل محاولة نشر وحدة مصرفة، ابحث عن الاسم الذي تريد استخدامه، فإذا كان الاسم مستخدمًا، ستحتاج إلى البحث عن اسم آخر وتعديل حقل name في ملف Cargo.toml في قسم [package] لاستخدام الاسم الجديد للنشر، كما يلي: اسم الملف: Cargo.toml [package] name = "guessing_game" حتى إذا اخترت اسمًا مميزًا، عند تنفيذ cargo publish لنشر الوحدة المصرفة في هذه المرحلة، ستتلقى تحذيرًا ثم خطأ: $ cargo publish Updating crates.io index warning: manifest has no description, license, license-file, documentation, homepage or repository. See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info. --snip-- error: failed to publish to registry at https://crates.io Caused by: the remote server responded with an error: missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for how to upload metadata تحدث هذه الأخطاء بسبب افتقاد بعض المعلومات المهمة؛ إذ أن الوصف والترخيص مطلوبان حتى يعرف الأشخاص ما تفعله الوحدة المصرفة الخاصة بك وتحت أي شروط يمكنهم استخدامها. أضف وصفًا في Cargo.toml بحيث يكون مجرد جملة أو جملتين، لأنه سيظهر مع الوحدة المصرفة الخاصة بك في نتائج البحث، أما بالنسبة لحقل license، فأنت بحاجة لمنح قيمة معرّف الترخيص. تُدرج مؤسسة لينكس لتبادل بيانات حزم البرمجيات Linux Foundation’s Software Package Data Exchange -أو اختصارًا SPDX- المعرّفات التي يمكنك استخدامها لهذه القيمة. على سبيل المثال، لتحديد أنك رخّصت وحدتك المصرفة باستخدام ترخيص MIT، أضف معرف MIT: اسم الملف: Cargo.toml [package] name = "guessing_game" license = "MIT" إذا أردت استخدام ترخيص غير موجود في SPDX، فأنت بحاجة إلى وضع نص هذا الترخيص في ملف، وتضمين الملف في مشروعك، ثم استخدام license-file لتحديد اسم هذا الملف بدلاً من ذلك من استخدام المفتاح license. التوجيه بشأن الترخيص المناسب لمشروعك هو خارج نطاق هذا الكتاب. يرخِّص الكثير من الأشخاص في مجتمع رست مشاريعهم بنفس طريقة رست ألا وهي باستخدام ترخيص مزدوج من "MIT OR Apache-2.0". تدلّك هذه الممارسة على أنه بإمكانك أيضًا تحديد معرّفات ترخيص متعددة مفصولة بـ OR لتضمين تراخيص متعددة لمشروعك. باستخدام الاسم المميز والإصدار والوصف والترخيص المضاف، أصبح ملف Cargo.toml الخاص بالمشروع جاهزًا للنشر على النحو التالي: اسم الملف: Cargo.toml [package] name = "guessing_game" version = "0.1.0" edition = "2021" description = "A fun game where you guess what number the computer has chosen." license = "MIT OR Apache-2.0" [dependencies] يصف توثيق كارجو البيانات الوصفية الأخرى التي يمكنك تحديدها للتأكد من أن الآخرين يمكنهم اكتشاف واستخدام وحدة التصريف الخاصة بك بسهولة أكبر. النشر على Crates.io الآن وبعد أن أنشأت حسابًا، وحفظت رمز API، واخترت اسمًا للوحدة المصرفة، وحددت البيانات الوصفية المطلوبة، فأنت جاهزٌ للنشر، إذ يؤدي نشر وحدة مصرفة إلى رفع إصدار معين إلى crates.io ليستخدمه الآخرون. كن حذرًا، لأن النشر دائم، ولا يمكن الكتابة فوق الإصدار مطلقًا، ولا يمكن حذف الشيفرة البرمجية. يتمثل أحد الأهداف الرئيسة لموقع crates.io بالعمل مثل أرشيف دائم للشيفرة البرمجية بحيث تستمر عمليات إنشاء جميع المشاريع التي تعتمد على الوحدات المصرفة من crates.io في العمل، والسماح بحذف نسخة ما سيجعل تحقيق هذا الهدف مستحيلًا، ومع ذلك، لا يوجد حد لعدد إصدارات الوحدات المصرفة التي يمكنك نشرها. نفّذ الأمر cargo publish مرةً أخرى. يجب أن تنجح الآن: $ cargo publish Updating crates.io index Packaging guessing_game v0.1.0 (file:///projects/guessing_game) Verifying guessing_game v0.1.0 (file:///projects/guessing_game) Compiling guessing_game v0.1.0 (file:///projects/guessing_game/target/package/guessing_game-0.1.0) Finished dev [unoptimized + debuginfo] target(s) in 0.19s Uploading guessing_game v0.1.0 (file:///projects/guessing_game) تهانينا، فقد شاركت الآن الشيفرة الخاصة بك مع مجتمع رست، ويمكن لأي أحدٍ بسهولة إضافة الوحدة المصرفة الخاصة بك مثل اعتمادية لمشروعه. نشر نسخة جديدة لوحدة مصرفة موجودة مسبقا عندما تُجري تغييرات على الوحدة المصرفة الخاصة بك وتكون جاهزًا لطرح إصدار جديد، فإنك تغيّر قيمة version المحددة في ملف Cargo.toml الخاص بك وتعيد النشر. استخدم قواعد الإدارة الدلالية لنُسخ البرمجيات Semantic Versioning rules لتحديد رقم الإصدار التالي المناسب بناءً على التغييرات التي أجريتها، ومن ثم نفّذ cargo publish لرفع الإصدار الجديد. تعطيل النسخ من Crates.io باستخدام cargo yank على الرغم من أنه لا يمكنك إزالة الإصدارات السابقة للوحدة المصرفة، إلا أنه يمكنك منع أي مشاريع مستقبلية من إضافتها مثل اعتمادية جديدة، ويكون هذا مفيدًا عندما يُعطَّل إصدار الوحدة المصرفة لسبب أو لآخر، وفي مثل هذه الحالات، يدعم كارجو سحب yanking إصدار وحدة مصرفة. يمنع سحب إصدار ما المشاريع الجديدة من الاعتماد على هذا الإصدار مع السماح لجميع المشاريع الحالية التي تعتمد عليه بالاستمرار، إذ يعني السحب أن جميع المشاريع التي تحتوي على Cargo.lock لن تتعطّل، ولن تستخدم أي ملفات Cargo.lock المستقبلية المنشأة الإصدار المسحوب. لسحب نسخة من وحدة مصرفة، نفّذ cargo yank في دليل الوحدة المصرفة الذي نشرتَه سابقًا، وحدّد أي إصدار تريد إزالته. على سبيل المثال، إذا نشرنا وحدة مصرفة باسم guessing_game الإصدار 1.0.1 وأردنا انتزاعه، في مجلد المشروع guessing_game ننفّذ ما يلي: $ cargo yank --vers 1.0.1 Updating crates.io index Yank guessing_game@1.0.1 يمكنك أيضًا التراجع عن عملية السحب من خلال إضافة undo-- إلى الأمر والسماح للمشاريع بالاعتماد على الإصدار مرة أخرى: $ cargo yank --vers 1.0.1 --undo Updating crates.io index Unyank guessing_game_:1.0.1 لا تحذف عملية السحب أي شيفرة برمجية، إذ من غير الممكن على سبيل المثال حذف بيانات حساسة رُفعَت بالخطأ. إذا حدث ذلك، يجب عليك إعادة تعيين تلك البيانات على الفور. ترجمة -وبتصرف- لقسم من الفصل More About Cargo and Crates.io من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: مساحة عمل كارجو Cargo Workspaces في لغة رست وتحميل حزمة من crates.io المقال السابق: الاختيار بين الحلقات Loops والمكررات Iterators في لغة رست كتابة برنامج سطر أوامر Command Line بلغة رست Rust المسارات paths والنطاق الخاص بها في لغة رست Rust
  11. تعرفنا في الفصل السابق "معالجة سلسلة من العناصر باستخدام المكررات iterators" والذي يليه "استخدام المكررات Iterators في تطبيق سطر أوامر" على المكررات Iterators وكيفية استخدامها عمليًا ولعل السؤال الذي خطر ببالك الآن بعد قراءتهما هو: أيّ طرق التطبيق ينبغي عليك استعمالها عند برمجتك لبرنامجك ولماذا؟ التطبيق الأصلي في الشيفرة 21، أم الإصدار باستخدام المكررات في الشيفرة 22 مقال "استخدام المكررات Iterators في تطبيق سطر أوامر بلغة رست"؟ يفضِّل معظم مبرمجي لغة رست استخدام المكررات، وعلى الرغم من أن هذه الطريقة أصعب فهمًا في البداية إلا أنك ستفضلها بعد الاعتياد عليها وعلى محولات المكررات المختلفة، إذ ستركّز عندها شيفرتك البرمجية على الهدف العام للحلقة بدلًا من إضاعة الوقت في ضبط الأجزاء المختلفة من الحلقات وإنشاء أشعة جديدة. تخلّصنا هذه الطريقة من الكثير من الشيفرات البرمجية الاعتيادية بحيث يكون من الأسهل رؤية المقصد الأساسي من الشيفرة البرمجية على نحوٍ فريد لكل استخدام، مثل ترشيح كل عنصر في المكرر بحسب شرط ما. لكن هل الطريقتان متساويتان حقًا؟ يقول الافتراض المنطقي: الحلقة التي يكون تطبيقها على مستوى منخفض أسرع. دعنا نتناقش بالتفاصيل. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن المقارنة بين أداء الحلقات والمكررات عليك معرفة أيّ التطبيقين أسرع، الحلقات أم المكررات؟ وذلك لتحديد متى يجب عليك استخدام أحد التطبيقين؛ هل إصدار الدالة search (من المقال السابق) باستخدام حلقة for أسرع أم إصدار المكررات؟ ننفّذ اختبار أداء بكتابة كامل محتويات رواية "مغامرات شيرلوك هولمز The Adventures of Sherlock Holmes" لكاتبها سير آرثر كونان دويل Sir Arthur Conan Doyle إلى String والبحث عن الكلمة "the" في المحتويات. إليك نتائج اختبار الأداء على كلا الإصدارين لدالة search باستخدام حلقة for، وباستخدام المكرّرات: test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700) test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200) كان إصدار المكررات أسرع قليلًا. لن نشرح الشيفرة البرمجية الخاصة باختبار الأداء هنا، لأن الهدف من المقال ليس برهنة أن الإصدارين متساويين بالأداء بل هو لملاحظة أداء كل طريقة بالنسبة للأخرى. ينبغي عليك التحقق من نصوص متفاوتة الطول للوسيط contents للحصول على اختبار أداء أكثر وضوًحا، واستخدام كلمات مختلفة من أطوال متفاوتة للوسيط query وتجربة مختلف أنواع الحالات. ما نريد الوصول إليه هنا هو التالي: على الرغم من أن المكررات تستخدم تطبيقًا مجرّدًا عالي المستوى، إلا أنها تُصرَّف إلى شيفرة برمجية مماثلة لشيفرة تطبيق منخفض المستوى كتبتها بنفسك، إذ أن المكررات هي من الطرق المجرّدة عديمة الحمل zero-cost abstraction في رست، وهذا يعني أن استخدام التجريد لن يؤثر على وقت التشغيل. هذا الأمر مماثل لكيفية تعريف بيارن ستروستروب Bjarne Stroustrup مصمّم لغة سي بلس بلس C++‎ ومطبّقها لمبدأ انعدام الحمل غير المباشر zero-overhead في كتابه "أساسيات سي بلس بلس Foundations of C++‎"‏ (2012): الشيفرة البرمجية التالية هي مثال آخر مأخوذ من برنامج فك ترميز صوت، إذ تستخدم خوارزمية فك الترميز عملية التوقع الخطي الرياضي لتوقّع القيم المستقبلية بناءً على دالة خطية تأخذ العينات السابقة. تستخدم هذه الشيفرة البرمجية سلسلة مكررات لإنجاز بعض العمليات الرياضية على ثلاثة متغيرات موجودة في النطاق scope، هي: buffer شريحة البيانات، ومصفوفة من 12 عنصر coefficients وأي مقدار إزاحة بالبيانات مُخزَّن في qlb_shift، وقد صرّحنا عن هذه المتغيرات داخل المثال دون إسناد أي قيمة لها. على الرغم من أن هذه الشيفرة البرمجية ليست مفيدة خارج سياقها إلا أنها مثال مختصر وواقعي على كيفية ترجمة رست للأفكار والتطبيقات عالية المستوى إلى مستوى منخفض. let buffer: &mut [i32]; let coefficients: [i64; 12]; let qlp_shift: i16; for i in 12..buffer.len() { let prediction = coefficients.iter() .zip(&buffer[i - 12..i]) .map(|(&c, &s)| c * s as i64) .sum::<i64>() >> qlp_shift; let delta = buffer[i]; buffer[i] = prediction as i32 + delta; } تمرّ الشيفرة البرمجية على القيم الاثنا عشر الموجودة في coefficients وتستخدم التابع zip لاقتران القيم الاثنا عشر الحالية مع القيم الاثني العشر السابقة الموجودة في buffer وذلك لحساب قيمة التوقع prediction، ثم نضرب قيمة كل زوج على حدى ونجمع النتيجة ونُزيح shift البتات bits في نتيجة الجمع بمقدار qlb_shift بت إلى اليمين. تركّز العمليات الحسابية في تطبيقات مثل فك ترميز الصوت على الأداء في المقام الأول. نُنشئ هنا مكررًا باستخدام محوّلَين adaptor ومن ثم نستهلك القيمة. ما هي شيفرة أسيمبلي Assembly التي ستُصرَّف إليها شيفرة رست هذه؟ تُصرَّف حتى الوقت الحالي إلى نفس شيفرة أسيمبلي التي قد تكتبها بنفسك. ليس هناك أي حلقة للمكرر الخاص بالقيم coefficients، إذ تعرف رست أن هناك 12 تكرارًا فقط، لذا فهي تنشر الحلقة؛ وعملية النشر unrolling هي عملية لتحسين الأداء، إذ يُزال فيها الحمل الإضافي غير المباشر الخاص بالشيفرة البرمجية المُتحكمة بالحلقة وتُولَّد شيفرة برمجية متكررة بدلًا من ذلك للشيفرة البرمجية في كل تكرار من الحلقة. تُخزَّن جميع المعاملات في المسجلات registers، مما يعني أن الوصول إلى القيم عملية سريعة، ولا يوجد هناك أي تحقق للقيود على مصفوفة الوصول access array خلال وقت التشغيل. ترفع جميع هذه الخطوات التي تفعلها رست لتحسين الأداء من فعالية أداء الشيفرة البرمجية الناتجة كثيرًا. الآن وبعد معرفتك لهذا، يمكنك استخدام المكررات والمغلفات دون أي خوف، إذ يجعلان من شيفرتك البرمجية ذات مستوى تطبيقي أعلى دون التفريط بسرعة الأداء عند وقت التشغيل. ترجمة -وبتصرف- لقسم من الفصل Functional Language Features: Iterators and Closures من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: تخصيص نسخ مشروع بلغة رست ونشرها على crates.io المقال السابق: استخدام المكررات Iterators في تطبيق سطر أوامر بلغة رست بنية match للتحكم بسير برامج لغة رست Rust التحكم بسير تنفيذ برامج راست Rust
  12. يمكننا الآن تحسين مشروع سطر الأوامر الذي نفذناه في فصل سابق بعنوان كتابة برنامج سطر أوامر Command Line بلغة رست Rust بعد تعرُّفنا على المكرّرات في المقال السابق معالجة سلسلة من العناصر باستخدام المكررات iterators، إذ سنستخدمها لجعل شيفرتنا البرمجية أكثر وضوحًا واختصارًا. دعنا نلقي نظرةً على طريقة تطبيق المكررات في مشروعنا والتي ستجعل منه إصدارًا محسنًا خاصةً على الدالتين Config::build و search. إزالة clone باستخدام المكرر أضفنا في الشيفرة 6 (من فصل كتابة برنامج سطر أوامر بلغة رست: إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء) شيفرةً برمجية تأخذ شريحةً slice من القيم ذات النوع String وأنشأنا بها نسخةً instance من الهيكل Config بالمرور على الشريحة ونسخ القيم والسماح بالهيكل Config بامتلاك هذه القيم. سنُعيد كتابة التطبيق ذاته الخاص بالدالة Config::build في الشيفرة 17 كما كانت في الشيفرة 23 (الفصل 12): اسم الملف: src/lib.rs impl Config { pub fn build(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let file_path = args[2].clone(); let ignore_case = env::var("IGNORE_CASE").is_ok(); Ok(Config { query, file_path, ignore_case, }) } } [الشيفرة 17: إعادة بناء الدالة Config::build من الشيفرة 23 (الفصل 12)] ذكرنا وقتها أنه ليس علينا القلق بخصوص استدعاءات clone غير الفعالة لأننا سنزيلها في المستقبل. حسنًا، أتى الوقت الآن. نحتاج clone هنا لوجود شريحة بعناصر String في المعامل args، إلا أن الدالة build لا تمتلك args، ولإعادة ملكية نسخة Config، اضطررنا لنسخ القيم من الحقول query و file_path الموجودة في الهيكل Config بحيث تمتلك نسخة Config القيم الموجودة في حقوله. أصبح بإمكاننا -بمعرفتنا الجديدة بخصوص المكررات- التعديل على الدالة build لأخذ ملكية مكرر مثل وسيط لها بدلًا من استعارة شريحة، وسنستخدم وظائف المكرر بدلًا من الشيفرة البرمجية التي تتحقق من طول السلسلة النصية وتمرّ على عناصرها في مواقع محددة، وسيوضِّح ذلك استخدام Config::build لأن المكرر يستطيع الحصول على هذه القيم. بعد أخذ الدالة Config::build لملكية المكرر وتوقف استخدامها لعمليات الفهرسة indexing التي تستعير القيم، أصبح بإمكاننا نقل قيم String من المكرر إلى الهيكل Config بدلًا من استدعاء clone وإنشاء تخصيص allocation جديد. إزالة المكرر المعاد مباشرة افتح ملف src/main.rs الخاص بمشروع الدخل والخرج، والذي يجب أن يبدو مماثلًا لما يلي: اسم الملف: src/main.rs fn main() { let args: Vec<String> = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {err}"); process::exit(1); }); // --snip-- } سنعدّل أولًا بداية الدالة main التي كانت موجودة في الشيفرة 24 من فصل سابق لتكون الشيفرة 18 التي تستخدم مكررًا، إلا أنها لن تُصرَّف بنجاح إلا بعد تعديلنا للدالة Config::build أيضًا. اسم الملف: src/main.rs fn main() { let config = Config::build(env::args()).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {err}"); process::exit(1); }); // --snip-- } [الشيفرة 18: تمرير القيمة المعادة من env::args إلى Config::build] تُعيد الدالة env::args مكررًا، إذ يمكننا تمرير ملكية المكرّر المُعاد من env::args إلى Config::build الآن مباشرةً بدلًا من تجميع قيم المكرر في شعاع ثم تمرير شريحة إلى الدالة Config::build. نحتاج إلى تحديث تعريف الدالة Config::build في ملف src/lib.rs الخاص بمشروعنا. دعنا نغير بصمة الدالة Config::build لتبدو على نحوٍ مماثل للشيفرة 19، إلا أن هذا لن يُصرَّف بنجاح لأننا بحاجة لتحديث متن الدالة أيضًا. اسم الملف: src/lib.rs impl Config { pub fn build( mut args: impl Iterator<Item = String>, ) -> Result<Config, &'static str> { // --snip-- [الشيفرة 19: تحديث بصمة الدالة Config::build بحيث تتوقع تمرير مكرر] يوضّح توثيق المكتبة القياسية بالنسبة للدالة env::args بأن نوع المكرر المُعاد هو std::env::Args وأن هذا النوع يطبّق السمة Iterator ويُعيد قيمًا من النوع String. حدّثنا بصمة الدالة Config::build بحيث يحتوي المعامل args على نوع معمم generic type بحدّ السمة trait bound impl Iterator<Item = String>‎‎ بدلًا من ‎&[String]‎. ناقشنا الصيغة impl Trait سابقًا وهي تعني أن args يمكن أن يكون أي نوع يطبّق النوع Iterator ويُعيد عناصرًا من النوع String. يمكننا إضافة الكلمة المفتاحية mut إلى توصيف المعامل args لجعله متغيّرًا mutable بالنظر إلى أننا نأخذ ملكية args وسنعدّل args بالمرور ضمنه. استخدام توابع السمة Iterator بدلا من الفهرسة سنصحح متن الدالة Config::build، فنحن نعلم أنه بإمكاننا استدعاء التابع next على args لأنها تطبّق السمة Iterator. نحدّث في الشيفرة 20 الشيفرة البرمجية التي كانت موجودة في الشيفرة 23 بحيث نستخدم التابع next: اسم الملف: src/lib.rs impl Config { pub fn build( mut args: impl Iterator<Item = String>, ) -> Result<Config, &'static str> { args.next(); let query = match args.next() { Some(arg) => arg, None => return Err("Didn't get a query string"), }; let file_path = match args.next() { Some(arg) => arg, None => return Err("Didn't get a file path"), }; let ignore_case = env::var("IGNORE_CASE").is_ok(); Ok(Config { query, file_path, ignore_case, }) } } [الشيفرة 20: تعديل محتوى الدالة Config::build بحيث نستخدم توابع المكرر] تذكّر أن أول قيمة من القيم المُعادة من الدالة env::args هي اسم البرنامج، لذا نريد تجاهلها والحصول على القيمة التي تليها، ولذلك نستدعي next أولًا دون فعل أي شيء بالقيمة المُعادة، ثم نستدعي next مرةً أخرى للحصول على القيمة التي نريد وضعها في حقل query من الهيكل Config. إذا أعادت next القيمة Some، سنستخدم match لاستخلاص القيمة، أما إذا أعادت None، فهذا يعني عدم وجود وسطاء كافية من المستخدم وعندها نُعيد من الدالة القيمة Err مبكرًا، ونفعل ذلك مجددًا للتعامل مع القيمة file_path. جعل الشيفرة البرمجية أكثر وضوحا باستخدام محولات المكرر يمكننا أيضًا استغلال ميزة من مزايا المكررات في الدالة search ضمن مشروعنا، وهي موضّحة في الشيفرة 21 والشيفرة 19 (من فصل سابق): اسم الملف: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } [الشيفرة 21: تطبيق الدالة search من الشيفرة 19 (الفصل 12)] يمكننا كتابة هذه الشيفرة البرمجية بطريقة مختصرة باستخدام توابع محولات المكرر، إذ يسمح لنا ذلك أيضًا بوجود شعاع وسيط متغيّر نسميه results. يفضِّل أسلوب لغات البرمجة العمليّة تقليل كمية الحالات المتغيّرة لجعل الشيفرة البرمجية أكثر وضوحًا، إذ قد تفتح لنا إزالة الحالة المتغيّرة فرصةً لجعل عمليات البحث تُنفَّذ بصورةٍ متزامنة، لأنه لن يكون علينا حينها إدارة الوصول المتتالي إلى الشعاع results. توضح الشيفرة 22 هذا التغيير: اسم الملف: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { contents .lines() .filter(|line| line.contains(query)) .collect() } [الشيفرة 22: استخدام توابع محول المكرر في تطبيق الدالة search] تذكر أن الهدف من الدالة search هو إعادة جميع الأسطر الموجودة في contents التي تحتوي على query. تستخدم هذه الشيفرة البرمجية -بصورةٍ مشابهة لمثال filter في الشيفرة 16- محوّل filter للمحافظة على السطور التي يُعيد فيها التعبير line.contains(query)‎ القيمة true. يمكننا تجميع الأسطر الناتجة في شعاع آخر باستخدام collect. هذه الطريقة أبسط بكثير. جرّب تنفيذ التعديلات ذاتها بحيث تستخدم توابع المكرر في الدالة search_case_insensitive بصورةٍ مشابهة. ترجمة -وبتصرف- لقسم من الفصل Functional Language Features: Iterators and Closures من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: الاختيار بين الحلقات Loops والمكررات Iterators في لغة رست المقال السابق: معالجة سلسلة من العناصر باستخدام المكررات iterators في لغة رست برمجة لعبة تخمين الأرقام بلغة رست Rust كتابة الاختبارات في لغة رست Rust
  13. يسمح لك نمط المكرّر iterator pattern بإنجاز مهمة ما على سلسلة من العناصر بصورةٍ متتالية، والمكرّر مسؤول عن المنطق الخاص بالمرور على كل عنصر وتحديد مكان انتهاء السلسلة، إذ ليس من الواجب عليك إعادة تطبيق هذا المنطق بنفسك عند استخدام المكررات. المكررات في رست كسولة، بمعنى أنها لا تمتلك أي تأثير حتى تستدعي أنت التابع الذي يستخدم المكرر، فعلى سبيل المثال نُنشئ الشيفرة 10 مكرّرًا على العناصر الموجودة في الشعاع ‏ v1 باستدعاء التابع iter المعرّف على Vec<T>‎، ولا تفعل تلك الشيفرة البرمجية أي شيء مفيد. let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); [الشيفرة 10: إنشاء مكرر] نخزّن المكرر في المتغير v1_iter، ويمكننا استخدامه بطرق عدّة بعد إنشائه. على سبيل المثال، استخدمنا حلقة for في الشيفرة 5 من فصل سابق للمرور على مصفوفة وذلك لتنفيذ شيفرة برمجية على كل من عناصرها، وفي الحقيقة كان المكرّر موجودًا ضمنيًا في تلك الشيفرة لتحقيق ذلك إلا أننا لم نتكلم عن ذلك سابقًا. نفصل في الشيفرة 11 إنشاء المكرر من استخدامه في الحلقة for، فعندما تُستدعى الحلقة for باستخدام المكرر v1_iter، يُستخدم كل عنصر في المكرر مرةً تلو الأخرى في كل دورة للحلقة، وهذا يطبع كل قيمة من قيم العناصر. let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("Got: {}", val); } [الشيفرة 11: استخدام مكرر في حلقة for] يمكنك كتابة شيفرة برمجية تفعل الأمر نفسه في لغات البرمجة التي لا تستخدم المكررات في مكتبتها القياسية وذلك عن طريق البدء بالمتغير من الدليل‏ 0 واستخدام قيمة المتغير يمثابة دليلٍ للشعاع للحصول على قيمة ومن ثم زيادة قيمة المتغير في الحلقة حتى تصل قيمته لعدد العناصر الكلية في الشعاع. تتعامل المكررات مع المنطق البرمجي نيابةً عنك مما يقلل من الشيفرات البرمجية المتكررة التي من الممكن أن تتسبب بأخطاء، وتعطيك المكررات مرونةً أكبر في التعامل مع المنطق ذاته في الكثير من أنواع السلاسل وليس فقط هياكل البيانات data structures التي يمكنك الوصول إلى قيمها باستخدام دليل مثل الشعاع. دعنا نلقي نظرةً إلى كيفية تحقيق المكررات لكل هذا. سمة Iterator وتابع next تطبّق جميع المكررات السمة Iterator المُعرّفة في المكتبة القياسية، ويبدو تعريف السمة شيء مماثل لهذا: pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // methods with default implementations elided } لاحظ أن هذا التعريف يستخدم صيغتان جديدتان، هما: type Item و Self::Item اللتان تُعرِّفان نوعًا مترابطًا associated type مع هذه السمة. سنتحدث عن الأنواع المترابطة بالتفصيل لاحقًا، ويكفي للآن معرفتك أن هذه الشيفرة البرمجية تقول أن تطبيق السمة Iterator يتطلب منك تعريف نوع Item أيضًا وهذا النوع مُستخدم مثل نوع مُعاد من التابع next، وبكلمات أخرى، سيكون النوع Item هو النوع المُعاد من المكرّر. تتطلب السمة Iterator ممن يطبّقها فقط أن يعرّف تابعًا واحدًا هو next، الذي يُعيد عنصرًا واحدًا من المكرر كل مرة ضمن Some وعندما تنتهي العناصر (ينتهي التكرار)، يُعيد None. يمكننا استدعاء التابع next على المكررات مباشرةً، وتوضح الشيفرة 12 القيم المُعادة من الاستدعاءات المتعاقبة على المكرّر باستخدام next وهو المكرر الموجود في الشعاع. اسم الملف: src/lib.rs #[test] fn iterator_demonstration() { let v1 = vec![1, 2, 3]; let mut v1_iter = v1.iter(); assert_eq!(v1_iter.next(), Some(&1)); assert_eq!(v1_iter.next(), Some(&2)); assert_eq!(v1_iter.next(), Some(&3)); assert_eq!(v1_iter.next(), None); } [الشيفرة 12: استدعاء التابع next على المكرر] لاحظ أننا احتجنا لإنشاء v1_iter متغيّر mutable، إذ يغيّر استدعاء التابع next على مكرر الحالة الداخلية التي يستخدمها المكرر لتتبع مكانه ضمن السلسلة، وبكلمات أخرى، تستهلك consumes الشيفرة البرمجية المكرّر، إذ يستهلك كل استدعاء للتابع next عنصرًا واحدًا من المكرر. لم يكن هناك أي حاجة لإنشاء v1_iter متغيّر عندما استخدمنا حلقة for لأن الحلقة أخذت ملكية ownership المكرر v1_iter وجعلته متغيّرًا ضمنيًا. لاحظ أيضًا أن القيم التي نحصل عليها من استدعاءات التابع next هي مراجع ثابتة immutable references للقيم الموجودة في الشعاع، ويعطينا التابع iter مكرًرًا على المراجع الثابتة. إذا أردنا إنشاء مكرر يأخذ ملكية v1 ويُعيد القيم المملوكة فيمكننا استدعاء into_iter بدلًا من iter، ويمكننا بصورةٍ مماثلة استدعاء iter_mut بدلًا من iter إذا أردنا المرور على مراجع متغيّرة. توابع تستهلك المكرر للسمة Iterator العديد من التوابع في تطبيقها الافتراضي والموجودة في المكتبة القياسية، ويمكنك الاطّلاع على هذه التوابع عن طريق النظر إلى توثيق واجهة المكتبة القياسية البرمجية للسمة Iterator. تستدعي بعض هذه التوابع التابع next في تعريفها وهو السبب في ضرورة تطبيق التابع next عند تطبيق السمة Iterator. تُسمّى التوابع التي تستدعي التابع next بالمحوّلات المُستهلكة consuming adaptors لأن استدعائها يستهلك المكرر. يُعد تابع sum مثال على هذه التوابع، فهو يأخذ ملكية المكرر ويمرّ على عناصره بصورةٍ متتالية مع استدعاء next مما يتسبب باستهلاك المكرر، وبينما يمرّ على العناصر فهو يجمع كل عنصر إلى قيمة كلية، ثم يعيد القيمة الكلية في النهاية. تحتوي الشيفرة 13 على اختبار يوضح استخدام التابع sum: اسم الملف: src/lib.rs #[test] fn iterator_sum() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); let total: i32 = v1_iter.sum(); assert_eq!(total, 6); } [الشيفرة 13: استدعاء التابع sum للحصول على القيمة الكلية لجميع العناصر في المكرر] لا يُسمح لنا باستخدام v1_iter بعد استدعاء sum لأن sum يأخذ ملكية المكرّر الذي نستدعي sum عليه. التوابع التي تنشئ مكررات أخرى محولات المكرر iterator adaptors هي توابع مُعرّفة على السمة Iterator ولا تستهلك المكرر، بل تُنشئ مكررات مختلفة بدلًا من ذلك عن طريق تغيير بعض خصائص المكرر الأصل. توضح الشيفرة 14 مثالًا عن استدعاء تابع محول مكرر map وهو تابع يأخذ مغلّفًا closure ويستدعيه على كل من العناصر بصورةٍ متتالية. يُعيد التابع map مكررًا جديدًا يُنشئ عناصرًا مُعدّلٌ عليها، ويُنشئ المغلف في هذه الحالة مكررًا جديدًا يزيد قيمة عناصره بمقدار 1 لكل عنصر في الشعاع: اسم الملف: src/main.rs let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); [الشيفرة 14: استدعاء محول المكرر map لإنشاء مكرر جديد] إلا أن الشيفرة البرمجية السابقة تعطينا إنذارًا: $ cargo run Compiling iterators v0.1.0 (file:///projects/iterators) warning: unused `Map` that must be used --> src/main.rs:4:5 | 4 | v1.iter().map(|x| x + 1); | ^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(unused_must_use)]` on by default = note: iterators are lazy and do nothing unless consumed warning: `iterators` (bin "iterators") generated 1 warning Finished dev [unoptimized + debuginfo] target(s) in 0.47s Running `target/debug/iterators` لا تفعل الشيفرة 14 أي شيء، إذ أن المغلف الذي حددناه لا يُستدعى أبدًا، ويذكرنا الإنذار بسبب ذلك: إذ أن محولات المكرر كسولة ونحتاج لاستهلاك المكرر هنا. نستخدم التابع collect لتصحيح هذا الإنذار واستهلاك المكرر وهو ما استخدمناه في (الشيفرة 1 من فصل سابق) باستخدام env::args، إذ يستهلك هذا التابع المكرر ويجمّع القيم الناتجة إلى نوع بيانات تجميعة collection data type. نجمع في الشيفرة 15 النتائج من عملية المرور على المكرر والمُعادة من استدعاء التابع map على الشعاع، وسيحتوي هذا الشعاع في نهاية المطاف على جميع عناصره الموجودة مع زيادة على قيمة كل منها بمقدار 1. اسم الملف: src/main.rs let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]); [الشيفرة 15: استدعاء التابع map لإنشاء مكرر جديد ومن ثم استدعاء التابع collect لاستهلاك المكرر الجديد وإنشاء شعاع] يمكننا تحديد أي عملية نريد إنجازها على كل عنصر بالنظر إلى أن map تتلقى مغلفًا بمثابة وسيط لها. هذا مثال عظيم عن كيفية إنجاز مهام معقدة بطريقة مقروءة، ولأن جميع المكررات كسولة، فهذا يعني أنك بحاجة استدعاء واحد من توابع المحولات المستهلكة للحصول على نتائج استدعاءات محولات المكرر. استخدام المغلفات التي تحصل على القيم من بيئتها تأخذ الكثير من محولات المكرر المغلفات مثل وسطاء لها، وستحصل هذه المغلفات التي تُحدد مثل وسطاء لمحولات المكرر على قيم من بيئتها غالبًا. نستخدم في هذا المثال التابع filter الذي يأخذ مغلّفًا، ويحصل المغلف على عنصر من المكرر ويُعيد bool، وتُضمّن هذه القيمة في التكرار المُنشئ بواسطة filter إذا أعاد المغلف القيمة true، وإذا أعاد المكرر القيمة false فلن تُضمَّن القيمة. نستخدم في الشيفرة 16 التابع filter بمغلّف يحصل على المتغير shoe_size من بيئته للمرور على تجميعة من نسخ instances الهيكل‏ Shoe، وسيُعيد فقط الأحذية ذات مقاس محدد. اسم الملف: src/lib.rs #[derive(PartialEq, Debug)] struct Shoe { size: u32, style: String, } fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> { shoes.into_iter().filter(|s| s.size == shoe_size).collect() } #[cfg(test)] mod tests { use super::*; #[test] fn filters_by_size() { let shoes = vec![ Shoe { size: 10, style: String::from("sneaker"), }, Shoe { size: 13, style: String::from("sandal"), }, Shoe { size: 10, style: String::from("boot"), }, ]; let in_my_size = shoes_in_size(shoes, 10); assert_eq!( in_my_size, vec![ Shoe { size: 10, style: String::from("sneaker") }, Shoe { size: 10, style: String::from("boot") }, ] ); } } [الشيفرة 16: استخدام التابع filter مع مغلف يحصل على القيمة shoe_size] تأخذ الدالة shoes_in_size ملكية شعاع الأحذية وقياس الحذاء مثل معاملات، وتُعيد شعاعًا يحتوي على الأحذية بالمقاس المحدد. نستدعي into_iter في متن الدالة shoes_in_size لإنشاء مكرر يأخذ ملكية الشعاع، ثم نستدعي filter لتحويل المكرّر إلى مكرر جديد يحتوي فقط على عناصر يُعيد منها المغلّف القيمة true. يحصل المغلف على المعامل shoe_size من البيئة ويقارنه مع قيمة كل مقاس حذاء مع إبقاء الأحذية التي يتطابق مقاسها، وأخيرًا يجمع استدعاء collect القيم المُعادة بواسطة محول المكرر إلى شعاع يُعاد من الدالة. يوضح هذا الاختبار أنه عندما نستدعي shoes_in_size، فنحن نحصل فقط على الأحذية التي تتطابق مقاساتها مع القيمة التي حددناها. ترجمة -وبتصرف- لقسم من الفصل Functional Language Features: Iterators and Closures من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: استخدام المكررات Iterators في تطبيق سطر أوامر بلغة رست المقال السابق: المغلفات closures في لغة رست Rust أنواع البيانات Data Types في لغة رست Rust التحكم بسير تنفيذ برامج راست Rust
  14. تمثّل المغلّفات في لغة رست دوالًا مجهولة anonymous يمكنك حفظها في متغير أو تمريرها مثل وسيط إلى دالة أخرى، ويمكنك إنشاء مغلف في مكان ما، ثم استدعاءه من مكان آخر ليُفَّذ بحسب سياق المكان، وعلى عكس الدوال فالمغلفات يمكنها الوصول إلى القيم الموجودة في النطاق المعرفة بها، وسنوضح كيف تسمح لنا مزايا المغلفات بإعادة استخدام شيفرتنا البرمجية وتخصيص سلوكها. الحصول على المعلومات من البيئة باستخدام المغلفات سنفحص أولًا كيفية استخدام المغلفات للحصول على القيم من البيئة التي عرّفناها فيها لاستخدام لاحق. إليك حالة استخدام ممكنة: لدينا شركة لبيع القمصان ونمنح شخصًا ما على قائمة مراسلة البريد الإلكتروني قميصًا حصريًا بين الحين والآخر مثل ترويج لشركتنا، ويمكن أن يُضيف الأشخاص على قائمة المراسلة لونهم المفضل إلى ملفهم بصورةٍ اختيارية، وإذا حدّد الشخص الذي سيحصل على قميص مجاني لونه المفضل فإنه يحصل على هذا اللون تحديدًا وإلا فإنه يحصل على اللون المتواجد بكثرة في المخزن. هناك عدة طرق لتطبيق ذلك، إذ يمكننا على سبيل المثال استخدام معدّد enum يدعى ShirtColor يحتوي على متغايرين variants هما‏ Red و Blue (حددنا لونين فقط للبساطة). نمثّل مخزن الشركة باستخدام الهيكل‏ Inventory الذي يحتوي على حقل يدعى shirts يحتوي على النوع Vec<ShirtColor>‎، الذي يمثّل لون القميص الموجود حاليًا في المخزن. يحصل التابع giveaway المُعرّف في Inventory على لون القميص المفضّل الاختياري للمستخدم من المستخدمين الرابحين القميص مجانًا ويُعيد لون القميص الذي سيحصل عليه المستخدم. التطبيق لكل ما سبق ذكره موضح في الشيفرة 1: اسم الملف: src/main.rss #[derive(Debug, PartialEq, Copy, Clone)] enum ShirtColor { Red, Blue, } struct Inventory { shirts: Vec<ShirtColor>, } impl Inventory { fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor { user_preference.unwrap_or_else(|| self.most_stocked()) } fn most_stocked(&self) -> ShirtColor { let mut num_red = 0; let mut num_blue = 0; for color in &self.shirts { match color { ShirtColor::Red => num_red += 1, ShirtColor::Blue => num_blue += 1, } } if num_red > num_blue { ShirtColor::Red } else { ShirtColor::Blue } } } fn main() { let store = Inventory { shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue], }; let user_pref1 = Some(ShirtColor::Red); let giveaway1 = store.giveaway(user_pref1); println!( "The user with preference {:?} gets {:?}", user_pref1, giveaway1 ); let user_pref2 = None; let giveaway2 = store.giveaway(user_pref2); println!( "The user with preference {:?} gets {:?}", user_pref2, giveaway2 ); } الشيفرة 1: شيفرة توزيع القمصان للمستخدمين يحتوي المتغير store المعرف في الدالة main على قميصين أحدهما باللون الأزرق والآخر باللون الأحمر للتوزيع ضمن حملة التسويق هذه. نستدعي التابع giveaway للمستخدم الذي يفضّل القميص الأحمر وللمستخدم الذي ليس لديه أي تفضيل معين. يمكن تطبيق الشيفرة البرمجية بمختلف الطرق، إلا أننا نركز هنا على استخدام المغلفات، لذا فقد التزمنا بالمفاهيم التي تعلمتها مسبقًا باستثناء ما بداخل التابع giveaway الذي يستخدم مغلّفًا. نحصل على تفضيل المستخدم مثل معامل من النوع Option<ShirtColor>‎ في التابع giveaway ونستدعي التابع unwrap_or_else على user_preference. التابع unwrap_or_else على النوع Option<T>‎ مُعرّف في المكتبة القياسية ويأخذ وسيطًا واحدًا ألا وهو مغلف دون أي وسطاء يعيد القيمة T (النوع ذاته المُخزن في المتغاير Some داخل النوع Option<T>‎، وفي هذه الحالة ShirtColor). إذا كان النوع Option<T>‎ هو المتغاير Some، سيُعيد التابع unwrap_or_else القيمة الموجودة داخل Some، وإذا كان المتغاير داخل النوع Option<T>‎ هو None، سيستدعي التابع المغلف ويُعيد القيمة المُعادة من المغلف. نحدد تعبير المغلف بالشكل ‎|| self.most_stocked()‎ مثل وسيط للتابع unwrap_or_else. لا يأخذ هذا المغلف أي معاملات، إذ نضع المعاملات بين الخطين العموديين إذا احتوى المغلف على معاملات. يستدعي متن المغلف التابع self.most_stocked()‎، ونعرّف هنا المغلف بحيث يُقيّم تطبيق unwrap_or_else المغلف لاحقًا إذا احتجنا للنتيجة. يطبع تنفيذ الشيفرة البرمجية السابقة الخرج التالي: $ cargo run Compiling shirt-company v0.1.0 (file:///projects/shirt-company) Finished dev [unoptimized + debuginfo] target(s) in 0.27s Running `target/debug/shirt-company` The user with preference Some(Red) gets Red The user with preference None gets Blue الجانب المثير للاهتمام هنا هو أننا نمرر مغلفًا يستدعي self.most_stocked()‎ على نسخة Inventory الحالية. لا تحتاج المكتبة القياسية لمعرفة أي شيء بخصوص النوعين Inventory أو ShirtColor الذين عرّفناهما، أو المنطق الذي نريد استخدامه في هذه الحالة، إذ أن المغلف يحصل على مرجع ثابث immutable reference إلى self الخاصة بنسخة Inventory، ثم يمرره إلى الشيفرة البرمجية التي كتبناها داخل التابع unwrap_or_else. إذا قارنّا هذه العملية بالتوابع، فالتوابع غير قادرة على الحصول على هذه المعلومات من البيئة بالطريقة ذاتها. استنتاج نوع المغلف وتوصيفه هناك المزيد من الاختلافات بين الدوال والمغلفات، إذ لا تتطلب المغلفات منك عادةً توصيف أنواع المعاملات، أو نوع القيمة المُعادة مثلما تتطلب الدوال fn ذلك، والسبب في ضرورة تحديد الأنواع في الدوال هو لأن الأنواع جزءٌ من واجهة صريحة مكشوفة لمستخدميك وتعريف الواجهة بصورةٍ دقيقة مهمٌ للتأكد من أن الجميع متفقٌ على أنواع القيم التي تستخدمها الدالة وتعيدها، بينما لا تُستخدم المغلّفات في الواجهات المكشوفة بهذه الطريقة، إذ أنها تُخزّن في متغيرات دون تسميتها وكشفها لمستخدمي مكتبتك. تكون المغلفات عادةً قصيرة وترتبط بسياق معين ضيق بدلًا من حالة عامة اعتباطية، ويمكن للمصرّف في هذا السياق المحدود استنتاج أنواع المعاملات والقيمة المعادة بصورةٍ مشابهة لاستنتاجه لمعظم أنواع المتغيرات، وهناك حالات نادرة يحتاج فيها المصرف وجود توصيف للأنواع في المغلفات أيضًا. يمكننا إضافة توصيف النوع إذا أردنا لزيادة دقة ووضوح المغلف كما هو الحال عند تعريف المتغيرات، إلا أن هذه الطريقة تتطلب كتابةً أطول غير ضرورية. يبدو توصيف الأنواع في المغلفات مثل التعريف الموجود في الشيفرة 2، ونعرّف في هذا المثال مغلّفًا في النقطة التي نمرّر فيها وسيطًا كما فعلنا في الشيفرة 1. اسم الملف: src/main.rs let expensive_closure = |num: u32| -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; الشيفرة 2: إضافة توصيف اختياري لأنواع المعاملات والقيمة المُعادة في المغلف تبدو طريقة كتابة المغلفات مشابهة لطريقة كتابة الدوال بعد إضافة توصيف النوع، إذ نعرّف هنا دالةً تجمع 1 إلى المعامل ومغلّفًا بالسلوك ذاته للمقارنة بين الاثنين، ويوضح ذلك كيف أن طريقة كتابة المغلفات مشابهة لطريقة كتابة الدوال باستثناء استخدام الرمز | وكمية الصياغة الاختيارية: fn add_one_v1 (x: u32) -> u32 { x + 1 } let add_one_v2 = |x: u32| -> u32 { x + 1 }; let add_one_v3 = |x| { x + 1 }; let add_one_v4 = |x| x + 1 ; يوضح السطر الأول تعريف دالة، بينما يوضح السطر الثاني تعريف مغلف موصّف بالكامل، ثمّ نزيل في السطر الثالث التوصيف من تعريف المغلف، ونزيل في السطر الرابع الأقواس الاختيارية لأن محتوى المغلف يتألف من تعبير واحد، وجميع التعاريف السابقة تعاريف صالحة الاستخدام تمنحنا السلوك ذاته عند استدعائها. يتطلب السطران add_one_v3 و add_one_v4 تقييم قيمة المغلف حتى يجري تصريفهما، لأن الأنواع يجب أن تُسنتج من خلال استخدامهما وهذا الأمر مشابه لحاجة السطر let v = Vec::new();‎ لتوصيف النوع أو إضافة قيم من نوع ما إلى Vec، بحيث تستطيع رست استنتاج النوع. يستنتج المصرف نوعًا واحدًا ثابتًا لكل من المعاملات في حال تعريف المغلفات، إضافةً إلى النوع المُعاد منها. على سبيل المثال، توضح الشيفرة 3 تعريف مغلف قصير يُعيد القيمة التي يتلقاها مثل معاملٍ له، وهذا المغلف ليس مفيدًا جدًا إلا أن هدفه توضيحي. لاحظ أننا لم نضِف أي توصيف للنوع في التعريف وبالتالي يمكننا استدعاء المغلف باستخدام أي نوع، وهو ما فعلناه في الشيفرة 3، إذ استدعينا المغلّف في المرة الأولى باستخدام النوع String، إلا أننا حصلنا على خطأ عند استدعائنا للمغلف example_closure باستخدام قيمة عدد صحيح integer. اسم الملف: src/main.rs let example_closure = |x| x; let s = example_closure(String::from("hello")); let n = example_closure(5); الشيفرة 3: محاولة استدعاء مغلف يُستنتج نوعه باستخدام نوعين مختلفين يعرض لنا المصرّف الخطأ التالي: $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example) error[E0308]: mismatched types --> src/main.rs:5:29 | 5 | let n = example_closure(5); | --------------- ^- help: try using a conversion method: `.to_string()` | | | | | expected struct `String`, found integer | arguments to this function are incorrect | note: closure parameter defined here --> src/main.rs:2:28 | 2 | let example_closure = |x| x; | ^ For more information about this error, try `rustc --explain E0308`. error: could not compile `closure-example` due to previous error استنبط المصرّف نوع x المُمرّر بكونه سلسلة نصية String عندما رأى أن أول استخدام للمغلف example_closure كان باستخدام قيمة String، كما استنبط القيمة المعادة بالنوع String. تُقيّد هذه الأنواع في المغلف example_closure من تلك النقطة فصاعدًا وسنحصل على خطأ، إذا حاولنا استخدام نوع مختلف بعد ذلك في المغلف ذاته. الحصول على المراجع أو نقل الملكية يمكن أن تحصل المغلفات على القيم من البيئة بثلاث طرق وهي مرتبطة بالطرق الثلاث التي تستطيع فيها الدالة الحصول على القيم على أنها معاملاتها: الاستعارة الثابتة أو الاستعارة المتغيّرة أو أخذ الملكية، ويحدد المغلف واحدًا من هذه الطرق الثلاث حسب القيم الموجودة في متن الدالة. نعرّف في الشيفرة 4 مغلفًا يحصل على مرجع ثابت لشعاع يدعى list لأنه يحتاج فقط للمراجع الثابتة لطباعة القيمة: اسم الملف: src/main.rs fn main() { let list = vec![1, 2, 3]; println!("Before defining closure: {:?}", list); let only_borrows = || println!("From closure: {:?}", list); println!("Before calling closure: {:?}", list); only_borrows(); println!("After calling closure: {:?}", list); } الشيفرة 4: تعريف مغلف واستدعاءه، بحيث يحصل هذا المغلف على مرجع ثابت يوضح هذا المثال أيضًا أنه من الممكن للمتغير أن يرتبط بتعريف مغلف، ويمكننا لاحقًا استدعاء المغلف باستخدام اسم المتغير والقوسين وكأن اسم المتغير يمثل اسم دالة. يمكن الوصول للشعاع list من الشيفرة البرمجية قبل تعريف المغلف وبعد تعريفه، شرط أن يكون قبل استدعاء المغلف وبعد استدعاء المغلف، وذلك لأنه من الممكن وجود عدّة مراجع ثابتة للشعاع list في ذات الوقت، وتُصرَّف الشيفرة البرمجية بنجاح وتُنفَّذ وتطبع التالي: $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example) Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/closure-example` Before defining closure: [1, 2, 3] Before calling closure: [1, 2, 3] From closure: [1, 2, 3] After calling closure: [1, 2, 3] نعدّل في الشيفرة 5 متن المغلف بحيث نضيف عنصرًا إلى الشعاع list، وبالتالي يحصل المغلف الآن على مرجع متغيّر: اسم الملف: src/main.rs fn main() { let mut list = vec![1, 2, 3]; println!("Before defining closure: {:?}", list); let mut borrows_mutably = || list.push(7); borrows_mutably(); println!("After calling closure: {:?}", list); } الشيفرة 5: تعريف واستدعاء مغلف يحتوي على مرجع متغيّر تُصرَّف الشيفرة البرمجية بنجاح وتُنفَّذ ونحصل على الخرج التالي: $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example) Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/closure-example` Before defining closure: [1, 2, 3] After calling closure: [1, 2, 3, 7] لاحظ أننا لا نستخدم println!‎ بين تعريف المغلف borrows_mutably واستدعائه، إذ يحصل المغلف على مرجع متغيّر للشعاع list عند تعريفه، ولا نستخدم المغلف مجددًا بعد استدعائه لذا تنتهي الاستعارة المتغيرة عندها. لا يُسمح بطباعة مرجع متغيّر بين تعريف المغلف واستدعائه، إذ لا يُسمح بوجود أي عمليات استعارة أخرى عند وجود استعارة متغيّرة. حاول إضافة استدعاء للماكرو println!‎ لترى رسالة الخطأ التي تظهر لك. إذا أردت إجبار المغلف على أخذ ملكية القيم التي يستخدمها في البيئة على الرغم من أن متن المغلف لا يتطلب أخذ الملكية، فيمكنك استخدام الكلمة المفتاحية move قبل قائمة المعاملات. هذه الطريقة مفيدة خصوصًا عند تمرير مغلف لخيط thread جديد لنقل البيانات، بحيث تُمتلك بواسطة الخيط الجديد. سنناقش الخيوط بالتفصيل وسبب استخدامنا لها لاحقًا، لكن دعنا للوقت الحالي نستكشف عملية إضافة خيط جديد باستخدام مغلّف يحتاج الكلمة المفتاحية move. توضح الشيفرة 6 إصدارًا عن الشيفرة 4، إلا أننا نطبع الشعاع هنا في خيط جديد بدلًا من الخيط الرئيسي: اسم الملف: src/main.rs use std::thread; fn main() { let list = vec![1, 2, 3]; println!("Before defining closure: {:?}", list); thread::spawn(move || println!("From thread: {:?}", list)) .join() .unwrap(); } الشيفرة 6: استخدام move لإجبار خيط المغلف على أخذ ملكية list نُنشئ خيطًا جديدًا وذلك بمنح الخيط مغلف ليعمل به مثل وسيط، ويطبع متن المغلف القائمة. يحصل المغلف في الشيفرة 4 على list باستخدام مرجع ثابت لأنها تُعد أدنى درجات الوصول المطلوبة لطباعة محتويات list، إلا أننا في هذا المثال نحدد أن list يجب أن تُنقل إلى المغلف بإضافة الكلمة المفتاحية move في بداية تعريف المغلّف على الرغم من أن متن المغلف يتطلب فقط مرجعًا ثابتًا. يمكن للخيط الجديد أن ينتهي من التنفيذ قبل انتهاء الخيط الرئيسي من التنفيذ أو أن ينتهي الخيط الرئيسي أولًا، وإذا احتفظ الخيط الرئيسي بملكية list وانتهى من التنفيذ قبل الخيط الجديد وأسقط list فهذا يعني أن المرجع الثابت في الخيط الجديد سيكون غير صالحًا، ولذلك يتطلب المصرف نقل list إلى المغلف في الخيط الجديد بحيث يبقى المرجع صالحًا داخله. جرّب إزالة الكلمة المفتاحية move أو استخدام list في الخيط الرئيسي بعد تعريف المغلف لرؤية خطأ المصرف الذي يظهر لك. نقل القيم خارج المغلفات وسمات Fn تعرّف الشيفرة البرمجية الموجودة داخل مغلف ما الأمر الذي سيحصل للمراجع أو القيم بعد أن يُقيّم المغلف (بالتالي التعريف على الشيء الذي نُقل خارج المغلف إذا كان موجودًا) وذلك بعد أن يحصل المغلف على المرجع أو ملكية قيمة ما من البيئة مكان تعريفه (بالتالي التعريف على الشيء الذي نُقل إلى المغلف إذا كان موجودًا). يمكن لمتن المغلف أن يفعل أيًا من الأشياء التالية: نقل قيمة خارج المغلّف، أو التعديل على قيمة داخل المغلف، أو عدم نقل القيمة وعدم تعديلها، أو عدم الحصول على أي قيمة من البيئة في المقام الأول. تؤثر الطريقة التي يحصل بها المغلف على القيم ويتعامل معها من البيئة على السمات التي يطبقها المغلف والسمات traits هي الطريقة التي تستطيع فيها كل من الدوال والهياكل تحديد نوع المغلفات الممكن استخدامها. تطبّق المغلفات تلقائيًا سمةً أو سمتين أو ثلاث من سمات Fn التالية على نحوٍ تراكمي بحسب تعامل متن المغلف للقيم: السمة FnOnce: تُطبَّق على جميع المغلفات الممكن استدعاؤها مرةً واحدة. تُطبّق جميع المغلفات هذه السمة على الأقل لأنه يمكن لجميع المغلفات أن تُستدعى، وسيطبق المغلف الذي ينقل القيم خارج متنه السمة FnOnce فقط دون أي سمات Fn أخرى لأنه يُمكن استدعاءه مرةً واحدةً فقط. السمة FnMut: تُطبَّق على جميع المغلفات التي لا تنقل القيم خارج متنها، إلا أنها من الممكن أن تعدّل على هذه القيم، ويمكن استدعاء هذه المغلفات أكثر من مرة واحدة. السمة Fn: تُطبَّق على المغلفات التي لا تنقل القيم خارج متنها ولا تعدل على القيم، إضافةً إلى المغلفات التي لا تحصل على أي قيم من البيئة. يمكن لهذه المغلفات أن تُستدعى أكثر من مرة واحدة دون التعديل على بيئتها وهو أمرٌ مهم في حالة استدعاء مغلف عدة مرات على نحوٍ متعاقب. دعنا ننظر إلى تعريف التابع unwrap_or_else على النوع Option<T>‎ الذي استخدمناه في الشيفرة 1: impl<T> Option<T> { pub fn unwrap_or_else<F>(self, f: F) -> T where F: FnOnce() -> T { match self { Some(x) => x, None => f(), } } } تذكّر أن T هو نوع معمم generic type يمثل نوع القيمة في المتغاير Some للنوع Option، وهذا النوع T يمثل أيضًا نوع القيمة المعادة من الدالة unwrap_or_else، إذ ستحصل الشيفرة البرمجية التي تستدعي unwrap_or_else على النوع Option<String>‎ على النوع String على سبيل المثال. لاحظ تاليًا أن الدالة unwrap_or_else تحتوي على معامل نوع معمم إضافي يُدعى F والنوع F هنا هو نوع المعامل ذو الاسم f وهو المغلف الذي نمرره للدالة unwrap_or_else عند استدعائها. حد السمة trait bound المحدد على النوع المعمم E هو FnOnce() -> T، مما يعني أن النوع F يجب أن يكون قابلًا للاستدعاء مرةً واحدة وألّا يأخذ أي وسطاء وأن يعيد قيمةً من النوع T. يوضح استخدام FnOnce في حد السمة القيد: أن unwrap_or_else ستستدعي f مرةً واحدةً على الأكثر. يمكنك من رؤية متن الدالة unwrap_or_else معرفة أن f لن تُستدعى إذا كان المتغاير Some موجودًا في Option، بينما ستُستدعى f مرةً واحدة إذا وُجد المتغاير None في Option. تقبل الدالة unwrap_or_else أنواعًا مختلفة من المغلفات بصورةٍ مرنة، لأن جميع المغلفات تطبّق السمة FnOnce. ملاحظة: يمكن أن تطبق الدوال سمات Fn الثلاث أيضًا. يمكننا استخدام اسم الدالة بدلًا من مغلف عندما نريد شيئًا يطبق واحدةً من سمات Fn إذا كان ما نريد فعله لا يتطلب الحصول على قيمة من البيئة. يمكننا على سبيل المثال، استدعاء unwrap_or_else(Vec::new)‎ على قيمة Option<Vec<T>>‎ للحصول على شعاع فارغ جديد إذا كانت القيمة هي None. دعنا ننظر الآن إلى تابع المكتبة القياسية sort_by_key المعرف على الشرائح slices لرؤية الاختلاف بينه وبين unwrap_or_else وسبب استخدام sort_by_key للسمة FnMut بدلًا من السمة FnOnce لحد السمة. يحصل المغلف على وسيط واحد على هيئة مرجع للعنصر الحالي في الشريحة ويُعيد قيمةً من النوع K يمكن ترتيبها، وهذه الدالة مفيدة عندما تريد ترتيب شرحة بسمة attribute معينة لكل عنصر. لدينا في الشيفرة 7 قائمة من نسخ instances من الهيكل Rectangle ونستخدم sort_by_key لترتيبها حسب سمة width من الأصغر إلى الأكبر: اسم الملف: src/main.rs #[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; list.sort_by_key(|r| r.width); println!("{:#?}", list); } الشيفرة 7: استخدام sort_by_key لترتيب المستطيلات بحسب عرضها نحصل على الخرج التالي مما سبق: $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) Finished dev [unoptimized + debuginfo] target(s) in 0.41s Running `target/debug/rectangles` [ Rectangle { width: 3, height: 5, }, Rectangle { width: 7, height: 12, }, Rectangle { width: 10, height: 1, }, ] السبب في كون sort_by_key معرفًا ليأخذ مغلفًا يطبق السمة FnMut هو استدعاء الدالة للمغلف عدة مرات: مرةً واحدةً لكل عنصر في الشريحة. لا يحصل المغلف ‎|r| r.width على أي قيمة من البيئة أو يعدل عليها أو ينقلها لذا فهو يحقق شروط حد السمة هذه. توضح الشيفرة 8 مثالًا لمغلف على النقيض، إذ يطبق هذا المغلف السمة FnOnce فقط لأنه ينقل قيمة خارج البيئة، ولن يسمح لنا المصرّف باستخدام هذا المغلف مع sort_by_key: اسم الملف: src/main.rs #[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; let mut sort_operations = vec![]; let value = String::from("by key called"); list.sort_by_key(|r| { sort_operations.push(value); r.width }); println!("{:#?}", list); } الشيفرة 8: محاولة استخدام مغلف يطبق السمة FnOnce فقط مع التابع sort_by_key يمثّل ما سبق طريقةً معقدة (لا تعمل بنجاح) لمحاولة عدّ المرات التي يُستدعى بها التابع sort_by_key عند ترتيب list، وتحاول الشيفرة البرمجية تحقيق ذلك بإضافة value -ألا وهي قيمة من النوع String من بيئة المغلف- إلى الشعاع sort_operations. يحصل المغلف على القيمة value، ثم ينقلها خارج المغلف بنقل ملكيتها إلى الشعاع sort_operations، ويمكن أن يُستدعى هذا المغلف مرةً واحدةً إلا أن محاولة استدعائه للمرة الثانية لن تعمل لأن value لن يكون في البيئة ليُضاف إلى الشعاع sort_operations مجددًا، وبالتالي يطبق هذا المغلف السمة FnOnce فقط، وعندما نحاول تصريف الشيفرة البرمجية السابقة، سنحصل على خطأ مفاده أن value لا يمكن نقلها خارج المغلف لأن المغلف يجب أن يطبّق السمة FnMut: $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure --> src/main.rs:18:30 | 15 | let value = String::from("by key called"); | ----- captured outer variable 16 | 17 | list.sort_by_key(|r| { | --- captured by this `FnMut` closure 18 | sort_operations.push(value); | ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait For more information about this error, try `rustc --explain E0507`. error: could not compile `rectangles` due to previous error يشير الخطأ إلى السطر الذي ننقل فيه القيمة value خارج البيئة داخل متن المغلف، ولتصحيح هذا الخطأ علينا تعديل متن المغلف بحيث لا ينقل القيم خارج البيئة. نحافظ على وجود عدّاد في البيئة ونزيد قيمته داخل المغلف بحيث نستطيع عدّ المرات التي يُستدعى فيها التابع sort_by_key. يعمل المغلف في الشيفرة 9 مع sort_by_key لأنه يحصل فقط على المرجع المتغيّر الخاص بالعداد num_sort_operations ويمكن بالتالي استدعاؤه أكثر من مرة واحدة: اسم الملف: src/main.rs #[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; let mut num_sort_operations = 0; list.sort_by_key(|r| { num_sort_operations += 1; r.width }); println!("{:#?}, sorted in {num_sort_operations} operations", list); } الشيفرة 9: استخدام مغلف يطبق السمة FnMut مع التابع sort_by_key دون الحصول على أخطاء سمات Fn مهمة عند تعريف أو استخدام الدوال أو الأنواع التي تستخدم المغلفات. سنناقش في المقال التالي المكررات iterators، إذ أن العديد من توابع المكررات تأخذ المغلفات مثل وسطاء، لذا تذكّر التفاصيل المتعلقة بالمغلّفات عند قراءة المقالة التالية. ترجمة -وبتصرف- لقسم من الفصل Functional Language Features: Iterators and Closures من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: معالجة سلسلة من العناصر باستخدام المكررات iterators في لغة رست المقال السابق: التعامل مع متغيرات البيئة وطباعة الأخطاء في لغة رست تنظيم الاختبارات Tests في لغة رست Rust السمات Traits في لغة رست Rust تعلم لغة رست Rust: البدايات
  15. بدأنا عملية برمجة أداة سطر الأوامر المشابهة لأداة grep التي تبحث داخل ملف معيّن عن سلسلة نصية محددة في مقال التعامل مع الدخل والخرج أين وضع أساس برنامج سطر الأوامر بلغة رست حيث برمجنا منطق التعامل مع الوسطاء المرّرة في سطر الأوامر، ثم حسناه وطورناه أكثر في مقال إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء حيث عملنا على تجزئة برنامجنا إلى وحدات منفصلة لتسهل عملية اختبار البرنامج، ثم اختبرنا البرنامج عبر كتابة اختبارات له في المقال السابق ، وسنوضّح أخيرًا في هذا المقال كيفية العمل مع متغيرات البيئة environment variables، إضافةً لكيفية طباعة الأخطاء إلى مجرى الأخطاء القياسي، وهما أمران مهمّان في برامج سطر الأوامر. التعامل مع متغيرات البيئة سنحسّن على برنامج "minigrep" باستخدام ميزة إضافية، ألا وهي خيار استخدام البحث بنمط عدم حساسية حالة الحروف case-insensitive (سواءٌ كانت أحرف صغيرة أو كبيرة) بحيث يمكن للمستخدم تفعيل هذا النمط أو تعطيله باستخدام متغيرات البيئة. يمكننا جعل هذه الميزة خيارًا لسطر الأوامر إلا أن المستخدم سيكون بحاجةٍ لكتابة هذا الخيار في سطر الأوامر في كل مرة يريد استخدام البرنامج، وباستخدام متغيرات البيئة نجعل ضبط هذا الخيار لمرة واحدة بحيث تكون كل عمليات البحث غير حساسة لحالة الأحرف في جلسة الطرفية تلك. كتابة اختبار يفشل لدالة search لميزة عدم حساسية حالة الأحرف نُضيف أولًا دالة search_case_insensitive التي تُستدعى عندما يكون لمتغير البيئة قيمةً ما، وسنستمر باتباع عملية التطوير المُقاد بالاختبار هنا، وبالتالي ستكون الخطوة الأولى هي كتابة اختبار يفشل؛ إذ سنضيف اختبارًا جديدًا للدالة الجديدة search_case_insensitive وسنعيد تسمية الاختبار القديم من اسمه السابق one_result إلى case_sensitive لتوضيح الفرق بين الاختبارين كما هو موضح في الشيفرة 20. اسم الملف: src/lib.rs #[cfg(test)] mod tests { use super::*; #[test] fn case_sensitive() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three. Duct tape."; assert_eq!(vec!["safe, fast, productive."], search(query, contents)); } #[test] fn case_insensitive() { let query = "rUsT"; let contents = "\ Rust: safe, fast, productive. Pick three. Trust me."; assert_eq!( vec!["Rust:", "Trust me."], search_case_insensitive(query, contents) ); } } الشيفرة 20: إضافة اختبار جديد يفشل لدالة عدم حساسية حالة الحرف التي سنضيفها لاحقًا لاحظ أننا أضفنا اختبار contents القديم أيضًا، وأضفنا سطرًا جديدًا للنص ".Duct tape" باستخدام حرف D كبير، والذي يجب ألا يطابق استعلام السلسلة النصية "duct" عندما نبحث في حالة حساسية حالة الأحرف. يساعد تغيير الاختبار القديم بهذه الطريقة في التأكد من أننا لن نعطّل خاصية البحث في حالة حساسية الأحرف (وهي الحالة التي طبقناها أولًا، والتي تعمل بنجاح للوقت الحالي). يجب أن ينجح هذا الاختبار الآن ويجب أن يستمر بالنجاح بينما نعمل على خاصية عدم حساسية حالة الأحرف. يستخدم الاختبار الجديد للبحث بخاصية عدم حساسية حالة الأحرف "rUsT" مثل كلمة بحث، لذلك سنُضيف في الدالة search_case_insensitive الكلمة "rUsT" والتي يجب أن تطابق ":Rust" بحرف R كبير وأن تطابق السطر ".Trust me" أيضًا رغم أن للنتيجتين حالة أحرف مختلفة عن الكلمة التي استخدمناها. هذا هو اختبارنا الذي سيفشل، وستفشل عملية تصريفه لأننا لم نعرّف بعد الدالة search_case_insensitive. ضِف هيكلًا للدالة بحيث تُعيد شعاعًا فارغًا بطريقة مشابهة لما فعلناه في دالة search في الشيفرة 16 حتى نستطيع تصريف الاختبار ورؤية أنه يفشل فعلًا. تنفيذ دالة search_case_insensitive ستكون الدالة search_case_insensitive الموضحة في الشيفرة 21 مماثلة تقريبًا للدالة search، والفارق الوحيد هنا هو أننا سنحوّل حال الأحرف للكلمة التي نبحث عنها إلى أحرف صغيرة (الوسيط query) إضافةً إلى كل سطر line، بحيث تكون حالة الأحرف متماثلة عند المقارنة بينهما بغضّ النظر عن حالة الأحرف الأصلية. اسم الملف: src/lib.rs pub fn search_case_insensitive<'a>( query: &str, contents: &'a str, ) -> Vec<&'a str> { let query = query.to_lowercase(); let mut results = Vec::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line); } } results } الشيفرة 21: تعريف الدالة search_case_insensitive بحيث تحوّل أحرف الكلمة التي نبحث عنها مع السطر إلى أحرف صغيرة قبل مقارنتهما نحوّل أولًا أحرف السلسلة النصية query إلى أحرف صغيرة ونخزّنها في متغير يحمل الاسم ذاته، ولتحقيق ذلك نستدعي to_lowercase على السلسلة النصية بحيث تكون النتيجة واحدة بغضّ النظر عن حالة الأحرف المدخلة "rust" أو "RUST" أو "Rust" أو "rUsT" وسنعامل السلسلة النصية المدخلة وكأنها "rust" لإهمال حالة الأحرف، سيعمل التابع to_lowercase على محارف يونيكود Unicode الأساسية إلا أن عمله لن يكون صحيحًا مئة بالمئة. إن كنّا نبرمج تطبيقًا واقعيًا فعلينا أن نقوم بالمزيد من العمل بخصوص هذه النقطة، إلا أننا نناقش في هذا القسم متغيرات البيئة وليس يونيكود، لذا لن نتطرق لذلك الآن. لاحظ أن query من النوع String الآن وليس شريحة سلسلة نصية لأن استدعاء التابع to_lowercase يُنشئ بيانات جديدة عوضًا عن استخدام مرجع للبيانات الموجودة مسبقًا. لنفرض بأن الكلمة هي "rUsT" كمثال: لا تحتوي شريحة السلسلة النصية على حرف u أو t صغير لنستخدمه لذا علينا حجز مساحة جديدة لنوع String يحتوي على "rust"، وعندما نمرّر query كوسيط إلى التابع contains فنحن بحاجة لإضافة الرمز & لأن شارة contains معرفة بحيث تأخذ شريحة سلسلة نصية. نضيف من ثمّ استدعاءً للتابع to_lowercase لكل line لتحويل أحرفه إلى أحرف صغيرة، وبذلك نكون حولنا كل من أحرف line وquery إلى أحرف صغيرة وسنجد حالات التطابق بغض النظر عن حالة الأحرف في السلسلتين الأصليتين. دعنا نرى إذا كان تطبيقنا سيجتاز الاختبار: $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 1.33s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 2 tests test tests::case_insensitive ... ok test tests::case_sensitive ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s عظيم، اجتزنا الاختبار. دعنا نستدعي الآن الدالة search_case_insensitive من الدالة run، وسنُضيف أولًا خيار الضبط إلى الهيكل Config للتبديل بين البحث الحساس وغير الحساس لحالة الأحرف، إلا أن إضافة هذا الحقل ستتسبب بأخطاء عند التصريف لأننا لم نسند هذا الحقل إلى أي مكان بعد: اسم الملف: src/lib.rs pub struct Config { pub query: String, pub file_path: String, pub ignore_case: bool, } أضفنا الحقل igonre_case الذي يخزن متحول بولياني Boolean، وسنحتاج الدالة run للتتحقق من قيمة ignore_case لتحديد استدعاء أيّ من دالتي البحث: search أو search_case_insensitive كما هو موضح في الشيفرة 22. لن تُصرَّف هذه الشيفرة بنجاح بعد. اسم الملف: src/lib.rs pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.file_path)?; let results = if config.ignore_case { search_case_insensitive(&config.query, &contents) } else { search(&config.query, &contents) }; for line in results { println!("{line}"); } Ok(()) } الشيفرة 22: استدعاء الدالة search أو الدالة search_case_insensitive بحسب القيمة الموجودة في config.ignore_case أخيرًا، نحن بحاجة إلى فحص متغير البيئة. الدوال الخاصة بالتعامل مع متغيرات البيئة موجودة في الوحدة module env في المكتبة القياسية، لذا نضيف الوحدة إلى النطاق أعلى الملف src/lib.rs. نستخدم الدالة var من الوحدة env لفحص القيمة المضبوطة في متغير البيئة ذو الاسم IGNORE_CASE كما هو موضح في الشيفرة 23. اسم الملف: src/lib.rs use std::env; // --snip-- impl Config { pub fn build(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let file_path = args[2].clone(); let ignore_case = env::var("IGNORE_CASE").is_ok(); Ok(Config { query, file_path, ignore_case, }) } } الشيفرة 23: التحقق من القيمة المضبوطة في متغير البيئة ذو الاسم IGNORE_CASE نُنشئ هنا متغيرًا جديدًا يدعى ignore_case ونُسند قيمته باستدعاء الدالة env::var ونمرّر اسم متغير البيئة IGNORE_CASE إليها. تُعيد الدالة env::var قيمةً من النوع Result تحتوي على متغاير variant‏ يدعى Ok يحتوي على قيمة متغير البيئة إذا كان متغير البيئة مضبوطًا إلى قيمة معينة وإلا فهي تعيد قيمة المتغاير Err. نستخدم التابع is_ok على القيمة Result للتحقق فيما إذا كان متغير البيئة مضبوطًا إلى قيمة معينة أم لا؛ فإذا كان مضبوطًا إلي قيمة فهذا يعني أنه علينا استخدام البحث بتجاهل حالة الأحرف؛ وإذا لم يكن مضبوطًا إلى قيمة معينة، فهذا يعني أن is_ok ستُعيد القيمة false وسينفذ البرنامج الدالة التي تجري البحث الحساس لحالة الأحرف. لا نهتم بقيمة متغير البيئة بل نهتم فقط فيما إذا كانت موجودة أو لا، ولذلك فنحن نستخدم التابع is_ok بدلًا من استخدام unwrap أو expect أو أيًا من التوابع الأخرى التي استخدمناها مع Result سابقًا. نمرر القيمة في المتغير ignore_case إلى نسخة Config بحيث يمكن للدالة run أن تقرأ هذه القيمة وتقرّر استدعاء الدالة search_case_insensitive أو search كما طبقنا سابقًا في الشيفرة 22. دعنا نجرّب البرنامج، ولننفذ أولًا البرنامج دون ضبط متغير البيئة وباستخدام الكلمة to، التي يجب أن تمنحنا جميع نتائج المطابقة للكلمة "to" بأحرف صغيرة فقط: $ cargo run -- to poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep to poem.txt` Are you nobody, too? How dreary to be somebody! يبدو أن البرنامج يعمل بنجاح. دعنا نجرّب الآن تنفيذ البرنامج مع ضبط IGNORE_CASE إلى 1 باستخدام الكلمة ذاتها to. $ IGNORE_CASE=1 cargo run -- to poem.txt إذا كنت تستخدم PowerShell، فعليك ضبط متغير البيئة، ثم تنفيذ البرنامج على أنهما أمرين منفصلين: PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt سيجعل ذلك متغير البيئة IGNORE_CASE مستمرًا طوال جلسة الصدفة shell. ويمكن إزالة القيمة عن طريق الأمر Remove-Item: PS> Remove-Item Env:IGNORE_CASE يجب أن نحصل على الأسطر التي تحتوي على الكلمة "to" بغض النظر عن حالة الأحرف: Are you nobody, too? How dreary to be somebody! To tell your name the livelong day To an admiring bog! عظيم، حصلنا على الكلمة "To" ضمن كلمات أخرى. يمكن لبرنامج minigrep الآن البحث عن الكلمات بغض النظر عن حالة الأحرف عن طريق متغير بيئة، ويمكنك الآن التحكم بخيارات البرنامج عن طريق وسطاء سطر الأوامر، أو عن طريق متغيرات البيئة. تسمح بعض البرامج بوسطاء سطر الأوامر ومتغيرات البيئة في ذات الوقت للخيار نفسه، وفي هذه الحالات يقرّر البرنامج أسبقية أحد الخيارين (وسيط سطر الأوامر أو متغير البيئة). تمرّن بنفسك عن طريق التحكم بحساسية حالة الأحرف عن طريق وسيط سطر أوامر أو متغير بيئة في الوقت ذاته، وحدّد أسبقية أحد الخيارين حسب تفضيلك إذا تعارض الخياران مع بعضهما. تحتوي الوحدة std::env على العديد من الخصائص الأخرى المفيدة للتعامل مع متغيرات البيئة، اقرأ توثيق الوحدة للاطّلاع على الخيارات المتاحة. كتابة رسائل الخطأ إلى مجرى الخطأ القياسي بدلا من مجرى الخرج القياسي نكتب رسائل الأخطاء حاليًا إلى الطرفية باستخدام الماكرو println!‎، وفي معظم الطرفيات هناك نوعين من الخرج: خرج قياسي ‏ ‎stdout‎ للمعلومات العامة وخطأ قياسي ‏stderr‎ لرسائل الخطأ، ويساعد التمييز بين النوعين المستخدمين بتوجيه خرج نجاح البرنامج إلى ملف مع المحافظة على ظهور رسائل الخطأ على شاشة الطرفية. الماكرو println!‎ قادرٌ فقط على الطباعة إلى الخرج القياسي، لذا علينا استخدام شيء مختلف لطباعة الأخطاء إلى مجرى الأخطاء القياسي standard error stream. التحقق من مكان كتابة الأخطاء دعنا أولًا نلاحظ كيفية طباعة المحتوى في برنامج minigrep حاليًا إلى الخرج القياسي، متضمنًا ذلك رسائل الأخطاء التي نريد كتابتها إلى مجرى الأخطاء القياسي بدلًا من ذلك، وسنحقّق ذلك بإعادة توجيه مجرى الخرج القياسي إلى ملف والتسبب بخطأ عمدًا، بينما سنُبقي على مجرى الأخطاء القياسي ولن نعيد توجيهه، وبالتالي سيُعرض محتوى مجرى الأخطاء القياسي على الشاشة مباشرةً. من المتوقع لبرامج سطر الأوامر أن ترسل رسائل الخطأ إلى مجرى الأخطاء القياسي بحيث يمكننا رؤية الأخطاء على الشاشة حتى لو كنّا نعيد توجيه مجرى الخرج القياسي إلى ملف ما. لا يسلك برنامجنا حاليًا سلوكًا جيدًا، إذ أننا على وشك رؤية أن رسائل الخطأ تُخزّن في الملف. لتوضيح هذا السلوك سننفذ البرنامج باستخدام < ومسار الملف output.txt وهو الملف الذي نريد إعادة توجيه مجرى الخرج القياسية إليه. لن نمرّر أي وسطاء عند التنفيذ، وهو ما سيتسبب بخطأ: $ cargo run > output.txt يخبر الرمز < الصدفة بكتابة محتويات الخرج القياسي إلى الملف output.txt بدلًا من الشاشة. لم نحصل على أي رسالة خطأ على الرغم من توقعنا لها بالظهور على الشاشة مما يعني أن الرسالة قد كُتبت إلى الملف. إليك محتوى الملف output.txt: Problem parsing arguments: not enough arguments نعم، تُطبع رسالة الخطأ إلى الخرج القياسي كما توقعنا ومن الأفضل لنا طباعة رسائل الخطأ إلى مجرى الأخطاء القياسي بدلًا من ذلك بحيث يحتوي الملف على البيانات الناتجة عن التنفيذ الناجح، دعنا نحقّق ذلك. طباعة الأخطاء إلى مجرى الأخطاء القياسي سنستخدم الشيفرة البرمجية في الشيفرة 24 لتعديل طريقة طباعة الأخطاء. لحسن الحظ، الشيفرة البرمجية المتعلقة بطباعة رسائل الخطأ موجودة في دالة واحدة ألا وهي main بفضل عملية إعادة بناء التعليمات البرمجية التي أنجزناها سابقًا. تقدّم لنا المكتبة القياسية الماكرو eprintln!‎ الذي يطبع إلى مجرى الأخطاء القياسي، لذا دعنا نعدّل من السطرين الذين نستخدم فيهما الماكرو println!‎ ونستخدم eprintln!‎ بدلًا من ذلك. اسم الملف: src/main.rs fn main() { let args: Vec<String> = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {err}"); process::exit(1); }); if let Err(e) = minigrep::run(config) { eprintln!("Application error: {e}"); process::exit(1); } } الشيفرة 24: كتابة رسائل الخطأ إلى مجرى الأخطاء القياسية بدلًا من مجرى الخرج القياسي باستخدام eprintln!‎ دعنا ننفذ البرنامج مجددًا بالطريقة ذاتها دون وسطاء وبإعادة توجيه الخرج القياسي إلى ملف باستخدام <: $ cargo run > output.txt Problem parsing arguments: not enough arguments نستطيع رؤية الخطأ على الشاشة الآن، ولا يحتوي الملف output.txt أي بيانات وهو السلوك الذي نتوقعه من برامج سطر الأوامر. دعنا ننفذ البرنامج مجددًا باستخدام الوسطاء لتنفيذ البرنامج دون أخطاء، وبتوجيه الخرج القياسي إلى ملف أيضًا كما يلي: $ cargo run -- to poem.txt > output.txt لن نستطيع رؤية أي خرج على الطرفية، وسيحتوي الملف output.txt على نتائجنا: اسم الملف: output.txt Are you nobody, too? How dreary to be somebody! يوضح هذا الأمر أننا نستخدم مجرى الخرج القياسي للخرج في حالة النجاح، بينما نستخدم مجرى الأخطاء القياسي في حالة الفشل. خاتمة المشروع لخّصت جزئية سطر الأوامر من هذه السلسلة بمشروعها الكثير من المفاهيم المهمة التي تعلمناها لحد اللحظة كما أننا تكلمنا عن كيفية إجراء عمليات الدخل والخرج في رست، وذلك باستخدام وسطاء سطر الأوامر والملفات ومتغيرات البيئة والماكرو eprintln!‎ لطباعة الأخطاء، ويجب أن تكون الآن مستعدًا لكتابة تطبيقات سطر الأوامر المختلفة. يجب أن تبقى شيفرتك البرمجية منظمةً جيدًا بمساعدة المفاهيم التي تعلمتها في المقالات السابقة وأن تخزن البيانات بفعالية في هياكل بيانات مناسبة وأن تتعامل مع الأخطاء بصورةٍ مناسبة، إضافةً إلى إجراء الاختبارات. سننظر في المقالات التالية إلى بعض مزايا رست التي تأثرت باللغات الوظيفية، ألا وهي المغلّفات closures والمكررات iterators. ترجمة -وبتصرف- لقسم من الفصل An I/O Project: Building a Command Line Program من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: المغلفات closures في لغة رست Rust المقال السابق: كتابة برنامج سطر أوامر بلغة رست: اختبار البرنامج ما هو سطر الأوامر ؟ كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج
  16. بدأنا عملية برمجة أداة سطر الأوامر المشابهة لأداة grep التي تبحث داخل ملف معيّن عن سلسلة نصية محددة في مقال كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج وضع أساس برنامج سطر الأوامر بلغة رست حيث برمجنا منطق التعامل مع الوسطاء المرّرة في سطر الأوامر، ثم حسناه وطورناه أكثر في المقال التالي كتابة برنامج سطر أوامر بلغة رست: إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء حيث عملنا على تجزئة برنامجنا إلى وحدات منفصلة لتسهل عملية اختبار البرنامج، ونتطرّق في هذا المقال إلى اختبار البرنامج. تطوير عمل المكتبة باستخدام التطوير المقاد بالاختبار test-driven الآن، وبعد استخراجنا لمعظم منطق البرنامج إلى الملف src/lib.rs، يبقى لدينا منطق الحصول على الوسطاء والتعامل مع الأخطاء في src/main.rs، ومن الأسهل كتابة الاختبارات في هذه الحالة، إذ ستركّز الاختبارات على منطق شيفرتنا البرمجية الأساسية. يمكننا استدعاء الدوال مباشرةً باستخدام مختلف الوسطاء arguments والتحقق من القيمة المعادة دون الحاجة لاستدعاء ملفنا التنفيذي من سطر الأوامر. نُضيف في هذا القسم منطق البحث إلى البرنامج "minigrep" باستخدام التطوير المُقاد بالاختبار test-driven development -أو اختصارًا TDD- باتباع الخطوات التالية: كتابة اختبار يفشل وتنفيذه للتأكد من أنه يفشل فعلًا للسبب الذي تتوقعه. كتابة شيفرة برمجية أو التعديل على شيفرة برمجية موجودة مسبقًا لجعل الاختبار الجديد ينجح. إعادة بناء التعليمات البرمجية المُضافة أو المُعدّلة للتأكد من أن الاختبارات ستنجح دومًا. كرّر الأمر مجدّدًا بدءًا من الخطوة 1. على الرغم من أن التطوير المُقاد بالاختبار هو طريقة من الطرق العديدة الموجودة لكتابة البرمجيات، إلا أنه من الممكن أن يساهم بتصميم الشيفرة البرمجية، إذ تساعد كتابة الاختبارات قبل كتابة الشيفرة البرمجية التي تجعل من الاختبار ناجحًا في المحافظة على اختبار جميع أجزاء الشيفرة البرمجية بسوّية عالية خلال عملية التطوير. سنختبر تطبيق الخاصية التي ستبحث عن السلسلة النصية المُدخلة في محتويات الملف وتعطينا قائمةً من الأسطر تحتوي على حالات التطابق، ثمّ سنضيف هذه الخاصية في دالة تدعى search. كتابة اختبار فاشل دعنا نتخلص من تعليمات println!‎ من الملفين "src/lib.rs" و "src/main.rs" التي كنا نستخدمها لتفقُّد سلوك البرنامج ولن نحتاج إليها بعد الآن، ثم نضيف الوحدة tests في الملف "src/lib.rs" مع دالة اختبار كما فعلنا في مقال سابق. تحدد دالة الاختبار السلوك الذي نريده من الدالة search ألا وهو: ستأخذ الدالة سلسلةً نصيةً محددةً ونصًا تبحث فيه وستُعيد السطور التي تحتوي على تطابق. توضح الشيفرة 15 هذا الاختبار، إلا أنها لن تُصرَّف بنجاح بعد. اسم الملف: src/lib.rs #[cfg(test)] mod tests { use super::*; #[test] fn one_result() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three."; assert_eq!(vec!["safe, fast, productive."], search(query, contents)); } } الشيفرة 15: إنشاء اختبار فاشل للدالة search التي كنا نتمنى الحصول عليها يبحث هذا الاختبار عن السلسلة النصية "duct"، إذ يتألف النص الذي نبحث فيه من ثلاثة أسطر ويحتوي واحدٌ منها فقط السلسلة النصية "duct" (يخبر الخط المائل العكسي backslash بعد علامتي التنصيص المزدوجتين رست بعدم إضافة محرف سطر جديد في بداية محتوى السلسلة النصية المجردة). نتأكد أن القيمة المُعادة من الدالة search تحتوي فقط على السطر الذي نتوقعه. لا يمكننا تنفيذ هذا الاختبار بعد ورؤيته يفشل لأن الاختبار لا يُصرَّف، والسبب في ذلك هو أن الدالة search غير موجودة بعد. وفقًا لمبادئ التطوير المُقاد بالاختبار، علينا إضافة القليل من الشيفرة البرمجية بحيث يمكننا تصريف الاختبار وتنفيذه بإضافة تعريف الدالة search التي تُعيد شعاعًا vector فارغًا دومًا كما هو موضح في الشيفرة 16، ومن ثم يجب أن يُصرَّف الاختبار ويفشل لعدم مطابقة الشعاع الفارغ للشعاع الذي يحتوي السطر "safe, fast, productive.". اسم الملف: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } الشيفرة 16: تعريف الدالة search باستخدام شيفرة برمجية قصيرة بحيث يُصرَّف الاختبار لاحظ أننا بحاجة إلى تعريف دورة حياة lifetime صراحةً تدعى ‎'a في بصمة الدالة search واستخدام دورة الحياة في الوسيط contents والقيمة المُعادة. تذكر أننا ذكرنا في مقال سابق أن معاملات دورة الحياة تحدد أي دورات حياة الوسطاء متصلة بدورة حياة القيمة المُعادة، وفي هذه الحالة فإننا نحدد أن الشعاع المُعاد يجب أن يحتوي على شرائح سلسلة نصية string slices تمثل مرجعًا لشرائح الوسيط contents بدلًا من الوسيط query. بكلمات أخرى، نخبر رست بأن البيانات المُعادة من الدالة search ستعيش طالما تعيش البيانات المُمرّرة إلى الدالة search في الوسيط contents. يجب أن تكون الشرائح المُستخدمة مثل مراجع للبيانات صالحة حتى يكون المرجع صالحًا، إذ سيكون التحقق من الأمان خاطئًا لو افترض المصرف أننا نُنشئ شرائح سلاسل نصية من query بدلًا من contents. نحصل على الخطأ التالي إذا نسينا توصيف دورات الحياة وجرّبنا تصريف هذه الدالة: $ cargo build Compiling minigrep v0.1.0 (file:///projects/minigrep) error[E0106]: missing lifetime specifier --> src/lib.rs:28:51 | 28 | pub fn search(query: &str, contents: &str) -> Vec<&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 `query` or `contents` help: consider introducing a named lifetime parameter | 28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> { | ++++ ++ ++ ++ For more information about this error, try `rustc --explain E0106`. error: could not compile `minigrep` due to previous error لا يمكن لرست معرفة أي الوسيطين نحتاج، لذا يجب أن نصرح عن ذلك مباشرةً. نعلم أن contents هو الوسيط الذي يجب أن يُربط مع القيمة المُعادة باستخدام دورة الحياة وذلك لأنه الوسيط الذي يحتوي على كامل محتوى الملف النصي الذي نريد أن نعيد أجزاءً متطابقةً منه. لا تتطلب لغات البرمجة الأخرى ربط الوسطاء للقيمة المعادة في بصمة الدالة، إلا أنك ستعتاد على ذلك مع الممارسة. ننصحك بمقارنة هذا المثال مع مثال موجود في مقال سابق. دعنا ننفذ الاختبار: $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 0.97s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 1 test test tests::one_result ... FAILED failures: ---- tests::one_result stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `["safe, fast, productive."]`, right: `[]`', src/lib.rs:44:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::one_result test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` عظيم، فشل الاختبار كما توقعنا. دعنا نجعل الاختبار ينجح الآن. كتابة شيفرة برمجية لاجتياز الاختبار يفشل اختبارنا حاليًا لأننا نُعيد دائمًا شعاعًا فارغًا، وعلى برنامجنا اتباع الخطوات التالية لتصحيح ذلك وتطبيق search: المرور على سطور محتوى الملف. التحقق ما إذا كان السطر يحتوي على السلسلة النصية التي نبحث عنها. إذا كان هذا الأمر محققًا: نضيف السطر إلى قائمة القيم التي سنعيدها. إن لم يكن محققًا: لا نفعل أي شيء. نُعيد قائمة الأسطر التي تحتوي على تطابق مع السلسلة النصية التي نبحث عنها. لنعمل على كل خطوة بالتدريج، بدءًا من المرور على الأسطر على الترتيب. المرور على الأسطر باستخدام التابع lines توفر لنا رست تابعًا مفيدًا للتعامل مع السلاسل النصية سطرًا تلو الآخر بصورةٍ سهلة وهو تابع lines، ويعمل التابع بالشكل الموضح في الشيفرة 17. انتبه إلى أن الشيفرة 17 لن تُصرَّف بنجاح. اسم الملف: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { // أجرِ بعض العمليات على ‫line } } الشيفرة 17: المرور على أسطر الوسيط contents يُعيد التابع lines مكرّرًا iterator، وسنتحدث عن المكررات فيما بعد؛ تذّكر أنك رأيت هذه الطريقة باستعمال المكررات في الشيفرة 5 من فصل سابق عندما استخدمنا حلقة for مع مكرر لتنفيذ شيفرة برمجية على كل عنصر من عناصر التجميعة collection. البحث عن الاستعلام في كل سطر الآن نبحث فيما إذا كان السطر يحتوي على السلسلة النصية المحددة بالاستعلام، وتحتوي السلاسل النصية لحسن الحظ على تابع مفيد يدعى contains يفعل هذا نيابةً عنّا. أضف استدعاءً للتابع contains في الدالة search كما هو موضح في الشيفرة 18. لاحظ أن هذه الشيفرة البرمجية لن تُصرَّف بعد. اسم الملف: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { if line.contains(query) { // do something with line } } } الشيفرة 18: إضافة ميزة البحث عن السلسلة النصية الموجودة في الوسيط query داخل السطر بدأنا ببناء وظيفة البرنامج الأساسية الآن، ولتصريف البرنامج بنجاح نحن بحاجة لإعادة قيمة من متن الدالة كما قلنا أننا سنفعل في بصمة الدالة. تخزين الأسطر المطابقة أخيرًا يجب علينا استخدام طريقة لتخزين الأسطر المُطابقة التي نريد أن نُعيدها من الدالة، ونستخدم لذلك شعاعًا متغيّرًا mutable vector قبل الحلقة for ونستدعي التابع push لتخزين line في الشعاع، ونُعيد هذا الشعاع بعد انتهاء الحلقة for كما هو موضح في الشيفرة 19. اسم الملف: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } الشيفرة 19: تخزين الأسطر المتطابقة بحيث نستطيع إعادتهم من الدالة الآن يجب أن تُعيد الدالة search فقط الأسطر التي تحتوي على الوسيط query مما يعني أن اختبارنا سينجح. دعنا ننفّذ الاختبار: $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 1.22s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 1 test test tests::one_result ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s نجح الاختبار، لذا فالدالة تعمل. يجب أن نفكّر بفرص إعادة بناء التعليمات البرمجية المحتملة بحلول هذه النقطة داخل الدالة search مع المحافظة على نجاح الاختبار للحفاظ على الهدف من الدالة. ليست الشيفرة البرمجية الموجودة في الدالة search سيئة، لكنها لا تستغلّ خصائص المكرّرات المفيدة، وسنعود لهذه الشيفرة البرمجية في مقالات لاحقة عندما نتحدث عن المكررات بتعمق أكبر لمعرفة التحسينات الممكنة. استخدام الدالة search في الدالة run الآن وبعد عمل الدالة search وتجربتها بنجاح، نحتاج إلى استدعاء search من الدالة run، وتمرير قيمة config.query و contents التي تقرأهما run من الملف إلى الدالة search. تطبع الدالة run كل سطر مُعاد من الدالة search: اسم الملف: src/lib.rs pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.file_path)?; for line in search(&config.query, &contents) { println!("{line}"); } Ok(()) } ما زلنا نستخدم الحلقة for لإعادة كل سطر من search وطباعته. يجب أن يعمل كامل البرنامج الآن. دعنا نجرّبه أولًا بكلمة "الضفدع frog" التي ينبغي أن تُعيد سطرًا واحدًا بالتحديد من قصيدة ايميلي ديكنسون Emily Dickinson: $ cargo run -- frog poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.38s Running `target/debug/minigrep frog poem.txt` How public, like a frog عظيم. دعنا نجرب كلمةً يُفترض أنها موجودة في عدة أسطر مثل "body": $ cargo run -- body poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep body poem.txt` I'm nobody! Who are you? Are you nobody, too? How dreary to be somebody! وأخيرًا، دعنا نتأكد من أننا لن نحصل على أي سطر عندما تكون الكلمة غير موجودة ضمن أي سطر في القصيدة مثل الكلمة "monomorphization": $ cargo run -- monomorphization poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep monomorphization poem.txt` ممتاز، بنينا إصدارنا الخاص المصغّر من أداة البحث الكلاسيكية grep، وتعلمنا الكثير عن هيكلة التطبيقات، كما أننا تعلمنا بعض الأشياء بخصوص دخل وخرج الملفات ودورات الحياة والاختبار والحصول على الوسطاء من سطر الأوامر. سنوضّح في المقال التالي كيفية العمل مع متغيرات البيئة في ختام هذا المشروع، إضافةً لكيفية طباعة الأخطاء إلى مجرى الأخطاء القياسي، وهما أمران مهمّان في برامج سطر الأوامر. ترجمة -وبتصرف- لقسم من الفصل An I/O Project: Building a Command Line Program من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: التعامل مع متغيرات البيئة وطباعة الأخطاء في لغة رست المقال السابق: كتابة برنامج سطر أوامر بلغة رست: إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء ما هو سطر الأوامر ؟ كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج
  17. بدأنا في المقال السابق كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج بناء المشروع وسنكمل العملية في هذا المقال حيث سنصلح أربع مشكلات خاصة بهيكل البرنامج وكيفية تعامله مع الأخطاء المحتملة لتحسين برنامجنا. تتمثل المشكلة الأولى في أن للدالة main مهمتان: المرور على لائحة الوسطاء وقراءة الملفات. سيزداد عدد المهام المتفرقة التي تنجزها الدالة main مع نموّ حجم برنامجنا، وتصبح الدالة التي تحتوي على الكثير من المهام صعبة الفهم والاختبار والتعديل دون المخاطرة بتعطيل بعض خصائصها، ومن الأفضل فصل المهام عن بعضها بعضًا بحيث تكون كل دالة مسؤولةً عن مهمةٍ واحدة. تمتدّ المشكلة إلى مشكلة أخرى ثانية: على الرغم من أن query و file_path تمثلان متغيرات بيئة لبرنامجنا إلا أن متغيرات مثل contents تُستخدم في برنامجنا لتنفيذ المنطق، ومع زيادة حجم الدالة main سيزيد عدد المتغيرات التي سنحتاج إضافتها إلى النطاق مما سيصعّب مهمة متابعة قيمة كل منها، ومن الأفضل تجميع متغيرات الضبط في هيكل واحدة لجعل الهدف منها واضح. المشكلة الثالثة هي أننا استخدمنا expect لطباعة رسالة خطأ عندما تفشل عملية قراءة الملف، إلا أن رسالة الخطأ تقتصر على طباعة "Should have been able to read the file"، وقد تفشل قراءة ملف ما لعدّة أسباب؛ إذ يمكن أن يكون الملف مفقودًا؛ أو أنك لا تمتلك الأذونات المناسبة لفتحه، وحاليًا فنحن نعرض رسالة الخطأ ذاتها بغض النظر عن سبب الخطأ، وهو أمرٌ لن يمنح المستخدم أي معلومات مفيدة. رابعًا، استخدمنا expect بصورةٍ متكررة لنتعامل مع الأخطاء المختلفة وإذا نفّذ المستخدم البرنامج دون تحديد عددٍ كافٍ من الوسطاء فسيحصل على الخطأ "index out of bounds" من رست، وهو خطأ لا يشرح بدقة سبب المشكلة. يُفضَّل هنا أن يكون التعامل مع الأخطاء موجودًا في مكان واحد بحيث يعلم المبرمج الذي يعمل على تطوير البرنامج مستقبلًا المكان الذي يجب التوجه إليه في حال أراد تغيير منطق التعامل مع الأخطاء، كما سيسهّل وجود الشيفرة البرمجية التي تتعامل مع الأخطاء في مكان واحد عملية طباعة رسائل خطأ معبّرة للمستخدم. دعنا نصلح هذه المشاكل الأربع بإعادة بناء التعليمات البرمجية. فصل المهام في المشاريع التنفيذية مشكلة تنظيم المسؤوليات المختلفة بالنسبة للدالة main هي مشكلة شائعة في الكثير من المشاريع التنفيذية binary projects، ونتيجةً لذلك طوّر مجتمع رست توجيهات عامة لفصل المهام المختلفة الموجودة في البرنامج التنفيذي عندما تصبح الدالة main كبيرة، وتتلخص هذه العملية بالخطوات التالية: تجزئة برنامجك إلى "main.rs" و "lib.rs" ونقل منطق البرنامج إلى "lib.rs". يمكن أن يبقى منطق الحصول على الوسطاء من سطر الأوامر في "main.rs" طالما هو قصير. عندما يصبح منطق الحصول على الوسطاء من سطر الأوامر معقّدًا صدِّره من "main.rs" إلى "lib.rs". يجب أن تكون المهام التي يجب أن تبقى في main بعد هذه العملية محصورةً بما يلي: استدعاء منطق الحصول على وسطاء سطر الأوامر باستخدام قيم الوسطاء. إعداد أي ضبط لازم. استدعاء الدالة run في "lib.rs". التعامل مع الخطأ إذا أعادت الدالة run خطأ. يحلّ هذا النمط كل ما يتعلق بفصل المهام، إذ يتعامل "main.rs" بكل شيء يخص تشغيل البرنامج، بينما يتعامل "lib.rs" مع منطق المهة المطروحة. بما أنك لا تستطيع اختبار الدالة main مباشرةً، سيساعدك هذا الهيكل في اختبار كل منطق برنامجك بنقله إلى دوال موجودة في "lib.rs"، وستكون الشيفرة البرمجية المتبقىة في "main.rs" قصيرة وسيكون التحقق من صحتها بالنظر إليها ببساطة كافيًا. دعنا نعيد كتابة التعليمات البرمجية باستخدام هذه الخطوات. استخلاص الشيفرة البرمجية التي تحصل على الوسطاء سنستخرج خاصية الحصول على الوسطاء إلى دالة تستدعيها main لتحضير نقل منطق سطر الأوامر إلى src/lib.rs. توضح الشيفرة 5 بدايةً جديدةً من الدالة main تستدعي دالةً جديدة تدعى parse_config وسنعرّفها حاليًا في src/main.rs. اسم الملف: src/main.rs fn main() { let args: Vec<String> = env::args().collect(); let (query, file_path) = parse_config(&args); // --snip-- } fn parse_config(args: &[String]) -> (&str, &str) { let query = &args[1]; let file_path = &args[2]; (query, file_path) } [الشيفرة 5: استخراج الدالة parse_config من main] ما زلنا نجمع وسطاء سطر الأوامر في شعاع، إلا أننا نمرّر الشعاع كاملًا إلى الدالة parse_config بدلًا من إسناد القيمة في الدليل "1" إلى المتغير query والقيمة في الدليل "2" إلى المتغير file_path داخل الدالة main، إذ تحتوي الدالة parse_config على المنطق الذي يحدّد أي القيمتين سيُخزَّن في أي المتغيّرين ومن ثم تعديل القيم إلى الدالة main، إلا أننا ما زلنا نُنشئ المتغيرين query و file_path في main، لكن لا تحمل main مسؤولية تحديد أي وسطاء سطر الأوامر تنتمي إلى أي المتغيرات. قد تبدو هذه الإضافة مبالغةً شديدةً في برنامجنا البسيط هذا، إلا أننا نعيد بناء التعليمات البرمجية بخطوات صغيرة وتدريجية. نفّذ هذا البرنامج بعد تطبيق التعديلات لتتأكد من أن الحصول على الوسطاء ما زال يعمل. من المحبّذ التحقق من تنفيذ البرنامج بعد كل تعديل بحيث تستطيع معرفة سبب المشكلة فورًا إذا حصلت. تجميع قيم الضبط يمكننا اتخاذ خطوة بسيطة لتحسين الدالة parse_config أكثر، إذ أننا نعيد حاليًا صف tuple على الرغم من أننا نجزّء هذا الصف مباشرةً إلى أجزاء متفرقة من جديد، وهذا يشير إلى أننا لا نطبّق الفكرة صحيحًا. كون config جزءًا من parse_config هو مؤشر آخر لوجود احتمالية تحسين، إذ يشير ذلك أن القيمتين التي نُعيدهما مترابطتان وهما جزء من قيمة ضبط واحدة، إلا أننا لا ننقل هذا المعنى بهيكل البيانات، كما أننا نجمع القيمتين في صف. دعنا نضع القيمتين بدلًا من ذلك في هيكل واحد ونمنح كلًا من حقول الهيكل اسمًا معبرًا. ستكون الشيفرة البرمجية بعد ذلك أسهل فهمًا للمطورين الذين سيعملون على الشيفرة البرمجية مستقبلًا، إذ سيوضّح ذلك كيف ترتبط القيم المختلفة مع بعضها بعضًا وهدف كل واحدة منها. توضح الشيفرة 6 التحسينات التي أجريناها على الدالة parse_config. اسم الملف: src/main.rs fn main() { let args: Vec<String> = env::args().collect(); let config = parse_config(&args); println!("Searching for {}", config.query); println!("In file {}", config.file_path); let contents = fs::read_to_string(config.file_path) .expect("Should have been able to read the file"); // --snip-- } struct Config { query: String, file_path: String, } fn parse_config(args: &[String]) -> Config { let query = args[1].clone(); let file_path = args[2].clone(); Config { query, file_path } } [الشيفرة 6: إعادة بناء التعليمات البرمجية في الدالة parse_config لإعادة نسخة instance من الهيكل Config] أَضفنا هيكلًا يدعى Config وعرّفناه، بحيث يحتوي على حقلين query و file_path. تشير بصمة signature الدالة parse_config الآن أنها تُعيد قيمةً من النوع Config، إلا أننا اعتدنا إعادة شرائح السلسلة النصية string slice التي تمثل مرجعًا للنوع String في args، لذلك نعرّف Config بحيث يحتوي على قيمتين مملوكتين owned من النوع String. المتغير args في main هو المالك لقيم الوسطاء ويسمح للدالة parse_config باستعارتها فقط، مما يعني أننا سنخرق قواعد الاستعارة في رست إذا حاول Config أخذ ملكية القيم من args. هناك عدة طرق نستطيع فيها إدارة بيانات String، إلا أن أسهل الطرق غير فعال وهو باستدعاء التابع clone على القيم، مما سيعطينا نسخةً من البيانات بحيث تستطيع Config امتلاكها وهو أمرٌ يستغرق وقتًا ويشغل ذاكرةً أكبر مقارنةً باستخدام مرجع لبيانات السلسلة النصية، إلا أن نسخ البيانات يجعل من شيفرتنا البرمجية واضحةً لأنه ليس علينا وقتها إدارة دورات حياة المراجع، وفي هذه الحالة تُعد مقايضة الفعالية بالأداء بصورةٍ طفيفة مقابل البساطة أمرًا مقبولًا. حدّثنا main بحيث تضع نسخة من الهيكل Config مُعادة بواسطة الدالة parse_config إلى متغير يدعى config وحدّثنا الشيفرة البرمجية التي استخدمت سابقًا المتغيرات query و file_path بصورةٍ منفصلة، إذ نستخدم الآن الحقول الموجودة في Config بدلًا من ذلك. أصبحت شيفرتنا البرمجية الآن توضح بصورةٍ أفضل أن القيمتين query و file_path مترابطتان وأن الهدف منهما هو ضبط كيفية عمل البرنامج. أي شيفرة برمجية تستخدم هاتين القيمتين ستعثر عليهما في نسخة config في الحقول المسمّاة بحسب الهدف منهما. ملاحظة حول سلبيات استخدام clone: يميل مبرمجو لغة رست لتفادي استخدام clone لتصحيح مشاكل الملكية بسبب الوقت الذي يستغرقه تنفيذها. ستتعلم لاحقًا كيفية استخدام توابع ذات كفاءة في حالات مشابهة لهذه، إلا أن نسخ بعض السلاسل النصية حاليًا أمرٌ مقبول لأنك تنسخ هذه القيم مرةً واحدةً فقط ومسار الملف والكلمة التي تبحث عنها ليستا بالحجم الكبير. من الأفضل وجود برنامج لا يعمل بفعالية مئةً بالمئة من محاولة زيادة فعالية الشيفرة البرمجية بصورةٍ مفرطة في محاولتك الأولى، إذ سيصبح الأمر أسهل بالنسبة لك مع اكتسابك للخبرة في رست، بحيث تستطيع كتابة حلول برمجية أكثر فاعلية ومن المقبول الآن استدعاء clone. إنشاء باني للهيكل Config استخلصنا بحلول هذه اللحظة المنطق المسؤول عن الحصول على قيم وسطاء سطر الأوامر من الدالة main ووضعناه في الدالة parse_config، وساعدنا هذا في رؤية كيفية ارتباط القيمتين query و file_path ببعضهما، ثم أضفنا هيكل Config لتسمية الهدف من القيمتين query و file_path ولنكون قادرين على إعادة أسماء القيم مثل حقول هيكل من الدالة parse_config. إذًا، أصبح الآن الهدف من الدالة parse_config إنشاء نسخ من الهيكل Config، ويمكننا تعديل parse_config من دالة اعتيادية إلى دالة تدعى new مرتبطة بالهيكل Config، وسيؤدي هذا التعديل إلى جعل شيفرتنا البرمجية رسميةً idiomatic أكثر. يمكننا إنشاء نسخ من الأنواع الموجودة في المكتبة القياسية مثل String باستدعاء String::new، وكذلك يمكننا بتعديل الدالة parse_config إلى دالة new مرتبطة مع الهيكل Config استدعاء Config::new للحصول على نسخ من Config. توضح الشيفرة 7 التعديلات التي نحتاج لإجرائها. اسم الملف: src/main.rs fn main() { let args: Vec<String> = env::args().collect(); let config = Config::new(&args); // --snip-- } // --snip-- impl Config { fn new(args: &[String]) -> Config { let query = args[1].clone(); let file_path = args[2].clone(); Config { query, file_path } } } [الشيفرة 7: تغيير الدالة parse_config إلى Config::new] حدّثنا الدالة main التي استدعينا فيها parse_config سابقًا لتستدعي Config::new بدلًا من ذلك، وعدّلنا اسم الدالة parse_config إلى new ونقلناها لتصبح داخل كتلة impl، مما يربط الدالة new مع الهيكل Config. جرّب تصريف الشيفرة البرمجية مجددًا لتتأكد من أنها تعمل دون مشاكل. تحسين التعامل مع الأخطاء سنعمل الآن على تصحيح التعامل مع الأخطاء. تذكّر أن محاولتنا للوصول إلى القيم الموجودة في الشعاع args في الدليل 1 أو الدليل 2 تسببت بهلع البرنامج في حال احتواء الشعاع أقل من 3 عناصر. جرّب تنفيذ البرنامج دون أي وسطاء، من المفترض أن تحصل على رسالة الخطأ التالية: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep` thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:27:21 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace يمثل السطر index out of bounds: the len is 1 but the index is 1 رسالة خطأ موجهة للمبرمجين، ولن تساعد مستخدم البرنامج لفهم الخطأ. دعنا نصلح ذلك الآن. تحسين رسالة الخطأ سنُضيف في الشيفرة 8 اختبارًا في الدالة new يتأكد من أن الشريحة طويلة كفاية قبل محاولة الوصول إلى الدليل 1 و2، وإذا لم كانت الشريحة طويلة كفاية، سيهلع البرنامج وسيعرض رسالة خطأ أفضل من الرسالة التي رأيناها سابقًا. اسم الملف: src/main.rs // --snip-- fn new(args: &[String]) -> Config { if args.len() < 3 { panic!("not enough arguments"); } // --snip-- [الشيفرة 8: إضافة اختبار للتحقق من عدد الوسطاء] الشيفرة البرمجية مشابهة لما فعلناه في مقال سابق عند كتابة الدالة Guess::new، إذ استدعينا الماكرو panic!‎ عندما كان الوسيط value خارج مجال القيم الصالحة، إلا أننا نفحص طول args إذا كان على الأقل بطول 3 بدلًا من فحص مجال القيم، وتتابع بقية الدالة فيما بعد عملها بافتراض أن الشرط محقق. إذا احتوى الشعاع args على أقل من ثلاثة عناصر، سيتحقق الشرط الذي يستدعي الماكرو panic!‎ وبالتالي سيتوقف البرنامج مباشرةً. دعنا نجرّب تنفيذ البرنامج بعد إضافتنا للسطور البرمجية القليلة هذه في دالة new دون أي وسطاء ونرى رسالة الخطأ: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep` thread 'main' panicked at 'not enough arguments', src/main.rs:26:13 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace الخرج هذا أفضل لأنه يدلّنا على الخطأ بوضوح أكبر، إلا أننا نحصل على معلومات زائدة لسنا بحاجة لعرضها للمستخدم في ذات الوقت. لعلّ هذه الطريقة ليست بالطريقة المثلى؛ إذ أن استدعاء panic!‎ ملائم لعرض الخطأ في مرحلة كتابة الشيفرة البرمجية للمبرمج وليس في مرحلة استخدام البرنامج للمستخدم (كما ناقشنا في مقالات سابقة)، نستخدم بدلًا من ذلك طريقة أخرى تعلمناها سابقًا ألا وهي إعادة النوع Result الذي يمثّل قيمة نجاح أو فشل. إعادة النوع Result بدلا من استدعاء panic!‎ يمكننا إعادة قيمة Result التي تحتوي على نسخة من Config في حال النجاح وقيمة تصف الخطأ في حالة الفشل. سنغيّر أيضًا اسم الدالة new إلى build، إذ سيتوقع العديد من المبرمجين أن الدالة new لن تفشل أبدًا. يمكننا استخدام النوع Result للإشارة إلى مشكلة عندما تتواصل الدالة Config::build مع الدالة main، ومن ثم يمكننا التعديل على main بحيث تحوّل المتغاير‏ Err إلى خطأ أوضح للمستخدم دون النص الذي يتضمن thread 'main'‎ و RUST_BACKTRACE وهو ما ينتج عن استدعاء panic!‎. توضح الشيفرة 9 التغييرات التي أجريناها لقيمة الدالة المُعادة -التي تحمل الاسم Config::build الآن- ومتن الدالة الذي يُعيد قيمة Result. لاحظ أن هذه الشيفرة لن تُصرَّف حتى نعدل الدالة main أيضًا، وهو ما سنفعله في الشيفرة التي تليها. اسم الملف: src/main.rs impl Config { fn build(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let file_path = args[2].clone(); Ok(Config { query, file_path }) } } [الشيفرة 9: إعادة قيمة Result من الدالة Config::build] تُعيد الدالة build قيمة Result بنسخة Config في حال النجاح و ‎&'static str في حال الفشل، وستكون قيم الأخطاء سلاسل نصية مجرّدة string literals دومًا بدورة حياة ‎'static. أجرينا تعديلين على محتوى الدالة، فبدلًا من استدعاء panic!‎ عندما لا يمرّر المستخدم عددًا كافيًا من الوسطاء، أصبحنا نُعيد قيمة Err، وغلّفنا قيمة Config المُعادة بمتغاير Ok. تجعل هذه التغييرات من الدالة متوافقة مع نوع بصمتها الجديد. تسمح إعادة القيمة Err من Config::build إلى الدالة main بالتعامل مع القيمة Result المُعادة من الدالة build ومغادرة البرنامج بصورةٍ أفضل في حالة الفشل. استدعاء الدالة Config::build والتعامل مع الأخطاء نحتاج لتعديل الدالة main بحيث تتعامل مع النوع Result المُعاد إليها من الدالة Config::build كي نتعامل مع الأخطاء عند حدوثها وطباعة رسالة مفهومة للمستخدم، وهذا التعديل موضّح في الشيفرة 10. كما أننا سنعدّل البرنامج بحيث نخرج من أداة سطر الأوامر برمز خطأ غير صفري عند استدعاء panic!‎ إلا أننا سنطبق ذلك يدويًا. رمز الخطأ غير الصفري nonzero exit code هو اصطلاح للإشارة إلى العملية التي استدعت البرنامج الذي تسبب بالخروج من برنامجنا برمز الخطأ. اسم الملف: src/main.rs use std::process; fn main() { let args: Vec<String> = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { println!("Problem parsing arguments: {err}"); process::exit(1); }); // --snip-- [الشيفرة 10: الخروج برمز خطأ إذا فشل بناء Config] استخدمنا في الشيفرة السابقة تابعًا لم نشرحه بالتفصيل بعد، ألا وهو unwrap_or_else وهو تابع معرَّف في Result<T, E>‎ في المكتبة القياسية. يسمح لنا استخدام التابع unwrap_or_else بتعريف بعض طرق التعامل مع الأخطاء المخصصة التي لا تستخدم الماكرو panic!‎. سلوك التابع مماثل للتابع unwrap إذا كانت قيمة Result هي Ok، إذ أنه يعيد القيمة المغلّفة داخل Ok، إلا أن التابع يستدعي شيفرةً برمجيةً في مغلِّفه closure إذا كانت القيمة Err وهي دالة مجهولة anonymous function نعرّفها ونمرّرها بمثابة وسيط للتابع unwrap_or_else. سنتحدث عن المُغلِّفات بالتفصيل لاحقًا، وكل ما عليك معرفته حاليًا هو أن unwrap_or_else ستمرّر القيمة الداخلية للقيمة Err -وهي في هذه الحالة السلسلة النصية "not enough arguments" التي أضفناها في الشيفرة 9- إلى مغلّفنا ضمن الوسيط err الموجود بين الخطين الشاقوليين (|)، ومن ثم تستطيع الشيفرة البرمجية الموجودة في المغلّف استخدام القيمة الموجودة في err عند تنفيذها. أضفنا سطرuse جديد لإضافة process من المكتبة القياسية إلى النطاق. تُنفَّذ الشيفرة البرمجية الموجودة في المغلّف في حالة الخطأ ضمن سطرين فقط: نطبع قيمة err ومن ثم نستدعي process::exit. توقف الدالة process::exit البرنامج مباشرةً وتُعيد العدد المُمرّر إليها بمثابة رمز حالة خروج. تشابه العملية استخدام panic!‎ في الشيفرة 8، إلا أننا لا نحصل على الخرج الإضافي بعد الآن. دعنا نجرّب الأمر: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.48s Running `target/debug/minigrep` Problem parsing arguments: not enough arguments عظيم، فالرسالة التي نحصل عليها الآن مفهومةً أكثر للمستخدمين. استخراج المنطق من الدالة main الآن وبعد انتهائنا من إعادة بناء التعليمات البرمجية الخاصة بالحصول على الوسطاء، دعنا ننتقل إلى منطق البرنامج، إذ علينا أن نستخرج المنطق إلى دالةٍ نسميها run كما ذكرنا سابقًا، بحيث تحتوي على الشيفرة البرمجية الموجودة في main حاليًا مع استثناء الشيفرة البرمجية المخصصة لضبط البرنامج، أو التعامل مع الأخطاء، وبحلول نهاية المهمة هذه يجب أن تكون الدالة main سهلة الفحص والقراءة ومختصرة، وسنكون قادرين على كتابة الاختبارات لجزء المنطق من البرنامج بصورةٍ منفصلة. توضح الشيفرة 11 الدالة run المُستخلصة، وسنبدأ بإجراء تحسينات صغيرة وتدريجية حاليًا لاستخلاص المنطق إلى الدالة. نبدأ بتعريف الدالة في src/main.rs. اسم الملف: src/main.rs fn main() { // --snip-- println!("Searching for {}", config.query); println!("In file {}", config.file_path); run(config); } fn run(config: Config) { let contents = fs::read_to_string(config.file_path) .expect("Should have been able to read the file"); println!("With text:\n{contents}"); } // --snip-- [الشيفرة 11: استخراج الدالة run التي تحتوي على بقية منطق البرنامج] تحتوي الدالة run الآن على جميع المنطق المتبقي في main بدءًا من قراءة الملف، وتأخذ الدالة run نسخةً من الهيكل Config مثل وسيط. إعادة الأخطاء من الدالة run أصبح بإمكاننا -بعد فصل منطق البرنامج في الدالة run- تحسين التعامل مع الأخطاء كما فعلنا بالدالة Config::build في الشيفرة 9. تُعيد الدالة run القيمة Result<T, E>‎ بعد حصول خطأ بدلًا من السماح للبرنامج بالهلع باستدعاء expect، وسيسمح لنا ذلك بدعم المنطق الخاص بالتعامل مع الأخطاء في main بطريقة سهلة الاستخدام. توضح الشيفرة 12 التغييرات الواجب إجرائها ومحتوى الدالة run. اسم الدالة: src/main.rs use std::error::Error; // --snip-- fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.file_path)?; println!("With text:\n{contents}"); Ok(()) } [الشيفرة 12: تعديل الدالة run بحيث تعيد القيمة Result] أجرينا ثلاثة تعديلات هنا؛ إذ عدّلنا أولًا القيمة المُعادة من الدالة run إلى النوع Result<(), Box<dyn Error>>‎، فقد أعادت هذه الدالة سابقًا نوع الوحدة unit type ()، إلا أننا نُبقي هذا النوع مثل قيمة مُعادة في حالة Ok. استخدمنا كائن السمة Box<dyn Error>‎ لنوع الخطأ (وقد أضفنا std::error::Error إلى النطاق باستخدام التعليمة use في الأعلى). سنغطّي كائنات السمة لاحقًا، ويكفي الآن معرفتك أن Box<dyn Error>‎ تعني أن الدالة ستُعيد نوعًا يطبّق السمة Error، إلا أن تحديد نوع القيمة المُعادة ليس ضروريًا. يمنحنا ذلك مرونة إعادة قيم الخطأ التي قد تكون من أنواع مختلفة في حالات فشل مختلفة، والكلمة المفتاحية dyn هي اختصار للكلمة "ديناميكي dynamic". يتمثّل التعديل الثاني بإزالة استدعاء expect واستبداله بالعامل ?، الذي شرحناه سابقًا هنا؛ فبدلًا من استخدام الماكرو panic!‎ على الخطأ، يُعيد العامل ? قيمة الخطأ من الدالة الحالية للشيفرة البرمجية المستدعية لكي تتعامل معه. ثالثًا، تُعيد الدالة run الآن قيمة Ok في حالة النجاح. صرّحنا عن نوع نجاح الدالة run في بصمتها على النحو التالي: ()، مما يعني أننا بحاجة تغليف قيمة نوع الوحدة في قيمة Ok. قد تبدو طريقة الكتابة Ok(())‎ هذه غريبة قليلًا، إلا أن استخدام () بهذا الشكل هو طريقة اصطلاحية للإشارة إلى أننا نستدعي run من أجل تأثيرها الجانبي فقط، إذ أنها لا تُعيد قيمةً نستخدمها. ستُصرَّف الشيفرة البرمجية السابقة عند تنفيذها، إلا أننا سنحصل على التحذير التالي: $ cargo run the poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) warning: unused `Result` that must be used --> src/main.rs:19:5 | 19 | run(config); | ^^^^^^^^^^^^ | = note: `#[warn(unused_must_use)]` on by default = note: this `Result` may be an `Err` variant, which should be handled warning: `minigrep` (bin "minigrep") generated 1 warning Finished dev [unoptimized + debuginfo] target(s) in 0.71s Running `target/debug/minigrep the poem.txt` Searching for the In file poem.txt With text: I'm nobody! Who are you? Are you nobody, too? Then there's a pair of us - don't tell! They'd banish us, you know. How dreary to be somebody! How public, like a frog To tell your name the livelong day To an admiring bog! تخبرنا رست أن شيفرتنا البرمجية تتجاهل قيمة Result، وأن قيمة Result قد تشير إلى حصول خطأ ما، إلا أننا لا نتحقق من حدوث خطأ، ويذكّرنا المصرّف بأنه من الأفضل لنا كتابة شيفرة برمجية للتعامل مع الأخطاء هنا. دعنا نصحّح هذه المشكلة الآن. التعامل مع الأخطاء المُعادة من الدالة run في الدالة main سنتحقق من الأحطاء ونتعامل معها باستخدام طرق مماثلة للطرق التي استخدمناها مع Config::build في الشيفرة 10، مع اختلاف بسيط: اسم الملف: src/main.rs fn main() { // --snip-- println!("Searching for {}", config.query); println!("In file {}", config.file_path); if let Err(e) = run(config) { println!("Application error: {e}"); process::exit(1); } } نستخدم if let بدلًا من unwrap_or_else للتحقق فيما إذا كانت الدالة run تُعيد قيمة Err ونستدعيprocess::exit(1)‎ إذا كانت هذه الحالة محققة. لا تُعيد الدالة run قيمة نحتاج لفك التغليف عنها unwrap باستخدام unwrap بالطريقة ذاتها التي تُعيد فيها الدالة Config::build نسخةً من Config. نهتم فقط بحالة حصول خطأ والتعرف عليه لأن run تُعيد () في حالة النجاح، لذا لا نحتاج من unwrap_or_else أن تُعيد القيمة المفكوك تغليفها، والتي هي ببساطة (). محتوى تعليمات if let ودوال unwrap_or_else مماثلة في الحالتين: إذ نطبع الخطأ، ثم نغادر البرنامج. تجزئة الشيفرة البرمجية إلى وحدة مكتبة مصرفة يبدو مشروع minigrep جيّدًا حتى اللحظة. سنجزّء الآن ملف src/main.rs ونضع جزءًا من الشيفرة البرمجية في ملف "src/lib.rs"، إذ يمكننا بهذه الطريقة اختبار الشيفرة البرمجية مع ملف src/main.rs لا ينجز العديد من المهام. دعنا ننقل الشيفرة البرمجية غير الموجودة في الدالة main من الملف src/main.rs إلى الملف src/lib.rs، والتي تتضمن: تعريف الدالة run. تعليمات use المرتبطة بالشيفرة البرمجية التي سننقلها. تعريف الهيكل Config. تعريف دالة Config::build. يجب أن يحتوي الملف src/lib.rs على البصمات الموضحة في الشيفرة 13 (أهملنا محتوى الدوال لاختصار طول الشيفرة). لاحظ أن الشيفرة البرمجية لا تُصرَّف حتى نعدّل الملف src/main.rs وهو ما نفعله في الشيفرة 14. اسم الملف: src/lib.rs use std::error::Error; use std::fs; pub struct Config { pub query: String, pub file_path: String, } impl Config { pub fn build(args: &[String]) -> Result<Config, &'static str> { // --snip-- } } pub fn run(config: Config) -> Result<(), Box<dyn Error>> { // --snip-- } [الشيفرة 13: نقل Config و run إلى src/lib.rs] استخدمنا الكلمة المفتاحية pub هنا بحرية في كل من الهيكل Config وحقوله وتابعه build وعلى الدالة run. أصبح لدينا وحدة مكتبة مصرَّفة library crate بواجهة برمجية عامة public API يمكننا استخدامها. نحتاج إضافة الشيفرة البرمجية التي نقلناها إلى الملف src/lib.rs إلى نطاق الوحدة الثنائية المصرفة binary crate في الملف src/main.rs كما هو موضح في الشيفرة 14: اسم الملف: src/main.rs use std::env; use std::process; use minigrep::Config; fn main() { // --snip-- if let Err(e) = minigrep::run(config) { // --snip-- } } [الشيفرة 14: استخدام وحدة المكتبة المصرفة minigrep في src/main.rs] أضفنا السطر use minigrep::Config لإضافة النوع Config من وحدة المكتبة المصرّفة إلى نطاق الوحدة الثنائية المصرّفة، وأسبقنا prefix الدالة run باسم الوحدة المصرفة crate. يجب أن تكون وظائف البرنامج مترابطة مع بعضها بعضًا الآن، وأن تعمل بنجاح، لذا نفّذ البرنامج باستخدام cargo run وتأكد من أن كل شيء يعمل كما هو مطلوب. أخيرًا، كان هذا عملًا شاقًا، إلا أننا بدأنا بأساس يضمن لنا النجاح في المستقبل، إذ أصبح التعامل مع الأخطاء الآن سهلًا وقد جعلنا من شيفرتنا البرمجية أكثر معيارية. سنعمل في الملف src/lib.rs بصورةٍ أساسية من الآن وصاعدًا. دعنا نستفيد من المعيارية الجديدة في برنامجنا بتحقيق شيءٍ كان من الممكن أن يكون صعب التحقيق في الشيفرة البرمجية القادمة، إلا أنه أصبح أسهل بالشيفرة البرمجية الجديدة، ألا وهو كتابة الاختبارات. ترجمة -وبتصرف- لقسم من الفصل An I/O Project: Building a Command Line Program من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: كتابة برنامج سطر أوامر بلغة رست: اختبار البرنامج المقال السابق: كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust حل المشكلات وأهميتها في احتراف البرمجة
  18. يمثّل هذا المقال تطبيقًا لجميع المهارات التي تعلمتها حتى الآن باتباعك لهذه السلسلة البرمجة بلغة رست، ونظرةً عمليةً إلى المزيد من المزايا الموجودة في المكتبة القياسية Standard Library. سنبني سويًّا أداة سطر أوامر command line tool تتفاعل مع ملف وتُجري عمليات الدخل والخرج باستخدام سطر الأوامر للتمرُّن على بعض مفاهيم رست التي تعلمتها حتى هذه اللحظة. تجعل السرعة والأمان والخرج الثنائي الوحيد ودعم مختلف المنصات cross-platform من رست لغةً مثالية لإنشاء أدوات السطر البرمجي، لذا سيكون مشروعنا هو إصدار خاص من أداة بحث سطر الأوامر الكلاسيكية "grep" (اختصارًا للبحث العام باستخدام التعابير النمطية والطباعة ‎globally search a regular expression and print). يبحث "grep" في حالات الاستخدام الأسهل على سلسلة نصية string محددة داخل ملف معين، ولتحقيق ذلك يأخذ "grep" مسار الملف وسلسلة نصية مثل وسطاء له، ثم يقرأ الملف ويجد الأسطر التي تحتوي على وسيط السلسلة النصية داخل الملف ويطبع هذه الأسطر. سنوضّح لك كيف ستستخدم أداة سطر الأوامر لخصائص سطر الأوامر وهو ما تستخدمه العديد من أدوات سطر الأوامر الأخرى، إذ سنقرأ قيمة متغير البيئة environment variable للسماح للمستخدم بضبط سلوك أداتنا، كما أننا سنطبع رسالة خطأ إلى مجرى الخطأ القياسي الخاص بالطرفية standard error console stream -أو اختصارًا stderr- بدلًا من الخرج القياسي standard output -أو اختصارًا stdout. لذلك، يمكن للمستخدم مثلًا إعادة توجيه الخرج الناجح إلى ملف بحيث يظل قادرًا على رؤية رسالة الخطأ على الشاشة في الوقت ذاته. أنشأ عضو من مجتمع رست يدعى آندرو غالانت Andrew Gallant إصدارًا سريعًا ومليئًا بالمزايا من grep وأطلق عليه تسمية ripgrep. سيكون إصدارنا الذي سننشئه في هذه المقالة أكثر بساطةً إلا أن هذه المقالة ستمنحك بعض الأساسيات التي تحتاجها لتفهم المشاريع العملية الواقعية مثل ripgrep. سيجمع مشروع grep الخاص بنا عددًا من المفاهيم التي تعلمناها سابقًا: تنظيم الشيفرة البرمجية (استخدام ما تعلمناه بخصوص الوحدات modules في مقال الحزم packages والوحدات المصرفة crates في لغة رست Rust) استخدام الأشعة vectors والسلاسل النصية (في مقال تخزين لائحة من القيم باستخدام الأشعة Vectors في لغة رست Rust) التعامل مع الأخطاء (في مقال الأخطاء والتعامل معها في لغة رست Rust) استخدام السمات traits ودورات الحياة lifetimes عند الحاجة (في مقال السمات Traits في لغة رست Rust ومقال التحقق من المراجع References عبر دورات الحياة Lifetimes في لغة رست) كتابة الاختبارات (في مقال كتابة الاختبارات في لغة رست Rust) سنتكلم أيضًا بإيجاز عن المغلّفات closures والمكرّرات iterators وسمة الكائنات trait objects، إلا أننا سنتكلم عن هذه المفاهيم بالتفصيل لاحقًا. الحصول على الوسطاء من سطر الأوامر دعنا نُنشئ مشروعًا جديدًا باستخدام cargo new كما اعتدنا، سنسمّي مشروعنا باسم "minigrep" للتمييز بينه وبين الأداة grep التي قد تكون موجودةً على نظامك مسبقًا. $ cargo new minigrep Created binary (application) `minigrep` project $ cd minigrep المهمة الأولى هي بجعل minigrep يقبل وسيطين من سطر الأوامر، ألا وهما مسار الملف وسلسلة نصية للبحث عنها، أي أننا نريد أن نكون قادرين على تنفيذ برنامجنا باستخدام cargo run متبوعًا بشرطتين للدلالة على أن الوسطاء التي ستتبعها تنتمي للبرنامج الذي نريد أن ننفذه وليس لكارجو cargo، سيكون لدينا وسيطان أولهما سلسلة نصية للبحث عنها وثانيهما مسار الملف الذي نريد البحث بداخله على النحو التالي: $ cargo run -- searchstring example-filename.txt لا يستطيع البرنامج المولَّد باستخدام cargo new حاليًا معالجة الوسطاء التي نمرّرها له، ويمكن أن تساعدك بعض المكتبات الموجودة على crates.io بكتابة برامج تقبل وسطاء سطر الأوامر، إلا أننا سنطبّق هذا الأمر بأنفسنا بهدف التعلُّم. قراءة قيم الوسطاء سنحتاج لاستخدام الدالة std::env::args الموجودة في مكتبة رست القياسية لتمكين minigrep من قراءة قيم وسطاء سطر الأوامر التي نمرّرها إليه، إذ تُعيد هذه الدالة مكرّرًا iterator إلى وسطاء سطر الأوامر الممرّرة إلى minigrep. سنغطّي المكررات لاحقًا، إلا أنه من الكافي الآن معرفتك معلومتين بخصوص المكررات: تنتج المكررات مجموعةً من القيم، ويمكننا استدعاء التابع collect على مكرّر لتحويله إلى تجميعة collection مثل الأشعة التي تحتوي جميع العناصر التي تنتجها المكررات. تسمح الشيفرة 1 لبرنامجك minigrep بقراءة أي وسطاء سطر أوامر تمررها إليه، ثمّ تجمّع القيم في شعاع. اسم الملف: src/main.rs use std::env; fn main() { let args: Vec<String> = env::args().collect(); dbg!(args); } [الشيفرة 1: تجميع وسطاء سطر الأوامر في شعاع وطباعتها] نُضيف أوّلًا الوحدة std::env إلى النطاق scope باستخدام تعليمة use، بحيث يمكننا استخدام الدالة args المحتواة داخلها. لاحظ أن الدالة std::env::args متداخلة nested مع مستويين من الوحدات. سنختار إحضار الوحدة الأب إلى النطاق بدلًا من الدالة كما ناقشنا سابقًا في الحالات التي تكون فيها الدالة المطلوبة متداخلة مع أكثر من وحدة واحدة، ويمكننا بذلك استخدام الدوال الأخرى من std::env، كما أن هذه الطريقة أقل غموضًا من إضافة use std::env::args، ثم استدعاء الدالة بكتابة args فقط لأن args قد يُنظر إليها بكونها دالة معرّفة بالوحدة الحالية بصورةٍ خاطئة. ملاحظة: ستهلع std::env::args إذا احتوى أي وسيط على يونيكود غير صالح، وإذا احتاج البرنامج لقبول الوسطاء التي تحتوي على يونيكود غير صالح، فعليك استخدام std::env::args_os بدلًا منها، إذ تعيد هذه الدالة مكرّرًا ينتج قيم OsString بدلًا من قيم String، وقد اخترنا استخدام std::env::args هنا لبساطتها، ونظرًا لاختلاف قيم OsString بحسب المنصة وهي أكثر تعقيدًا في التعامل معها مقارنةً بقيم String. نستدعي env::args في السطر الأول من main ومن ثم نستخدم collect مباشرةً لتحويل المكرّر إلى شعاع يحتوي على جميع القيم الموجودة في المكرّر، ويمكننا استخدام collect هنا لإنشاء عدة أنواع من التجميعات، لذا يجب أن نشير صراحةً لنوع args لتحديد أننا نريد شعاع من السلاسل النصية، وعلى الرغم من أن تحديد الأنواع في رست نادر، إلا أن collect دالة يجب أن تحدد فيها النوع عادةً، لأن رست لا تستطيع استنتاج نوع التجميعة التي تريدها. أخيرًا، نطبع الشعاع باستخدام ماكرو تنقيح الأخطاء debug macro. دعنا نجرّب تنفيذ الشيفرة البرمجية أولًا دون استخدام أي وسطاء ومن ثم باستخدام وسيطين: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.61s Running `target/debug/minigrep` [src/main.rs:5] args = [ "target/debug/minigrep", ] $ cargo run -- needle haystack Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 1.57s Running `target/debug/minigrep needle haystack` [src/main.rs:5] args = [ "target/debug/minigrep", "needle", "haystack", ] لاحظ أن القيمة الأولى في الشعاع هي "target/debug/minigrep" وهو اسم ملفنا التنفيذي، وهذا يطابق سلوك لائحة الوسطاء في لغة سي C بالسماح للبرامج باستخدام الاسم الذي كان السبب في بدء تنفيذها، ومن الملائم عادةً الحصول على اسم البرنامج في حال أردت طباعته ضمن رسائل، أو تعديل سلوك البرنامج المبني على اسم سطر الأوامر البديل command line alias الذي نستخدمه لبدء تشغيل البرنامج، وسنتجاهله الآن ونحفظ فقط أول وسيطين نستخدمهما. حفظ قيم الوسطاء في متغيرات يستطيع البرنامج حاليًا الحصول على القيم المحدّدة مثل وسطاء سطر أوامر، أما الآن فنحن بحاجة لحفظ قيم الوسيطين في متغيرين بحيث يمكننا استخدام المتغيرين ضمن بقية البرنامج، وهو ما فعلناه في الشيفرة 2. اسم الملف: src/main.rs use std::env; fn main() { let args: Vec<String> = env::args().collect(); let query = &args[1]; let file_path = &args[2]; println!("Searching for {}", query); println!("In file {}", file_path); } [الشيفرة 2: إنشاء متغيرين لتخزين وسيط السلسلة النصية ووسيط مسار الملف] يحتلّ اسم البرنامج القيمة الأولى في الشعاع عند args[0]‎ كما رأينا سابقًا عندما طبعنا الشعاع، لذا نبدأ من الدليل "1" إذ أن الوسيط الأول للبرنامج minigrep هو السلسلة النصية التي سنبحث عنها، لذا نضع مرجعًا reference على أول وسيط في المتغير query، بينما يمثل الوسيط الثاني مسار الملف، لذا نضع مرجعًا عليه في المتغير file_path. نطبع مؤقتًا قيم المتغيرَين لنتأكد من أن الشيفرة البرمجية تعمل وفق ما هو متوقع. دعنا ننفذ البرنامج مجددًا بالوسيطين test و sample.txt: $ cargo run -- test sample.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep test sample.txt` Searching for test In file sample.txt عظيم، يعمل برنامجنا بنجاح، إذ تُمرّر قيم الوسطاء التي نحتاجها وتُحفظ في المتغيرات المناسبة. سنضيف لاحقًا شيفرةً برمجيةً للتعامل مع الأخطاء في حالات استخدام خاطئة محتملة، مثل الحالة التي لا يدخل فيها المستخدم أي وسطاء، والتي سنتجاهلها الآن ونبدأ بالعمل على إضافة شيفرة برمجية لقراء الملف بدلًا من ذلك. قراءة ملف سنضيف الآن إمكانية قراءة الملف المحدد في الوسيط file_path. نحتاج أولًا لملف تجريبي لتجربة البرنامج باستخدامه، وسنستخدم ملفًا يحتوي على نصٍ قصير يحتوي على عدّة أسطر مع كلمات مكرّرة. تحتوي الشيفرة 3 على قصيدة لإيميلي ديكنز Emily Dickinson وهو ما سنستخدمه هنا. أنشئ ملفًا يدعى "poem.txt" في مستوى جذر المشروع وأدخل قصيدة "أنا لا أحد! من أنت؟ I'm Nobody! Who are you?‎". اسم الملف: poem.txt I'm nobody! Who are you? Are you nobody, too? Then there's a pair of us - don't tell! They'd banish us, you know. How dreary to be somebody! How public, like a frog To tell your name the livelong day To an admiring bog! [الشيفرة 3: قصيدة إيميلي ديكنز تمثّل ملف تجريبي مناسب] بعد تهيئتك للنص، عدّل الملف "src/main.rs" وضِف شيفرة برمجية لقراءة الملف كما هو موضح في الشيفرة 4. اسم الملف: src/main.rs use std::env; use std::fs; fn main() { // --snip-- println!("In file {}", file_path); let contents = fs::read_to_string(file_path) .expect("Should have been able to read the file"); println!("With text:\n{contents}"); } [الشيفرة 4: قراءة محتويات الملف المحدّد وفق الوسيط الثاني] أضفنا أولًا جزءًا متعلقًا بالبرنامج من المكتبة القياسية باستخدام تعليمة use إلى النطاق، لأننا نحتاج إلى std::fs إلى التعامل مع الملفات. تأخذ التعليمة fs::read_to_string الجديدة في الدالة main القيمة file_path، ثم تفتح ذلك الملف وتُعيد قيمةً من النوع std::io::Result<String>‎ تمثّل محتويات الملف. أضفنا مجددًا تعليمة println!‎ مؤقتة تطبع قيمة contents بعد قراءة الملف، حتى نتأكد من عمل البرنامج بصورةٍ صحيحة. لننفّذ هذه الشيفرة البرمجية باستخدام أي سلسلة نصية تمثّل وسيط سطر أوامر أول (لأننا لم نطبق جزء البحث عن السلسلة النصية بعد) والملف "poem.txt" بمثابة وسيط ثاني: $ cargo run -- the poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep the poem.txt` Searching for the In file poem.txt With text: I'm nobody! Who are you? Are you nobody, too? Then there's a pair of us - don't tell! They'd banish us, you know. How dreary to be somebody! How public, like a frog To tell your name the livelong day To an admiring bog! عظيم، تقرأ الآن الشيفرة البرمجية محتويات الملف ثم تطبعها، إلا أن الشيفرة البرمجية تحتوي على بعض الثغرات، إذ تحتوي الدالة main الآن على عدّة مسؤوليات، ومن الأفضل عمومًا استخدام دالة واحدة لمسؤولية واحدة للحصول على دوال أسهل بالتعامل وأوضح، والمشكلة الثانية هي أننا لم نتعامل مع الأخطاء كما ينبغي لنا، إلا أن البرنامج ما زال صغيرًا وبالتالي لا تشكل هذه المشاكل تهديدًا كبيرًا، لكنها ستصبح صعبة الحل مع زيادة حجم البرنامج، فمن الأفضل إعادة بناء التعليمات البرمجية refactor بمرحلة مبكرة من تطوير البرنامج لأن إعادة بناء التعليمات البرمجية سيكون أسهل بكثير من كميات قليلة من الشيفرات البرمجية، لذا دعنا نفعل ذلك تاليًا في المقال التالي. ترجمة -وبتصرف- لقسم من الفصل Refactoring to Improve Modularity and Error Handling من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: كتابة برنامج سطر أوامر بلغة رست: إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء المقال السابق: تنظيم الاختبارات Tests في لغة رست Rust ما هو سطر الأوامر 10 أمثلة عملية على استخدام الأداة Grep تفيد المبرمجين
  19. يُعد اختبار الشيفرة البرمجية كما ذكرنا سابقًا في مقال كتابة الاختبارات في لغة رست Rust ممارسةً معقدة، ويستخدم الناس الاختبارات بطرق ومصطلحات مختلفة، إضافةً إلى تنظيمها، إلا أن مجتمع مبرمجي لغة رست ينظر إلى الاختبارات بكونها تنتمي إلى أحد التصنيفين الرئيسين: اختبارات الوحدة unit tests واختبارات التكامل integration tests. تعدّ اختبارات الوحدة اختبارات صغيرة ومحددة على وحدة معيّنة منعزلة ويُمكن أن تختبر الواجهات الخاصة private interfaces، بينما تكون اختبارات التكامل خارجية كليًا لمكتبتك وتستخدم شيفرتك البرمجية بالطريقة ذاتها التي تستخدم فيها شيفرة برمجية خارجية اعتيادية شيفرتك البرمجية باستخدام الواجهات العامة public interface وتتضمن غالبًا أكثر من وحدة ضمن الاختبار الواحد. كتابة نوعَي الاختبارات مهمٌ للتأكد من أن أجزاء في مكتبتك تنجز ما هو مطلوب منها بغض النظر عن باقي الأجزاء. اختبارات الوحدة الهدف من اختبارات الوحدة هو اختبار كل وحدة من شيفرة برمجية بمعزل عن الشيفرة البرمجية المتبقية، وذلك لتشخيص النقطة التي لا تعمل فيها الشيفرة البرمجية بصورةٍ صحيحة وبدقة، لذلك ستضع اختبارات الوحدة في المجلد "src" في كل ملف مع الشيفرة البرمجة التي تختبرها، ويقتضي الاصطلاح بإنشاء وحدة تدعى tests في كل ملف لاحتواء دوال الاختبار وتوصيف الوحدة باستخدام cfg(test)‎. وحدة الاختبارات وتوصيف ‎#[cfg(test)]‎ يخبر توصيف ‎#[cfg(test)]‎ على وحدة الاختبارات رست بأنه يجب تصريف وتشغيل شيفرة الاختبار البرمجية فقط عند تنفيذ cargo test وليس عند تنفيذ cargo build، مما يختصر وقتًا من عملية التصريف عندما تريد فقط بناء المكتبة وتوفير المساحة التي سيشغلها الملف المُصرَّف الناتج وذلك لأن الاختبارات غير مُضمّنة به. توضع اختبارات التكامل في مجلد مختلف ولا تستخدم التوصيف ‎#[cfg(test)]‎، إلا أنك بحاجة لاستخدام ‎#[cfg(test)]‎ لتحديد أنها لا يجب أن تُضمَّن في الملف المصرَّف الناتج لأن اختبارات الوحدة موجودة في ملف الشيفرة البرمجية ذاته. تذكر أن كارجو Cargo ولّد الشيفرة البرمجية التالية لنا عندما ولدنا المشروع adder الجديد سابقًا: اسم الملف: src/lib.rs #[cfg(test)] mod tests { #[test] fn it_works() { let result = 2 + 2; assert_eq!(result, 4); } } تُولّد هذه الشيفرة البرمجية تلقائيًا على هيئة وحدة اختبار. تمثّل السمة cfg اختصارًا لكلمة الضبط configuration، إذ تُعلم رست أن العنصر الآتي يجب أن يُضمَّن فقط في خيار ضبط معيّن، وفي هذه الحالة فإن خيار الضبط test الموجود في رست لتصريف وتنفيذ الاختبارات. يصرّف كارجو شيفرة الاختبار البرمجية فقط في حال تنفيذ الاختبارات بفعالية باستخدام cargo test، وهذا يتضمّن أي دوال مساعدة يمكن أن تكون داخل هذه الوحدة، إضافةً للدوال الموصّفة باستخدام ‎#[test]‎. اختبار الدوال الخاصة هناك اختلافٌ بين المبرمجين بخصوص الاختبارات وبالأخص اختبار الدوال الخاصة، إذ يعتقد البعض أن الدوال الخاصة يجب أن تُختبر مباشرةً، بينما لا يتفق البعض الآخر مع ذلك، وتجعل لغات البرمجة اختبار الدوال الخاصة صعبًا أو مستحيلًا، وبغض النظر عمّا تعتقد بخصوص هذا الشأن، تسمح قوانين خصوصية رست لك باختبار الدوال الخاصة. ألقِ نظرةً على الشيفرة 12 التي تحتوي على الشيفرة الخاصة internal_adder. اسم الملف: src/lib.rs pub fn add_two(a: i32) -> i32 { internal_adder(a, 2) } fn internal_adder(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn internal() { assert_eq!(4, internal_adder(2, 2)); } } [الشيفرة 12: اختبار دالة خاصة] لاحظ أن الدالة internal_adder ليست دالة عامة (لا تحتوي على pub). الاختبارات هي شيفرة برمجية مكتوبة بلغة رست وبالتالي تمثل وحدة tests وحدة اعتيادية، وكما ناقشنا سابقًا يمكن العناصر في الوحدات الابن استخدام العناصر الموجودة في الوحدات الأب، وفي هذا الاختبار نُضيف كل عناصر الوحدات الأب الخاصة بالوحدة test إلى النطاق بكتابة use super::*‎، بحيث يمكننا استدعاء internal_adder فيما بعد. إذا لم تكن مقتنعًا بأن الدوال الخاصة يجب أن تُختَبر فلن تجبرك رست على ذلك. اختبارات التكامل Integration Tests اختبارات التكامل Integration Tests في رست خارجية external كليًا بالنسبة لمكتبتك، وتستخدم هذه الاختبارات مكتبتك بالطريقة ذاتها لأي شيفرة برمجية، مما يعني أنها يمكن أن تستدعي دوال تشكل جزءًا من الواجهة البرمجية العامة Public API للمكتبة. الهدف من هذا النوع من الاختبارات هو اختبار ما إذا كانت أجزاء من مكتبتك تعمل مع بعضها بعضًا بصورةٍ صحيحة، إذ أن بعض الأجزاء من الشيفرة البرمجية قد تعمل بصورةٍ صحيحة لوحدها ولكن تواجه بعض المشاكل عند تكاملها مع أجزاء أخرى، لذا يُغطّي هذا النوع من الاختبارات الشيفرة البرمجية المتكاملة. نحتاج لإنشاء اختبارات التكامل أولًا إلى مجلد tests. مجلد tests نُنشئ مجلد "tests" في المستوى الأعلى لمجلد مشروعنا بجانب "src"، إذ يتعرّف كارجو على المجلد والاختبارات التي بداخله، ويمكننا إنشاء ملفات اختبار قدر ما شئنا، وسيصرّف كارجو كل ملف اختبار بدوره بكونه وحدة مصرّفة crate منفصلة. لننشئ اختبار تكامل باستخدام الشيفرة البرمجية الموجودة في الشيفرة 12 الموجودة في الملف src/lib.rs، نبدأ أولًا بإنشاء مجلد tests ونُنشئ ملفًا جديدًا نسميه tests/integration_test.rs. يجب أن يبدو هيكل المجلد كما يلي: adder ├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── tests └── integration_test.rs أدخل الشيفرة البرمجية في الشيفرة 13 إلى الملف tests/integration_test.rs: اسم الملف: tests/integration_test.rs use adder; #[test] fn it_adds_two() { assert_eq!(4, adder::add_two(2)); } [الشيفرة 13: اختبار تكامل لدالة في الوحدة المصرَّفة adder] يمثل كل ملف في المجلد "tests" وحدة مصرفة منعزلة، لذا يجب علينا إضافة مكتبتنا إلى نطاق وحدة الاختبار المصرَّفة، ونُضيف لهذا السبب use adder في بداية الشيفرة البرمجية وهو ما لم نحتاجه سابقًا عند استخدامنا لاختبارات الوحدة. ليس علينا توصّف الشيفرة البرمجية في tests/integration_test.rs باستخدام ‎#[cfg(test)]‎، إذ أنّ كارجو تُعامل المجلد "tests" على نحوٍ خاص وتُصرِّف جميع الملفات في هذا المجلد عند تنفيذ الأمر cargo test. ننفّذ cargo test فنحصل على التالي: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 1.31s Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6) running 1 test test tests::internal ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6) running 1 test test it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s تتضمن أقسام الخرج الثلاثة اختبارات الوحدة والتكامل والتوثيق، لاحظ أنه إذا فشل أي اختبار في قسم ما، لن يُنفَّذ القسم التالي. على سبيل المثال، إذا فشل اختبار وحدة فهذا يعني أنه لن يكون هناك أي خرج لاختبار التكامل واختبار التوثيق لأن هذه الاختبارت ستُنفَّذ فقط في حال نجاح جميع اختبارات الوحدة. القسم الأول هو لاختبارات الوحدة وهو مشابه لما رأيناه سابقًا، إذ يُخصّص كل سطر لاختبار وحدة ما (هناك اختبار واحد يسمى internal أضفناه سابقًا في الشيفرة 12) بالإضافة إلى سطر الملخص لنتائج اختبارات الوحدة. يبدأ قسم اختبارات التكامل بالسطر Running tests/integration_test.rs، ومن ثم نلاحظ سطرًا لكل دالة اختبار في اختبار التكامل ذلك مع سطر ملخص لنتائج اختبار التكامل قبل بدء القسم Doc-tests adder. يحتوي كل اختبار تكامل على قسمه الخاص، لذا سنحصل على المزيد من الأقسام إن أضفنا مزيدًا من الملفات في المجلد "tests". يمكننا تنفيذ دالة اختبار معيّنة بتحديد اسم دالة الاختبار مثل وسيط للأمر cargo test، ولتنفيذ جميع الاختبارات في ملف اختبار تكامل معيّن نستخدم الوسيط ‎--test في الأمر cargo test متبوعًا باسم الملف كما يلي: $ cargo test --test integration_test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.64s Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298) running 1 test test it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ينفّذ هذا الأمر الاختبارات الموجودة في ملف "tests/integration_test.rs" فقط. الوحدات الجزئية في اختبارات التكامل قد تحتاج إنشاء المزيد من الملفات في المجلد "tests" لمساعدتك في تنظيم اختبارات التكامل في حال إضافتك للمزيد منها، على سبيل المثال يمكنك تجميع دوال الاختبار بناءً على الخاصية التي تفحصها، وكما ذكرنا سابقًا: يُصرَّف كل ملف في المجلد "tests" بمفرده على أنه وحدة مصرّفة، وهو أمر مفيد لإنشاء نطاقات متفرقة عن بعضها بعضًا لمحاكاة الطريقة التي يستخدم فيها المستخدمون وحدتك المصرفة، إلا أن هذا يعني أن الملفات في مجلد "tests" لن تشارك السلوك ذاته الخاص بالملفات في المجلد "src" كما تعلمت سابقًا بخصوص فصل الشيفرة البرمجية إلى وحدات وملفات. يُلاحظ السلوك بملفات المجلد "tests" بوضوح عندما يكون لديك مجموعةً من الدوال المساعدة تريد استخدامها في ملفات اختبار تكامل مختلفة وتحاول أن تتبع الخطوات الخاصة بفصل الوحدات إلى ملفات مختلفة كما ناقشنا سابقًا لاستخلاصها إلى وحدة مشتركة. على سبيل المثال، إذا أنشأنا "tests/common.rs" ووضعنا دالةً اسمها setup في الملف، يمكننا إضافة شيفرة برمجية إلى setup لاستدعائها من دوال اختبار مختلفة في ملفات اختبار متعددة. اسم الملف: tests/common.rs pub fn setup() { // شيفرة ضبط برمجية مخصصة لاختبارات مكتبتك } عند تشغيل الاختبارات مجددًا سنرى قسمًا جديدًا في خرج الاختبار للملف "common.rs"، على الرغم من أننا لا نستدعي الدالة setup من أي مكان كما أن هذا الملف لا يحتوي على أي دوال اختبار: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.89s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::internal ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/common.rs (target/debug/deps/common-92948b65e88960b4) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4) running 1 test test it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s العثور على common في نتائج الاختبار مع running 0 tests ليس ما أردنا رؤيته، إذا أننا أردنا مشاركة جزء من شيفرة برمجية مع ملفات اختبار التكامل الأخرى. لتجنب الحصول على common في خرج الاختبار، نُنشئ "tests/common/mod.rs" بدلًا من "tests/common.rs"، بحيث يبدو هيكل مجلد المشروع كما يلي: ├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── tests ├── common │ └── mod.rs └── integration_test.rs هذا هو اصطلاح التسمية القديم في رست وقد ذكرناه سابقًا في فصل الحزم والوحدات، إذ تخبر تسمية الملف بهذا الاسم رست بعدم التعامل مع الوحدة common على أنها ملف اختبار تكامل، وبالتالي لن يظهر هذا القسم في خرج الاختبار بعد أن ننقل شيفرة الدالة البرمجية setup إلى "tests/common/mod.rs" ونحذف الملف "tests/common.rs". لا تُصرَّف الملفات الموجودة في المجلدات الفرعية في المجلد tests مثل وحدات مصرّفة متفرقة أو تحتوي على أقسام متفرقة في خرج الاختبار. يمكننا استخدام أي من ملفات اختبار التكامل مثل وحدة بعد إنشاء الملف "tests/common/mod.rs"، إليك مثالًا على استدعاء الدالة setup من الاختبار it_adds_two في "tests/integration_test.rs": اسم الملف: tests/integration_tests.rs use adder; mod common; #[test] fn it_adds_two() { common::setup(); assert_eq!(4, adder::add_two(2)); } لاحظ أن التصريح mod common;‎ مماثلٌ لتصريح الوحدة التي شرحنا عنها سابقًا في الشيفرة 21 فصل المسارات paths والنطاق الخاص بها في لغة رست Rust. يمكننا بعد ذلك استدعاء الدالة common::setup()‎ من دالة الاختبار. اختبارات التكامل للوحدات الثنائية المصرفة لا يمكننا إنشاء اختبارات تكامل في المجلد "tests" وإضافة الدوال المعرفة في الملف "src/main.rs" إلى النطاق باستخدام تعليمة use إذا كان مشروعنا يمثّل وحدة ثنائية مصرفة binary crate تحتوي على ملف "src/main.rs" فقط ولا تحتوي على ملف "src/lib.rs"، إذ أن وحدات المكتبة المصرفة وحدها قادرة على كشف الدوال التي يمكن للوحدات المصرّفة الأخرى استخدامها؛ لأن الهدف من الوحدات الثنائية المصرفة هو تنفيذها بصورةٍ مستقلة. هذا واحدٌ من الأسباب لكون مشاريع رست التي تدعم ملفات ثنائية تأتي بملف "src/main.rs"، الذي يستدعي المنطق البرمجي الموجود في الملف src/lib.rs. يمكن لاختبارات التكامل باستخدام هذا التنظيم بأن تختبر صندوق المكتبة المصرف باستخدام use لجعل الدوال المهمة متاحة، فإذا عملت الوظيفة الأساسية، عنى ذلك أن الأجزاء الصغيرة من الشيفرة البرمجية في الملف src/main.rs ستعمل أيضًا ويجب اختبار هذه الأجزاء الصغيرة. ترجمة -وبتصرف- لقسم من الفصل Writing Automated Tests من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج المقال السابق: التحكم بتنفيذ الاختبارات في لغة رست Rust المسارات paths وشجرة الوحدة module tree في رست Rust الاختيار ما بين الماكرو panic!‎ والنوع Result للتعامل مع الأخطاء في لغة Rust
  20. يصرّف cargo test شيفرتك البرمجية بالطريفة نفسها التي يصرّف فيها الأمر cargo run شيفرتك البرمجية ويشغّلها، إلا أن cargo test يصرّف الشيفرة البرمجية في نمط الاختبار ويشغّل ملف الاختبار الثنائي. السلوك الافتراضي للملف الثنائي الناتج عن cargo test هو تشغيل جميع الاختبارات على التوازي والحصول على الخرج خلال تشغيل الاختبار ومنع الخرج من العرض مما يجعل من الخرج المتعلق بنتائج الاختبار أكثر وضوحًا، إلا أنه يمكنك كتابة خيارات في سطر الأوامر للتغيير من هذا السلوك الافتراضي. تنتمي بعض الخيارات في سطر الأوامر إلى cargo test بينما ينتمي بعضها لملف الاختبار الثنائي الناتج، وللفصل بين النوعين من الوسطاء نضع الوسطاء الخاصة بالأمر cargo test متبوعةً بالفاصل -- ومن ثم الوسطاء الخاصة بملف الاختبار الثنائي. يعرض تنفيذ الأمر cargo test --help الخيارات الممكن استخدامها مع cargo test، بينما يعرض تنفيذ cargo test -- --help الخيارات التي يمكنك استخدامها بعد الفاصل. تشغيل الاختبارات على نحو متعاقب أو على التوازي تُنفّذ الاختبارات على التوازي افتراضيًا عند تشغيل عدة اختبارات باستخدام خيوط threads، مما يعني أن تنفيذها سيكون سريعًا وستحصل على نتيجتك بصورةٍ أسرع، وبما أن الاختبارات تُنفّذ في الوقت ذاته فهذا يعني أنها يجب ألا تعتمد على بعضها بعضًا أو تحتوي على حالة مشتركة بما فيه بيئة مشتركة مثل مسار العمل الحالي current working directory أو متغيرات البيئة environment variables. على سبيل المثال لنقل أن اختباراتك تنفذ شيفرة برمجية تُنشئ ملفًا على قرص باسم test-output.txt وتكتب بعض البيانات على هذا الملف، يقرأ عندها كل اختبار البيانات في ذلك الملف ويتأكد أن الملف يحتوي على قيمة معينة وهي قيمة مختلفة بحسب كل اختبار، ولأن الاختبارات تُشغّل في الوقت ذاته فهذا يعني أن أحد الاختبارات قد يكتب على محتويات الملف في وقت تنفيذ اختبار آخر وقراءته للملف وسيفشل عندها الاختبار الثاني ليس لعدم صحة الشيفرة البرمجية بل لأن الاختبارات أثرت على بعضها بعضًا عند تشغيلها على التوازي. يكمن أحد الحلول هنا بالتأكد أن كل اختبار يكتب إلى ملف مختلف، وهناك حلّ آخر يتمثل بتشغيل الاختبارات على التتالي كلّ حسب دوره. إذا لم ترد تشغيل الاختبارات على التوازي، أو أردت تحكمًا أكبر على أرقام الخيوط التي تريد استخدامها فيمكنك عندئذ استخدام الراية ‎--test-threads متبوعةً بعدد الخيوط التي تريد استخدامها مع الاختبار الثنائي. ألقِ نظرةً على المثال التالي: $ cargo test -- --test-threads=1 نضبط عدد خيوط الاختبار إلى "1"، وهذا يجعل البرنامج يعرف أننا لا نريد تشغيل الاختبارات على التوازي، إذ يستغرق تشغيل الاختبارات باستخدام خيط واحد وقتًا أكبر من تشغيلها على التوازي، إلا أن الاختبارات لن تتداخل في عمل بعضها بعضًا إذا تشاركت في حالة ما. عرض خرج الدالة تلتقط مكتبة اختبار رست تلقائيًا كل شيء يُطبع إلى الخرج القياسي إذا نجح الاختبار، على سبيل المثال إذا استدعينا println!‎ في اختبار ما ونجح هذا الاختبار فلن نرى خرج println!‎ في الطرفية بل سنرى فقط السطر الذي يشير إلى نجاح الاختبار، بينما سنرى ما طُبع إلى الخرج القياسي إذا فشل الاختبار مصحوبًا مع رسالة الفشل. تحتوي الشيفرة 10 على مثال بسيط يحتوي على دالة تطبع قيمة معاملها وتُعيد 10 إضافةً إلى اختبار ينجح وآخر يفشل. اسم الملف: src/lib.rs fn prints_and_returns_10(a: i32) -> i32 { println!("I got the value {}", a); 10 } #[cfg(test)] mod tests { use super::*; #[test] fn this_test_will_pass() { let value = prints_and_returns_10(4); assert_eq!(10, value); } #[test] fn this_test_will_fail() { let value = prints_and_returns_10(8); assert_eq!(5, value); } } [الشيفرة 10: اختبارات للدالة التي تستدعي println!‎] نحصل على الخرج التالي عند تشغيل هذه الاختبارات باستخدام cargo test: $ cargo test Compiling silly-function v0.1.0 (file:///projects/silly-function) Finished test [unoptimized + debuginfo] target(s) in 0.58s Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166) running 2 tests test tests::this_test_will_fail ... FAILED test tests::this_test_will_pass ... ok failures: ---- tests::this_test_will_fail stdout ---- I got the value 8 thread 'main' panicked at 'assertion failed: `(left == right)` left: `5`, right: `10`', src/lib.rs:19:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::this_test_will_fail test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib' لاحظ أن الخرج قد التُقط ولا يوجد فيه I got the value 4 وهو ما نطبعه عند تشغيل الاختبار الذي سينجح، بينما يظهر الخرج I got the value 8 من الاختبار الذي فشل في قسم خرج ملخص الاختبار الذي يوضح أيضًا سبب فشل الاختبار. يمكننا اخبار رست بعرض خرج الاختبارات الناجحة باستخدام ‎--show-output إذا أردنا رؤية القيم المطبوعة للاختبارات الناجحة أيضًا. $ cargo test -- --show-output نحصل على الخرج التالي عند تشغيل الاختبارات الموجودة في الشيفرة 10 مجددًا باستخدام الراية ‎--show-output: $ cargo test -- --show-output Compiling silly-function v0.1.0 (file:///projects/silly-function) Finished test [unoptimized + debuginfo] target(s) in 0.60s Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166) running 2 tests test tests::this_test_will_fail ... FAILED test tests::this_test_will_pass ... ok successes: ---- tests::this_test_will_pass stdout ---- I got the value 4 successes: tests::this_test_will_pass failures: ---- tests::this_test_will_fail stdout ---- I got the value 8 thread 'main' panicked at 'assertion failed: `(left == right)` left: `5`, right: `10`', src/lib.rs:19:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::this_test_will_fail test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib' تشغيل مجموعة من الاختبارات باستخدام اسم قد يستغرق تشغيل كافة الاختبارات في بعض الأحيان وقتًا طويلًا، وقد تريد تشغيل مجموعة من الاختبارات مرتبطة فقط بجزئية معينة ضمن شيفرتك البرمجية، ويمكنك اختيار الاختبارات التي تريد تنفيذها بتمرير اسم الاختبار أو أسماء الاختبارات مثل وسطاء إلى الأمر cargo test. لتوضيح كيفية تشغيل مجموعة من الاختبارات نُنشئ أولًا ثلاث اختبارات للدالة add_two كما هو موضح في الشيفرة 11 ونختار أي الاختبارات التي نريد تشغيلها. اسم الملف: src/lib.rs pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn add_two_and_two() { assert_eq!(4, add_two(2)); } #[test] fn add_three_and_two() { assert_eq!(5, add_two(3)); } #[test] fn one_hundred() { assert_eq!(102, add_two(100)); } } [الشيفرة 11: ثلاثة اختبارات مع ثلاثة أسماء مختلفة] ستُنفّذ جميع الاختبارات على التوازي إذا شغّلنا الاختبارات دون تمرير أي وسطاء كما فعلنا سابقًا: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.62s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 3 tests test tests::add_three_and_two ... ok test tests::add_two_and_two ... ok test tests::one_hundred ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s تشغيل الاختبارات بصورة فردية يمكننا تمرير اسم أي دالة اختبار للأمر cargo test لتنفيذ الاختبار بصورةٍ فردية: $ cargo test one_hundred Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.69s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::one_hundred ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s جرى تشغيل الاختبار بالاسم one_hundred فقط، إذ لم يطابق الاختباران الآخران هذا الاسم. يسمح لنا الخرج بمعرفة أن هناك المزيد من الاختبارات التي لم ننفذها بعرض 2 filtered out في النهاية. لا يمكننا تحديد أسماء اختبارات متعددة بهذه الطريقة، إذ تُستخدم القيمة الأولى المُعطاة للأمر cargo test فقط، إلا أن هناك وسيلة أخرى لتنفيذ عدة اختبارات. تنفيذ عدة اختبارات عن طريق الترشيح يمكننا تحديد جزء من اسم اختبار بحيث يُنفّذ أي اختبار يطابق اسمه هذه القيمة. على سبيل المثال، يمكننا تنفيذ اختبارين من الاختبارات الثلاثة السابقة عن طريق add وذلك بكتابة الأمر cargo test add: $ cargo test add Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 2 tests test tests::add_three_and_two ... ok test tests::add_two_and_two ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s يشغّل هذا الأمر جميع الاختبارات التي تحتوي على add في اسمها، ويستثني هنا الاختبار one_hundred، كما يجب ملاحظة أن الوحدة التي تحتوي على الاختبار بداخلها تصبح جزءًا من اسم الاختبار، أي يمكننا تشغيل جميع الاختبارات الموجودة في وحدة معينة عن طريق استخدام اسمها. تجاهل بعض الاختبارات إلا في حال طلبها قد يكون لدينا في بعض الأحيان اختبارات معينة تستغرق وقتًا طويلًا لتنفيذها وقد ترغب باستثنائها من التشغيل عند كتابة الأمر cargo test. يمكنك هنا استخدام توصيف الاختبارات التي تستغرق وقتًا طويلًا باستخدام السمة ignore لاستثنائها بدلًا من كتابة جميع الاختبارات التي تريد تشغيلها مثل وسطاء باستثناء تلك الاختبارات، كما هو موضح هنا: اسم الملف: src/lib.rs #[test] fn it_works() { assert_eq!(2 + 2, 4); } #[test] #[ignore] fn expensive_test() { // شيفرة برمجية تستغرق عدة ساعات للتنفيذ } نضيف ‎#[ignore]‎ بعد سطر ‎#[test]‎ ضمن الاختبار الذي نريد استثناءه، والآن عند تشغيل الاختبارات يُنفّذ الاختبار it_works دون expensive_test: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.60s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 2 tests test expensive_test ... ignored test it_works ... ok test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s تُدرج الدالة expensive_test تحت ignored، وإذا أردنا تشغيل الاختبارات التي تجاهلناها فقط نكتب cargo test -- --ignored: $ cargo test -- --ignored Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test expensive_test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s نتأكد من خلال التحكم بالاختبارات التي تُنفَّذ من أن نتائج cargo test ستكون سريعة، يمكنك تنفيذ cargo test -- --ignored عندما تكون في حالة تريد فيها التحقق من الاختبارات التي تندرج تحت ignored ولديك الوقت لانتظار النتائج، بينما تستطيع تنفيذ الأمر التالي إذا أردت تشغيل جميع الاختبارات المتجاهلة وغير المتجاهلة دفعةً واحدة. cargo test -- --include-ignored ترجمة -وبتصرف- لقسم من الفصل Writing Automated Tests من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: تنظيم الاختبارات Tests في لغة رست Rust المقال السابق: كتابة الاختبارات في لغة رست Rust كيفية كتابة الدوال Functions والتعليقات Comments في لغة راست Rust
  21. الاختبارات Tests هي مجموعة من دوال رست تتأكد من أن الشيفرة البرمجية الأساسية تعمل كما هو مطلوب منها، ويؤدي متن دوال الاختبار عادةً هذه العمليات الثلاث: تجهيز أي بيانات أو حالة ضرورية. تنفيذ الشيفرة البرمجية التي تريد اختبارها. التأكد من أن النتائج وفق المتوقع. لننظر إلى المزايا التي توفرها رست لكتابة الاختبارات التي تؤدي العمليات الثلاث السابقة، ويتضمن ذلك السمة test وبعض الماكرو والسمة should_panic. بنية دالة الاختبار ‎ تُوصَّف دالة الاختبار في رست باستخدام السمة test؛ والسمات attributes هي بيانات وصفية metadata تصف أجزاءً من شيفرة رست البرمجية، ومثال على هذه السمات هي السمة derive التي استخدمناها مع الهياكل سابقًا. لتغيير دالة عادية إلى دالة اختبار نُضيف ‎#[test]‎ قبل السطر الذي نكتب فيه fn، إذ تبني رست ملف تنفيذ اختبار ثنائي عند تنفيذ الاختبارات باستخدام الأمر cargo test، وتختبر الدوال المُشار إليها بأنها دوال اختبار وتعرض لك تقريرًا يوضح أي الدوال التي فشلت وأيها التي نجحت. تُولَّد وحدة اختبار test module مع دالة اختبار تلقائيًا عندما نُنشئ مشروع مكتبة جديدة باستخدام كارجو Cargo، وتمنحنا هذه الوحدة قالبًا لكتابة الاختبارات التي نريد، بحيث لا يتوجب عليك النظر إلى هيكل الاختبار وطريقة كتابته كل مرة تُنشئ فيها مشروعًا جديدًا، ويمكنك إضافة دوال اختبار ووحدات اختبار إضافية قدر ما تشاء. سننظر سويًا إلى بعض جوانب عمل الاختبارات بتجربة قالب الاختبار قبل اختبار أي شيفرة برمجية فعليًا، ثم سنكتب اختبارات واقعية تستدعي شيفرةً برمجيةً كتبناها لاحقًا وتتأكد من صحة سلوكها. ننشئ مشروع مكتبة جديدة نسميه adder يضيف رقمين إلى بعضهما: $ cargo new adder --lib Created library `adder` project $ cd adder يجب أن تبدو محتويات الملف src/lib.rs في مكتبة adder كما هو موضح في الشيفرة 1. اسم الملف: src/lib.rs #[cfg(test)] mod tests { #[test] fn it_works() { let result = 2 + 2; assert_eq!(result, 4); } } الشيفرة 1: وحدة الاختبار والدالة المولّدة تلقائيًا باستخدام cargo new لنتجاهل أول سطرين ونركّز على الدالةحاليًا. لاحظ التوصيف ‎#[test]‎: تُشير هذه السمة إلى أن هذه دالة اختبار، ما يعني أن منفّذ الاختبار سيعامل هذه الدالة على أنها اختبار، وقد تحتوي شيفرتنا البرمجية أيضًا على دوال ليست بدوال اختبار في وحدة tests وذلك بهدف مساعدتنا لضبط حالات معينة، أو إجراء عمليات شائعة، لذا نحدّد دومًا فيما إذا كانت الدالة دالة اختبار. يُستخدم متن الدالة في المثال الماكرو assert_eq!‎ للتأكد من أن result تحتوي على القيمة 4 (وهي تحتوي على نتيجة جمع الرقم 2 مع 2). تمثّل عملية التأكد هذه عملية اختبارًا تقليديًا، دعنا ننفذ الاختبار لنرى إذا ما كان سينجح أم لا. ينفِّذ الأمر cargo test جميع الاختبارات الموجودة في المشروع كما هو موضح في الشيفرة 2. $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.57s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s الشيفرة 2: الخرج الناتج عن عملية تنفيذ الاختبارات المولّدة تلقائيًا يُصرّف كارجو الاختبار ويشغّله؛ إذ نجد في أحد السطور running 1 test، ثم سطر يليه يوضح اسم دالة الاختبار المولّدة التي تدعى it_works وأن نتيجة ذلك الاختبار هي ok، وتعني النتيجة النهائية test result: ok.‎ أن جميع الاختبارات نجحت، بينما يشير الجزء 1 passed; 0 failed إلى عدد الاختبارات الناجحة وعدد الاختبارات الفاشلة. من الممكن تجاهل الاختبار بحيث لا يُنفّذ في حالات معينة وسنتكلم عن هذا الأمر لاحقًا، إلا أن ملخص نتيجة الاختبارات يوضح ‎0 ignored لأننا لم نفعل ذلك هنا، كما يمكننا تمرير وسيط إلى الأمر cargo test بحيث ينفذ الاختبارات التي يوافق اسمها السلسلة النصية ويدعى هذا بالترشيح filtering وسنتكلم عن هذا الموضوع لاحقًا. تظهر نهاية الملخص ‎0 filtered out لأننا لم نستخدم الترشيح على الاختبارات التي ستُنفّذ. يشير ‎0 measured إلى الاختبارات المعيارية التي تقيس الأداء، إذ أن الاختبارات المعيارية benchmark tests متاحةٌ فقط في رست الليلية nightly Rust -في وقت كتابة هذه الكلمات- ويمكنك النظر إلى التوثيق المتعلق بالاختبارات المعيارية لتعلُّم المزيد. يبدأ الجزء التالي من خرج الاختبار بالجملة Doc-tests adder وهو نتيجة لأي من اختبارات التوثيق، إلا أنه لا توجد لدينا أي اختبارات توثيق حاليًا، لكن يمكن لرست تصريف أي مثال شيفرة برمجية موجودة في توثيق واجهتنا البرمجية API، وتساعدنا هذه الميزة بالمحافظة على التوثيق وشيفرتنا البرمجية على نحوٍ متوافق. سنناقش كيفية كتابة اختبارات التوثيق لاحقًا، وسنتجاهل قسم الخرج Doc-tests حاليًا. لنبدأ بتخصيص الاختبار ليوافق حاجتنا؛ إذ سنغيّر أولًا اسم الدالة it_works إلى اسم مختلف مثل exploration كما يلي: اسم الملف: src/lib.rs #[cfg(test)] mod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); } } نشغِّل cargo test مجددًا. يعرض لنا الخرج الآن exploration بدلًا من it_works: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.59s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::exploration ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s نضيف الآن اختبارًا جديدًا، إلا أننا سنجعل هذا الاختبار يفشل عمدًا، إذ تفشل الاختبارات عندما يهلع panic شيءٌ ما داخل دالة الاختبار. يُجرى كل اختبار ضمن خيط thread جديد وعندما يرى الخيط الرئيس أن خيط الاختبار قد "انتهى" يُعَلَّم الاختبار بأنه فشل. تكلمنا سابقًا عن طرق أبسط لهلع الشيفرة البرمجية باستخدام الماكرو panic!‎. الآن، نُدخل الاختبار الجديد مثل دالة تسمى another إلى الملف src/lib.rs كما هو موضح في الشيفرة 3. اسم الملف: src/lib.rs #[cfg(test)] mod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); } #[test] fn another() { panic!("Make this test fail"); } } الشيفرة 3: إضافة اختبار جديد يفشل لأننا نستدعي الماكرو panic!‎ شغِّل الاختبارات مجددًا باستخدام cargo test، يجب أن يكون الخرج مشابهًا لما هو موجود في الشيفرة 4، وهو يوضّح أن اختبار exploration نجح بينما فشل another. $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.72s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 2 tests test tests::another ... FAILED test tests::exploration ... ok failures: ---- tests::another stdout ---- thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::another test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib' الشيفرة 4: نتائج الاختبارات، إذ نجح اختبار وفشل آخر يعرض السطر test tests::another النتيجة FAILED بدلًا من ok، ويظهر لنا قسمين جديدين بين النتائج الفردية والملخص: إذ يعرض الأول السبب لفشل كل من الاختبارات بالتفصيل، وفي هذه الحالة نحصل على التفاصيل الخاصة بفشل another وهي أن panicked at 'Make this test fail'‎ في السطر 10 ضمن الملف src/lib.rs؛ بينما يعرض القسم التالي أسماء جميع الاختبارات التي فشلت، وهي معلومة مفيدة في حال وجد لدينا العديد من الاختبارات مع العديد من التفاصيل لكل اختبار فشل. يمكن استخدام اسم الاختبار الذي فشل لتشغيل الاختبار وحده والحصول على معلومات أدق لتنقيح الأخطاء، وسنتكلم عن طرق تشغيل الاختبارات لاحقًا. يعرض سطر الملخص في النهاية نتيجة الاختبارات كاملةً، إذ أن نتيجة الاختبار هي FAILED ووجِد لدينا اختبارٌ نجح وآخر فشل. الآن بعد أن تعرفنا إلى كيفية عرض نتائج الاختبار في حالات مختلفة، ننظر إلى ماكرو مختلفة عن panic!‎ مفيدة في الاختبارات. التحقق من النتائج باستخدام الماكرو assert!‎ تُعد الماكرو assert!‎ الموجودة في المكتبة القياسية مفيدةً عندما تريد التأكد من أن شرطًا ما ضمن الاختبار يُقيَّم إلى true، ونمرّر للماكرو assert!‎ وسيطًا يمكن تقييمه لقيمة بوليانية boolean؛ فإذا كانت القيمة true لا يحدث شيء عندها ونجتاز الاختبار بنجاح؛ وإذا حصلنا على القيمة false فهذا يعني فشل الاختبار، ويستدعي الماكرو assert!‎ عندها الماكرو panic!‎. يساعدنا استخدام الماكرو assert!‎ في التحقق من شيفرتنا البرمجية بالطريقة التي نريدها. استخدمنا في الأمثلة البرمجية سابقًا هيكلًا يدعى Rectangle وتابع can_hold، وتجد المثال مكررًا هنا في الشيفرة 5. دعنا نضع هذه الشيفرة البرمجية في الملف src/lib.rs ومن ثم نكتب بعض الاختبارات باستخدام الماكرو assert!‎. اسم الملف: src/lib.rs #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } الشيفرة 5: استخدام الهيكل Rectangle وتابعه can_hold من مثال سابق يمكن أن يُعيد التابع can_hold قيمةً بوليانية، وهذا يعني أننا نستطيع استخدامه مع الماكرو assert!‎. سنكتب اختبارًا لنتدرّب على التابع can_hold بإنشاء نسخة Rectangle في الشيفرة 6، وتحمل النسخة عرضًا بمقدار 8 وطولًا بمقدار 7، ومن ثم نتأكد من أنها تستطيع حمل hold نسخة Rectangle أخرى بعرض 5 وطول 1. اسم الملف: src/lib.rs #[cfg(test)] mod tests { use super::*; #[test] fn larger_can_hold_smaller() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(larger.can_hold(&smaller)); } } الشيفرة 6: اختبار للتابع can_hold يتحقق فيما إذا كان المستطيل الكبير يتسع مستطيلًا أصغر لاحظ أننا أضفنا سطرًا جديدًا داخل وحدة tests ألا وهو use super::*;‎، إذ تمثّل وحدة test وحدةً اعتيادية تتبع قواعد الظهورالاعتيادية visibility rules التي ناقشناها سابقًا في مقال المسارات paths وشجرة الوحدة module tree في رست Rust، ولأن وحدة tests هي وحدة داخلية inner module، فنحن بحاجة لإضافة الشيفرة البرمجية التي نريد إجراء الاختبار عليها في الوحدة الخارجية outer module إلى نطاق scope الوحدة الداخلية، ونستخدم هنا glob بحيث يكون كل شيء نعرفه في الوحدة الخارجية متاحًا للوحدة tests. سمّينا الاختبار بالاسم larger_can_hold_smaller وأنشأنا نسختين من الهيكل Rectangle ومن ثم استدعينا الماكرو assert!‎ ومرّرنا النتيجة باستدعاء larger.can_hold(&smaller)‎. يجب أن يُعيد هذا التعبير القيمة true إذا اجتاز الاختبار بنجاح، دعنا نرى بأنفسنا. $ cargo test Compiling rectangle v0.1.0 (file:///projects/rectangle) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e) running 1 test test tests::larger_can_hold_smaller ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests rectangle running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s اجتاز الاختبار فعلًا. دعنا نضيف اختبارًا آخر بالتأكد من أن المستطيل الصغير لا يتسع داخل المستطيل الكبير: اسم الملف: src/lib.rs #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } #[cfg(test)] mod tests { use super::*; #[test] fn larger_can_hold_smaller() { // --snip-- let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(larger.can_hold(&smaller)); } #[test] fn smaller_cannot_hold_larger() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(!smaller.can_hold(&larger)); } } لأن النتيجة الصحيحة من الدالة can_hold هي false في هذه الحالة، فنحن بحاجة لنفي النتيجة قبل أن نمررها إلى الماكرو assert!‎ وبالتالي سيجتاز الاختبار إذا أعادت can_hold القيمة false: $ cargo test Compiling rectangle v0.1.0 (file:///projects/rectangle) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e) running 2 tests test tests::larger_can_hold_smaller ... ok test tests::smaller_cannot_hold_larger ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests rectangle running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s اجتزنا اختبارين متتالين، مرحى لنا. دعنا نرى ما الذي سيحدث الآن لنتائج الاختبار إذا أضفنا خطأً برمجيًا عن عمد في شيفرتنا البرمجية؛ نفعل ذلك بالتغيير من كتابة متن التابع can_hold باستبدال إشارة أكبر من إلى إشارة أصغر من عند المقارنة بين عرض كل من المستطيلين: impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width < other.width && self.height > other.height } } نحصل على التالي عند تنفيذ الاختبارات: $ cargo test Compiling rectangle v0.1.0 (file:///projects/rectangle) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e) running 2 tests test tests::larger_can_hold_smaller ... FAILED test tests::smaller_cannot_hold_larger ... ok failures: ---- tests::larger_can_hold_smaller stdout ---- thread 'main' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:28:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::larger_can_hold_smaller test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib' تنبّهت اختباراتنا للخطأ، لأن larger.width هو 8 و smaller.width هو 5، والمقارنة بين العرضَين في can_hold تُعيد النتيجة false إذ أن 8 ليست أقل من 5. اختبار المساواة باستخدام الماكرو assert_eq!‎ و assert_ne!‎ نستخدم المساواة كثيرًا لاختبار البرنامج وذلك بين القيمة التي تعيدها الشيفرة البرمجية عند التنفيذ والقيمة التي تتوقع من الشيفرة البرمجية أن تعيدها، ويمكننا تحقيق ذلك باستخدام الماكرو assert!‎ وتمرير تعبير باستخدام العامل ==، إلا أن هناك طريقة أخرى أكثر شيوعًا موجودة في المكتبة القياسية ألا وهي الماكرو assert_eq!‎ و assert_ne!‎؛ إذ يقارن كلًا من الماكرو المذكورَين سابقًا القيمة للتحقق من المساواة أو عدم المساواة، كما أننا سنحصل على طباعة للقيمتين إذا فشل الاختبار، بينما يدلنا الماكرو assert!‎ فقط على حصوله على القيمة false من التعبير == دون طباعة القيم التي أدت لحصولنا للقيمة false في المقام الأول. نكتب في الشيفرة 11 دالة تدعى add_two تُضيف القيمة "2" إلى معاملها، ثم نفحص هذه الدالة باستخدام الماكرو assert_eq!‎. اسم الملف: src/lib.rs pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn it_adds_two() { assert_eq!(4, add_two(2)); } } الشيفرة 7: اختبار الدالة add_two باستخدام الماكرو assert_eq!‎ لنتحقّق من عمل الدالة: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.58s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s نمرّر القيمة "4" على أنها وسيط إلى الماكرو assert_eq!‎ وهي قيمة تساوي قيمة استدعاء add_two(2)‎. السطر الخاص بالاختبار هو كالآتي: test tests::it_adds_two ... ok وتُشير ok إلى أن نجاح الاختبار. دعنا نضيف خطأً برمجيًا متعمّدًا على شيفرتنا البرمجية لنرى كيف ستكون رسالة فشل الاختبار باستخدام الماكرو assert_eq!‎. لنغيّر من تطبيق add_two لنضيف قيمة "3" بدلًا من "2": pub fn add_two(a: i32) -> i32 { a + 3 } نشغِّل الاختبارات مجددًا: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::it_adds_two ... FAILED failures: ---- tests::it_adds_two stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `4`, right: `5`', src/lib.rs:11:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::it_adds_two test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib' نجح الاختبار بالتعرف على الخطأ، إذ أن الاختبار it_adds_two فشل وتخبرنا رسالة الخطأ أن سبب الفشل هو: assertion failed: (left == right) بالإضافة لتحديد قيمة كل من left و right، وتساعدنا رسالة الخطأ هذه بالبدء بعملية تنقيح الأخطاء، إذ أن قيمة left هي "4" إلا أن right -القيمة التي حصلنا عليها من add_two(2)‎- هي "5". يصبح هذا الأمر مفيدًا جدًا عندما يكون لدينا الكثير من الاختبارات. يُطلق على معامل التأكد من المساواة للتوابع في بعض لغات البرمجة وأطر العمل اسم expected و actual ويكون ترتيب تحديد المعاملات مهمًّا، إلا أنهما يحملان اسم left و right في رست ولا يهمّ ترتيبهما في عند تمريرهما للماكرو، إذ يمكننا كتابة التأكيد assertion في الاختبار بالشكل: assert_eq!(add_two(2), 4)‎ الذي سينتج رسالة الخطأ ذاتها التي حصلنا عليها سابقًا، وهي: assertion failed: (left == right)‎ ينجح الاختبار باستخدام الماكرو assert_ne!‎ إذا كانت القيمتين المُرّرتين له غير متساويتين، ويفشل في حال المساواة. نستفيد من هذا الماكرو عندما لا نعلم ماذا ستكون القيمة الناتجة تحديدًا إلا أننا نعلم أنها لا يجب أن تكون مساويةً لقيمة معينة؛ على سبيل المثال إذا كنا نختبر دالة يتغير دخلها بحسب يوم الأسبوع الذي ننفّذ فيه الاختبار، سيكون الأفضل في هذه الحالة هو التأكد من أن القيمة التي حصلنا عليها من خرج الدالة لا تساوي قيمة الدخل. يُستخدم كل من الماكرو assert_eq!‎ و assert_ne!‎ كلًا من العامل == و =! على الترتيب، وعندما تفشل عملية التأكيد، يطبع الماكرو معاملاته باستخدام تنسيق تنقيح الأخطاء، ما يعني أن القيم التي ستُقارن فيما بينها يجب أن تُطبّق السمتين ‏PartialEq و Debug، وتطبق جميع الأنواع البدائية ومعظم أنواع المكتبة القياسية هاتين السمتين. ستحتاج لتطبيق PartialEq للهياكل والمعددات enums التي تعرّفها بنفسك للتأكد من مساواة النوعين، كما ستحتاج أيضًا لتطبيق Debug لطباعة القيم عندما يفشل التأكيد. كلا من السمتين المذكورتين هي سمات قابلة للاشتقاق derivable كما ذكرنا سابقًا في الشيفرة 12 من فصل استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust، وهذا يعني أننا نستطيع إضافة الشيفرة التالية مباشرةً في تعريف الهيكل أو المعدّد: #[derive(PartialEq, Debug)] إضافة رسائل فشل مخصصة يمكنك إضافة رسائل مخصّصة لطباعتها مع رسالة الخطأ مثل معامل اختياري للماكرو assert!‎ و assert_eq!‎ و assert_ne!‎. تمرّر أي معاملات تُحدد بعد المعاملات المطلوبة للماكرو بدورها إلى الماكرو format!‎ (الذي ناقشناه في مقال تخزين لائحة من القيم باستخدام الأشعة Vectors)، بحيث يمكنك تمرير سلسلة نصية منسّقة تحتوي على {} بمثابة موضع مؤقت والقيم بداخلها. الرسائل المخصصة مفيدة لتوثيق معنى التأكيد؛ إذ سيكون لديك فهم واضح عن المشكلة في الشيفرة البرمجية عندما يفشل الاختبار. على سبيل المثال، دعنا نفترض وجود دالة تحيّي الناس بالاسم ونريد أن نختبر إذا كان الاسم الذي نمرّره إلى الدالة يظهر في الخرج: اسم الملف: src/lib.rs pub fn greeting(name: &str) -> String { format!("Hello {}!", name) } #[cfg(test)] mod tests { use super::*; #[test] fn greeting_contains_name() { let result = greeting("Carol"); assert!(result.contains("Carol")); } } لم يُتّفق على متطلبات هذا البرنامج بعد، ونحن متأكدون أن النص "Hello" في بداية نص التحية سيتغير لاحقًا، لذا قررنا أننا لا نريد أن نعدل النص عند تغيير المتطلبات، وتحققنا بدلًا من ذلك من المساواة بين القيمة المُعادة من الدالة greeting للتأكد من أن الخرج يحتوي على النص الموجود في معامل الدخل. لنضيف خطأ برمجي متعمد على الشيفرة البرمجية بتغيير greeting بحيث لا تحتوي على الاسم لنرى ما ستكون نتيجة فشل الاختبار الافتراضي: pub fn greeting(name: &str) -> String { String::from("Hello!") } تكون نتيجة تشغيل الاختبار على النحو التالي: $ cargo test Compiling greeter v0.1.0 (file:///projects/greeter) Finished test [unoptimized + debuginfo] target(s) in 0.91s Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a) running 1 test test tests::greeting_contains_name ... FAILED failures: ---- tests::greeting_contains_name stdout ---- thread 'main' panicked at 'assertion failed: result.contains(\"Carol\")', src/lib.rs:12:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::greeting_contains_name test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib' تشير النتيجة إلى أن التأكيد فشل في السطر الذي يحتويه، إلا أن رسالةً أكثر إفادة ستحتوي على القيمة الموجودة في دالة greeting. نضيف رسالة فشل مخصصة مؤلفة من سلسلة نصية منسّقة تحتوي على موضع مؤقت يُملأ بالقيمة الفعلية التي نحصل عليها من الدالة greeting: #[test] fn greeting_contains_name() { let result = greeting("Carol"); assert!( result.contains("Carol"), "Greeting did not contain name, value was `{}`", result ); } نحصل عندما نشغّل الاختبار الآن على رسالة خطأ أكثر وضوحًا: $ cargo test Compiling greeter v0.1.0 (file:///projects/greeter) Finished test [unoptimized + debuginfo] target(s) in 0.93s Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a) running 1 test test tests::greeting_contains_name ... FAILED failures: ---- tests::greeting_contains_name stdout ---- thread 'main' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::greeting_contains_name test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib' يمكننا استخدام القيمة التي حصلنا عليها من خرج الاختبار مما سيساعدنا في تنقيح الخطأ الذي تسبب بذلك بدلًا من الفعل الذي توقعناه من الدالة. التحقق من حالات الهلع باستخدام should_panic من المهم بالإضافة للتحقق من القيم المعادة التحقق من أن شيفرتنا البرمجية تتعامل مع حالات الخطأ كما نتوقع، على سبيل المثال تذكر النوع Guess الذي أنشأناه في أمثلة سابقة في مقال الأخطاء والتعامل معها، إذ استخدمت بعض الشيفرات البرمجية Guess واعتمدت على أن نسخ Guess ستحتوي على قيمة تترواح بين 1 و100. يمكننا كتابة اختبار يتسبب بحالة هلع إذا حاولنا إنشاء نسخة من Guess خارج المجال، وذلك عن طريق إضافة سمة should_panic إلى دالة الاختبار، وينجح الاختبار إذا كانت الشيفرة البرمجية داخل الدالة تهلع، وإلا يفشل الاختبار في حالة عدم هلع الدالة. توضح الشيفرة 8 اختبارًا يتأكد أن حالات الخطأ المتعلقة بالدالة Guess::new تحصل عندما نتوقع حصولها. اسم الملف: src/lib.rs pub struct Guess { value: i32, } impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!("Guess value must be between 1 and 100, got {}.", value); } Guess { value } } } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic] fn greater_than_100() { Guess::new(200); } } الشيفرة 8: اختبار أن حالة ما ستتسبب بالهلع panic!‎ نضع السمة ‎#[should_panic]‎ بعد السمة ‎#[test]‎ وقبل دالة الاختبار التي نريد تطبيق السمة عليها.دعنا نلقي نظرةًعلى النتيجة بعد نجاح الاختبار: $ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.58s Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d) running 1 test test tests::greater_than_100 - should panic ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests guessing_game running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s يبدو أن الأمر على ما يرام. لنضيف خطأ برمجي عن عمد إلى شيفرتنا البرمجية بإزالة الشرط الذي يتسبب بهلع الدالة new إذا كانت القيمة أكبر من 100: impl Guess { pub fn new(value: i32) -> Guess { if value < 1 { panic!("Guess value must be between 1 and 100, got {}.", value); } Guess { value } } } يفشل الاختبار في الشيفرة 8 عندما نشغّله: $ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.62s Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d) running 1 test test tests::greater_than_100 - should panic ... FAILED failures: ---- tests::greater_than_100 stdout ---- note: test did not panic as expected failures: tests::greater_than_100 test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib' لا نحصل على رسالة مفيدة جدًافي هذه الحالة، لكن إذا نظرنا إلى دالة الاختبار نرى توصيفها بالتالي: ‎#[should_panic]‎. حالة الفشل التي حصلنا عليها تعني أن الشيفرة البرمجية في دالة الاختبار لم تتسبب بحالة هلع. يمكن أن تصبح الاختبارات التي تستخدم should_panic غير دقيقة، إذ ينجح اختبار should_pass في حال هلع الاختبار لسبب مختلف عمّا كنا متوقعين، ولجعل اختبارات should_panic أكثر دقة، يمكننا إضافة معامل اختياري يدعى expected للسمة should_panic،وسيتأكد عندها الاختبار من أن رسالة الخطأ تحتوي على النص الذي زوّدناه به. على سبيل المثال، لنفترض أن الشيفرة البرمجية الخاصة بالدالة Guess في الشيفرة 9 احتوت على هلع الدالة new برسائل مختلفة بحسب إذا ما كانت القيمة صغيرة أو كبيرة. اسم الملف: src/lib.rs impl Guess { pub fn new(value: i32) -> Guess { if value < 1 { panic!( "Guess value must be greater than or equal to 1, got {}.", value ); } else if value > 100 { panic!( "Guess value must be less than or equal to 100, got {}.", value ); } Guess { value } } } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic(expected = "less than or equal to 100")] fn greater_than_100() { Guess::new(200); } } الشيفرة 9: اختبار panic!‎ برسالة هلع تحتوي على سلسلة نصية جزئية محددة سينجح الاختبار لأن القيمة التي نضعها في معامل expected الخاص بالسمة should_panic هي سلسلة نصية جزئية للرسالة التي تعرضها الدالة Guess::new عند الهلع. يمكننا تحديد كامل رسالة الهلع التي نتوقعها، وستكون في هذه الحالة: Guess value must be less than or equal to 100, got 200.‎ يعتمد ما تختار أن تحدده على رسالة الهلع إذا ما كانت مميزة أو ديناميكية ودقة الاختبار التي تريدها، وتُعد سلسلةً نصيةً جزئيةً لرسالة الهلع كافية في هذه الحالة للتأكد من أن الشيفرة البرمجية في دالة الاختبار تنفّذ حالة else if value > 100. دعنا نضيف خطأ برمجي جديد مجددًا لرؤية ما الذي سيحصل للاختبار should_panic باستخدام رسالة expected وذلك بتبديل محتوى الكتلة if value < 1 مع else if value > 100: if value < 1 { panic!( "Guess value must be less than or equal to 100, got {}.", value ); } else if value > 100 { panic!( "Guess value must be greater than or equal to 1, got {}.", value ); } يفشل الاختبار should_panic هذه المرة عند تشغيله: $ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d) running 1 test test tests::greater_than_100 - should panic ... FAILED failures: ---- tests::greater_than_100 stdout ---- thread 'main' panicked at 'Guess value must be greater than or equal to 1, got 200.', src/lib.rs:13:13 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace note: panic did not contain expected string panic message: `"Guess value must be greater than or equal to 1, got 200."`, expected substring: `"less than or equal to 100"` failures: tests::greater_than_100 test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib' تشير رسالة الخطأ إلى أن هذا الاختبار هلع فعلًا كما توقعنا إلا أن رسالة الهلع لم تتوافق مع السلسلة النصية الجزئية: 'Guess value must be less than or equal to 100' إذ حصلنا على رسالة الهلع التالية في هذه الحالة: Guess value must be greater than or equal to 1, got 200.‎ لنبدأ الآن بالبحث عن الخطأ. استخدام Result‎ في الاختبارات تهلع جميع اختباراتنا لحد هذه اللحظة عند الفشل، إلا أنه يمكننا كتابة اختبارات تستخدم Result<T, E>‎. إليك اختبارًا من الشيفرة 1 إذ أعدنا كتابته باستخدام Result<T, E>‎ ليعيد Err بدلًا من الهلع: #[cfg(test)] mod tests { #[test] fn it_works() -> Result<(), String> { if 2 + 2 == 4 { Ok(()) } else { Err(String::from("two plus two does not equal four")) } } } للدالة it_works الآن النوع المُعاد Result<(), String>‎، ونُعيد Ok(())‎ عندما ينجح الاختبار و Err مع Sring بداخله عندما يفشل وذلك بدلًا من استدعاء الماكرو assert_eq!‎. تمكّنك كتابة الاختبارات بحيث تعيد Result<T, E>‎ من استخدام عامل إشارة الاستفهام في محتوى الاختبار بحيث تكون وسيلةً ملائمةً لكتابة الاختبارات التي يجب أن تفشل إذا أعادت أي عملية داخلها المتغاير Err. لا يمكنك استخدام التوصيف ‎#[should_panic]‎ على الاختبارات التي تستخدم Result<T, E>‎. لا تستخدم عامل إشارة الاستفهام على Result<T, E>‎ للتأكد من أن عملية ما تُعيد المتغاير Err بل استخدم assert!(value.is_err())‎ بدلًا من ذلك. الآن وبعد أن تعلمنا الطرق المختلفة لكتابة الاختبارات، حان الوقت لننظر إلى ما يحدث عندما ننفذ الاختبارات ونكتشف الخيارات المختلفة التي يمكننا استخدامها مع cargo test في المقال التالي. ترجمة -وبتصرف- لقسم من الفصل Writing Automated Tests من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: التحكم بتنفيذ الاختبارات في لغة رست Rust المقال السابق: التحقق من المراجع References باستخدام دورات الحياة Lifetimes في لغة رست تعلم لغة رست Rust: البدايات استراتيجيات اختبارات مشاريع الويب للتوافق مع المتصفحات
  22. تعدّ دورات الحياة نوعًا آخر من الأنواع المعمّمة generic وقد استعملناها سابقًا دون معرفتنا، إذ تتأكد دورات الحياة أن المراجع references صالحة طوال حاجتنا لها بدلًا من التأكد أن لنوع ما سلوك معيّن. أغفلنا عند مناقشتنا للمراجع والاستعارة borrowing سابقًا أن كل مرجع له دورة حياة في رست، وهو نطاق المرجع الذي يبقى فيه صالحًا، وفي معظم الأحيان تكون دورات الحياة ضمنية واستنتاجية كما هو الحال بكون الأنواع استنتاجية، إذ أننا نحدد الأنواع فقط عندما يمكن وجود أكثر من نوع واحد في حالة ما، وبطريقة مشابهة، علينا أن نشير إلى دورات الحياة عندما ترتبط دورة حياة خاصة بمرجع بطرق عدّة مختلفة، إذ تتطلب منا رست تحديد العلاقة بين دورة حياة المعامل المعمّم للتأكد من أن المرجع الفعلي المستخدم وقت التشغيل سيكون صالحًا. مفهوم الإشارة إلى دورات الحياة غير موجود في معظم لغات البرمجة، لذا قد تشعر بأن محتوى هذا المقال غير مألوف بالنسبة لك، على الرغم من أننا لن نتكلم عن دورات الحياة بالتفصيل هنا بل سنتكلم عن الطرق الشائعة التي قد تصادف بها طريقة كتابة دورة حياة بحيث تألف هذا المفهوم. منع المراجع المعلقة dangling references بدورات الحياة هدف دورات الحياة الأساسي هو منع المراجع المعلّقة dangling references إذ تسبب للبرنامج إشارته إلى مرجع بيانات لا يتطابق مع البيانات التي نريدها، ألقِ نظرةً على البرنامج في الشيفرة 16، إذ يحتوي على نطاق خارجي وداخلي. fn main() { let r; { let x = 5; r = &x; } println!("r: {}", r); } الشيفرة 16: محاولة لاستخدام مرجع خرجت قيمته عن النطاق ملاحظة: تصرّح الأمثلة في الشيفرة 16 والشيفرة 17 والشيفرة 23 عن متغيرات دون إعطائها قيم أولية، لذا لا يوجد اسم المتغير في النطاق الخارجي. قد يبدو ذلك للوهلة الأولى تعارضًا مع مبدأ عدم وجود قيم فارغة null values في رست، إلا أننا سنحصل على خطأ عند التصريف إذا حاولنا استخدام متغير قبل منحه قيمة، وهو ما يؤكد عدم سماح رست بوجود قيم فارغة. يصرح النطاق الخارجي عن متغير يدعى 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 | 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` due to previous error لا "يعيش" المتغير x طويلًا، والسبب في ذلك هو أن x سيخرج عن النطاق عند انتهاء النطاق الداخلي في السطر 7، بينما سيبقى r صالحًا في النطاق الخارجي لأن نطاقه أكبر، وعندها نقول أنه "سيعيش" أطول. إذا سمحت رست لهذه الشيفرة البرمجية بالعمل فهذا يعني أن r سيمثل مرجعًا لمكان محرر في الذاكرة deallocated بعد خروج x من النطاق ولن يعمل أي شيء باستخدام r على النحو المطلوب، إذًا كيف تتحقق رست من صلاحية هذه الشيفرة البرمجية؟ باستخدام مدقق الاستعارة borrow checker. مدقق الاستعارة لمصرّف رست مدقق استعارة يقارن بين النطاقات لتحديد أن جميع عمليات الاستعارة صالحة، وتوضح الشيفرة 17 إصدارًا مماثلًا للشيفرة 16 ولكن بتوضيح دورة الحياة لكل من المتغيرات. fn main() { let r; // ---------+-- 'a // | { // | let x = 5; // -+-- 'b | r = &x; // | | } // -+ | // | println!("r: {}", r); // | } // ---------+ الشيفرة 17: دورة حياة rيشار إليها باستخدام ‎'a بينما يشار إلى دورة حياة x باستخدام ‎'b أشرنا هنا إلى دورة حياة r باستخدام ‎'a ودورة حياة x باستخدام ‎'b، وكما ترى، فإن الكتلة ‎'b الداخلية أصغر بكثير من كتلة دورة حياة ‎'a الخارجية. تقارن رست عند وقت التصريف ما بين حجم دورتي الحياة هاتين وتجد أن r لها دورة حياة ‎'a إلا أنها تمثل مرجعًا إلى موقع ذاكرة دورة حياته ‎'b، وبالتالي يُرفَض البرنامج لأن ‎'b أقصر من ‎'a، أي أن الغرض الذي نستخدم المرجع إليه يعيش أقصر من المرجع ذاته. نصلح الشيفرة البرمجية السابقة في الشيفرة 18، إذ لا يوجد لدينا مراجع معلقة بعد الآن، وتُصرَّف الشيفرة البرمجية بنجاح دون أي أخطاء. fn main() { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {}", r); // | | // --+ | } // ----------+ الشيفرة 18: مرجع صالح لأن للبيانات دورة حياة أطول من المرجع دورة حياة x التي تدعى ‎'b أكبر من ‎'a، مما يعني أن r يمكن أن يمثل مرجعًا للمتغير x، لأن رست تعلم أن المرجع في r صالح ما دام x صالح. الآن وبعد أن تعرفت على دورات حياة المراجع وكيف تحلل رست دورات الحياة للتأكد من أن المراجع ستكون دائمًا صالحة، حان وقت التعرف إلى دورات الحياة المعمّمة الخاصة بالمعاملات والقيمة المُعادة في سياق الدوال. دورات الحياة المعممة في الدوال دعنا نكتب دالةً تُعيد أطول شريحة نصية string slice من شريحتين نصيتين، إذ ستأخذ هذه الدالة شريحتين نصيتين وتُعيد شريحةً نصيةً واحدة. اسم الملف: src/main.rs fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {}", result); } الشيفرة 19: دالة main تستدعي الدالة longest لإيجاد أطول شريحة نصية من شريحتين نصيتين يجب أن تطبع الشيفرة 19 بعد تطبيق الدالة longest ما يلي: The longest string is abcd لاحظ أننا نريد أن تأخذ الدالة شرائح النصية وهي مراجع وليست سلاسل نصية لأننا لا نريد للدالة longest أن تأخذ ملكية معاملاتها. عُد للمقال الذي تكلمنا فيه عن شرائح السلاسل النصية مثل معاملات لمعرفة المزيد حول سبب استخدامنا للمعاملات في الشيفرة 19 كما هي. لن تُصرّف الشيفرة البرمجية إذا حاولنا كتابة الدالة longest كما هو موضح في الشيفرة 20. اسم الملف: src/main.rs fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } } الشيفرة 20: تنفيذ الدالة 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` due to previous error تساعدنا رسالة الخطأ في معرفة أن النوع المُعاد يجب أن يكون له معامل بدورة حياة معممة لأن رست لا تعلم إذا كان المرجع المُعاد يمثل مرجعًا إلى x أو y، وفي الحقيقة لا نعلم نحن أيضًا بدورنا لأن كتلة if في متن الدالة يُعيد مرجعًا للمتغير x وكتلة else تُعيد مرجعًا للمتغير y. لا نعلم القيم الثابتة التي ستُمرر لهذه الدالة عندما نعرفها، لذا لا نعلم إذا ما كانت حالة if محققة أو حالة else، كما أننا لا نعرف دورة الحياة الثابتة للمراجع التي ستُمرر للدالة، لذا لا يمكننا النظر إلى النطاق كما فعلنا في الشيفرة 17 والشيفرة 18 للتأكد إذا ما كان المرجع المُعاد صالحًا دومًا، ولا يمكن لمدقق الاستعارة معرفة ذلك أيضًا لأنه لا يعرف أيّ من دورتي الحياة لكل من x و y ستكون مرتبطة بدورة الحياة الخاصة بالقيمة المُعادة؛ ولتصحيح هذا الخطأ نُضيف معاملًا ذا دورة حياة معممة يعرّف العلاقة ما بين المراجع حتى يستطيع مدقق الاستعارة إجراء تحليله. طريقة كتابة دورة الحياة لا تغيّر طريقة كتابة دورة الحياة على طول حياة المراجع، إذ تصف طريقة الكتابة العلاقة ما بين دورات الحياة لعدة مراجع بين بعضها بعضًا دون التأثير على دورات الحياة بذاتها. يمكن أن تقبل الدوال المراجع بأي دورة حياة بتحديد معامل دورة حياة معممة كما تقبل أي نوع عند تخصيص معامل من نوع معمم في بصمتها. طريقة كتابة دورة الحياة غير مألوفة جدًا، إذ يجب أن تبدأ أسماء معاملات دورات الحياة بالفاصلة العليا ' وعادةً ما تكون أسمائها قصيرة ومكتوبة بأحرف قصيرة كما هو الحال مع الأنواع المعممة. يستخدم معظم الناس الاسم ‎'a بمثابة اسم أول دورة حياة، ومن ثم نضع معامل دورة الحياة بعد إشارة & الخاصة بالمرجع باستخدام المسافة للفصل بين طريقة كتابة دورة الحياة ونوع المرجع. إليك بعض الأمثلة على ذلك: مرجع لقيمة من نوع i32 دون معامل دورة حياة، ومرجع لقيمة من نوع i32 بمعامل دورة حياة يدعى ‎'a ومرجع قابل للتعديل mutable لقيمة من نوع i32 بالاسم 'a ذاته. &i32 // مرجع &'a i32 // مرجع مع دورة حياة صريحة &'a mut i32 // مرجع قابل للتعديل مع دورة حياة صريحة لا تعني كتابة دورة الحياة بمفردها بالشكل السابق الكثير، إذ أن الهدف من هذه الطريقة هو إخبار رست بعلاقة المراجع فيما بينها في معاملات دورة الحياة المعممة. دعنا ننظر إلى كيفية تحقيق ذلك في سياق الدالة longest. توصيف دورة الحياة في بصمات الدالة نحتاج للتصريح عن معاملات دورة الحياة المعممة داخل أقواس مثلثة حتى نستطيع استخدام توصيف دورة الحياة في بصمات الدوال، وذلك بين اسم الدالة وقائمة معاملاتها كما فعلنا سابقًا في معاملات النوع المعمم. نريد من بصمة الدالة أن توضح القيود التالية: سيكون المرجع المُعاد صالح طالما أن كلا المعاملين صالحان؛ وهذه هي العلاقة بين دورات حياة المعاملات والقيمة المعادة. سنسمّي دورة حياة بالاسم ‎'a، ثم نُضيفها لكل مرجع كما هو موضح في الشيفرة 21. اسم الملف: src/main.rs fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } الشيفرة 21: تعريف الدالة longest الذي يحدد أن دورة الحياة لجميع المراجع في بصمة الدالة هي ‎'a يجب أن تعمل الشيفرة البرمجية السابقة بنجاح وأن تمنحنا النتيجة المرجوة عند استخدامها ضمن الدالة main كما فعلنا في الشيفرة 19 السابقة. تخبر بصمة الدالة رست بأن الدالة تأخذ معاملين لبعض دورات الحياة ‎'a وكلاهما شريحة نصية يعيشان على الأقل بطول دورة حياة ‎'a، كما تخبر بصمة الدالة رست بأن شريحة السلسلة النصية المُعادة من الدالة ستعيش على الأقل بطول دورة الحياة ‎'a، وهذا يعني عمليًا أن دورة حياة المرجع المُعاد من الدالة longest مماثلة لأقصر دورة حياة من دورات حياة القيم التي استخدمنا مراجعها في وسطاء الدالة، وهذه هي العلاقة التي نريد أن تستخدمها رست عند تحليل هذه الشيفرة البرمجية. تذكر أننا لا نعدّل من دورات حياة القيم الممررّة أو المُعادة عندما نحدد دورة الحياة المعاملات في بصمة الدالة، وإنما نحدد أنه يجب على مدقق الاستعارة أن يرفض أي قيمة لا تتوافق مع القيود المذكورة. لاحظ أن الدالة longest لا تحتاج لمعرفة أيّ من المتغيرين x و y سيعيش لمدة أطول، بل فقط بحاجة لمعرفة أن نطاق ما سيُستبدل بدورة الحياة ‎'a التي ستطابق بصمة الدالة. نكتب توصيف دورات الحياة عند استخدامها مع الدوال في بصمة الدالة وليس في متنها، إذ يصبح توصيف دورة الحياة جزءًا من عقد contract الدالة كما هو الحال بالنسبة للأنواع ضمن بصمة الدالة. احتواء بصمة الدالة على عقد دورة الحياة يعني أن التحليل الذي يجريه مصرف رست سيصبح أبسط، وإذا وُجدت مشكلة بطريقة توصيف الدالة أو طريقة استدعائها يمكن لأخطاء المصرف أن تُشير إلى ذلك الجزء ضمن الشيفرة البرمجية والقيود التي خرقتها بصورةٍ أدقّ. إذا قدّم مصرف رست بعض الاستنتاجات حول العلاقة المقصودة لدورات الحياة، سيكون في هذه الحالة فادرًا على إعلامنا باستخدام الشيفرة الخاصة بنا وفق عدة خطوات لكنه سيكون بعيدًا عن السبب الحقيقي وراء المشكلة. عند تمرير المراجع الثابتة إلى longest تكون دورة الحياة الثابتة المُستبدلة بدورة الحياة ‎'a جزءًا من نطاق x الذي يتداخل مع نطاق y، بمعنى آخر، تحصل دورة الحياة المعممة ‎'a على دورة حياة ثابتة مساوية إلى أصغر دورة حياة (أصغر دورة بين الدورتين الخاصة بالمتغير y والمتغير x). دعنا ننظر إلى نتيجة استخدام توصيف دورة الحياة وكيف يقيّد ذلك من دالة longest بتمرير المراجع التي لها دورات حياة ثابتة مختلفة، وتمثّل الشيفرة 22 مثالًا مباشرًا على ذلك. اسم الملف: src/main.rs 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); } } الشيفرة 22: استخدام الدالة longest مع مراجع لقيم من نوع String تمتلك دورات حياة ثابتة مختلفة تكون القيمة string1 صالحةً في المثال السابق حتى الوصول لنهاية النطاق الخارجي، بينما تبقى string2 صالحة حتى نهاية النطاق الداخلي، وأخيرًا تمثل result مرجعًا لقيمة صالحة حتى نهاية النطاق الخارجية. نفّذ الشيفرة البرمجية السابقة وسترى أن مدقق الاستعارة لن يعترض على الشيفرة البرمجية وستُصرَّف وتطبع ما يلي: The longest string is long string is long دعنا نجرّب مثالًا يوضح أن دورة حياة المرجع في result يجب أن تكون أصغر من دورة حياة كلا الوسيطين؛ إذ سننقل التصريح عن المتغير result خارج النطاق الداخلي مع المحافظة على عملية إسناد قيمة إلى المتغير result داخل النطاق حيث توجد string2، ثم سننقل println!‎ الذي يستخدم result خارج النطاق الداخلي بعد انتهائه. لن تُصرَّف الشيفرة 23 بنجاح. اسم الملف: src/main.rs 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); } الشيفرة 23: محاولة استخدام 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 | 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` due to previous error يوضح الخطأ أنه يجب على result أن يكون صالحًا حتى تُنفَّذ التعليمة println!‎، كما يجب على المتغير string2 أن يكون صالحًا حتى نهاية النطاق الخارجي، وتعلم رست ذلك بسبب توصيفنا لدورات حياة معاملات الدالة والقيم المُعادة باستخدام معامل دورة الحياة ذاته ‎'a. يمكننا النظر إلى هذه الشيفرة البرمجية على أننا بشر ورؤية أن string1 أطول من string2 وبالتالي سيحتوي المتغير result على مرجع للمتغير string1، ولأن string1 لم يخرج من النطاق بعد، فسيبقى مرجع string1 صالحًا حتى تستخدمه تعليمة !println، إلا أن المصرف لا ينظر إلى المرجع بكونه صالحًا في هذه الحالة إذ أننا أخبرنا رست أن دورة حياة المرجع المُعاد بواسطة الدالة longest هو بطول أصغر دورة حياة مرجع مُمرّر لها، وبالتالي لا يسمح مدقق الاستعارة للشيفرة 23 بامتلاك الفرصة للحصول على مرجع غير صالح. جرّب كتابة المزيد من الأمثلة لتجربة الحالات والقيم ودورات حياة المراجع المختلفة المُمرّر إلى الدالة longest ولاحظ كيفية استخدام المرجع المُعاد، وتنبّأ فيما إذا كانت تجربتك ستُصرَّف ويوافق عليها مدقق الاستعارة أم لا قبل أن تحاول تصريفها، ومن ثم جرّب تصريفها لترى إن كنت مصيبًا أم لا. التفكير في سياق دورات الحياة تعتمد الطريقة التي تحدد فيها دورة حياة المعاملات على الغرض من الدالة، فعلى سبيل المثال إذا عدّلت من كتابة الدالة longest لتُعيد دائمًا المعامل الأول بدلًا من المعامل الذي يمثل أطول شريحة نصية فلن نحتاج عندئذ لتحديد دورة حياة المعامل y. تُصرَّف الشيفرة البرمجية التالية بنجاح: اسم الملف: src/main.rs fn longest<'a>(x: &'a str, y: &str) -> &'a str { x } حددنا معامل دورة حياة مُمثّل بالاسم ‎'a للمعامل x والقيمة المُعادة، إلا أننا لم نحدد دورة حياة للمعامل y لأن ليس لدورة حياة y أي علاقة بدورة حياة x أو القيمة المُعادة. يجب أن يطابق معامل دورة حياة القيمة المُعادة دورة حياة أحد من المعاملات عند إعادة مرجع من دالة ما، وإذا لم يشير المرجع المُعاد إلى واحد من المعاملات فيجب أن يشير إلى قيمة أُنشئت داخل الدالة ذاتها، إلا أن هذا المرجع سيكون مرجعًا معلَّقًا، لأن القيمة ستخرج من النطاق في نهاية الدالة. ألقِ نظرةً على المحاولة التالية لتطبيق الدالة longest التي لن تُصرَّف بنجاح: اسم الملف: src/main.rs 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 reference to local variable `result` --> src/main.rs:11:5 | 11 | result.as_str() | ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function For more information about this error, try `rustc --explain E0515`. error: could not compile `chapter10` due to previous error تكمن المشكلة هنا في أن result يخرج من النطاق ويُحرَّر من الذاكرة بنهاية الدالة longest، إلا أننا نحاول أيضًا إعادة مرجع للقيمة result من الدالة في ذات الوقت، ولا يوجد هناك أي وسيلة لتحديد معاملات دورة الحياة بحيث نتخلص من المرجع المُعلَّق ولن تسمح لنا رست بإنشاء مرجع معلّق. الحل الأمثل في هذه الحال هو بجعل القيمة المُعادة نوع بيانات مملوك owned data type بدلًا من استخدام مرجع، بحيث تكون الدالة المُستدعاة حينها مسؤولة عن تحرير القيمة فيما بعد. يتمثّل توصيف دورة الحياة بربط دورات حياة معاملات مختلفة والقيم المُعادة من الدوال، بحيث تحصل رست على معلومات كافية بعد الربط للسماح بعمليات آمنة على الذاكرة ومنع عمليات قد تتسبب بالحصول على مؤشرات معلّقة أو تخرق أمان الذاكرة. توصيف دورة الحياة في تعاريف الهيكل كانت الهياكل التي عرفناها لحد اللحظة تحتوي على أنواع مملوكة، إلا أنه يمكننا تعريف الهياكل بحيث تحتوي على مراجع وفي هذه الحالة علينا توصيف دورة حياة لكل من المراجع في تعريف الهيكل. تحتوي الشيفرة 24 على هيكل يدعى ImportantExcerpt يحتوي على شريحة سلسلة نصية. اسم الملف: src/main.rs struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence, }; } الشيفرة 24: هيكل يحتوي على مرجع، وبذلك يتطلب توصيف دورة الحياة يحتوي الهيكل على حقل يدعى part يخزّن داخله شريحة سلسلة نصية وهي مرجع، وينبغي علينا هنا التصريح عن اسم معامل دورة الحياة المعممة داخل أقواس مثلثة بعد اسم الهيكل كما هو الحال مع الأنواع المعممة وذلك حتى يتسنّى لنا استخدام معامل دورة الحياة في متن تعريف الهيكل، وتعني طريقة الكتابة هذه أنه لا يوجد أي نسخة من ImportantExcerpt تعيش أطول من المرجع الموجود في الحقل part. تُنشئ الدالة main هنا نسخةً من الهيكل ImportantExcerpt بحيث يحتوي على مرجع للجملة الأولى من String والمملوك من قبل المتغير novel، والبيانات في novel موجودةٌ قبل إنشاء نسخة من ImportantExcerpt، بالإضافة إلى ذلك فإن novel لا تخرج من النطاق إلى أن يخرج ImportantExcerpt من النطاق. إذًا، فالمرجع الموجود في نسخة ImportantExcerpt صالح. إخفاء دورة الحياة تعلمنا أنه لكل مرجع ما دورة حياة ويجب أن نحدّد معاملات دورة الحياة للدوال أو للهياكل التي تستخدم المراجع، إلا أننا كتبنا دالةً في السابق (الشيفرة 9) كما هي موضحة في الشيفرة 25، وقد صُرَِفت بنجاح دون استخدام توصيف دورة الحياة. اسم الملف: src/lib.rs 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[..] } الشيفرة 25: دالة عرفناها سابقًا وصرّفت بنجاح دون استخدام توصيف دورة الحياة على الرغم من كون كل من المعاملات والقيمة المعادة مراجع السبب في تصريف الشيفرة السابقة بنجاح هو سبب تاريخي، إذ لن تُصرَّف الشيفرة البرمجية هذه في الإصدارات السابقة من رست (قبل 1.0)، وذلك لحاجة كل مرجع لدورة حياة صريحة. إليك ما ستبدو عليه بصمة الدالة في ذلك الوقت من تطوير اللغة: fn first_word<'a>(s: &'a str) -> &'a str { وجد فريق تطوير رست بعد كتابة الكثير من الشيفرات البرمجية باستخدام اللغة أن معظم مبرمجي رست يُدخلون توصيف دورة الحياة ذاته مرةً بعد الأخرى في حالات معيّنة، وكان يمكن توقع هذه الحالات واتباعها بأنماط للتعرف عليها، وبالتالي برمج المطوّرون هذه الأنماط إلى شيفرة المصرّف البرمجية بحيث يتعرّف عليها مدقق الاستعارة ويستنتج دورات الحياة في هذه الحالات دون الحاجة لكتابة توصيف دورة الحياة مباشرةً. هذه النقطة في تاريخ تطوير رست مهمة لأنه من الممكن ظهور المزيد من الأنماط مستقبلًا وإضافتها إلى المصرف، وبذلك قد لا نحتاج لاستخدام توصيف دورات الحياة مباشرةً في العديد من الحالات. تُدعى هذه الأنماط الموجودة لتحليل المراجع بقواعد إخفاء دورة الحياة lifetime elision rules، إلا أن هذه القواعد ليست للمبرمجين حتى يتبعونها بل هي مجموعة من الحالات التي سينظر إليها المصرّف، إذ لن تحتاج لاستخدام توصيف دورات الحياة مباشرةً إذا كانت شيفرتك البرمجية تندرج ضمن واحدة من هذه الحالات. لا تقدّم قواعد الإخفاء القدرة على الاستنتاج بصورةٍ كاملة، إذ لن يستطيع المصرف تخمين دورات الحياة الخاصة بالمراجع الأخرى إذا طبقت رست هذه القواعد بصورةٍ حتمية ووُجد غموض ما بخصوص أي دورات الحياة تنتمي للمراجع، ففي هذه الحالة يعرض لك المصرف رسالة خطأ بدلًا من التخمين، ويمكنك حينها تصحيح هذا الخطأ عن طريق إضافة توصيفٍ لدورة الحياة. تُدعى دورات الحياة لمعاملات دالة أو تابع بدورات حياة الدخل input lifetimes بينما تُدعى دورات الحياة الخاصة بالقيم المُعادة بدورات حياة الخرج output lifetimes. يستخدم المصرف ثلاث قواعد لمعرفة دورات حياة المراجع عندما لا يوجد هناك توصيف مباشر لها: تُطبَّق القاعدة الأولى على دورات حياة الدخل والثانية والثالثة على دورات حياة الخرج. يتوقف المصرّف ويعطينا خطأ إذا تحقق من القواعد الثلاث ولم يتعرف على كل دورات حياة المراجع، وتنطبق هذه القواعد على تعاريف fn بالإضافة إلى كتل impl. تتمثل القاعدة الأولى بإسناد المصرف معامل دورة حياة لكل معامل يشكّل مرجع، بكلمات أخرى: تحصل دالةً تحتوي على معامل واحد على معامل دورة حياة واحد fn foo<'a>(x: &'a i32)‎، بينما تحصل دالة تحتوي على معاملين على دورتَي حياة منفصلتين foo<'a, 'b>(x: &'a i32, y: &'b i32)‎، وهلمّ جرًا. تنص القاعدة الثانية على وجود معامل دورة حياة دخل واحد فقط، وتُسند دورة الحياة هذه إلى جميع معاملات دورة حياة الخرج كالآتي: fn foo<'a>(x: &'a i32) -> &'a i32 أخيرًا، تنص القاعدة الثالثة على إسناد دورة الحياة الخاصة بـ self لجميع معاملات دورة حياة الخرج، إذا وُجدت عدّة معاملات دورة حياة دخل وكان أحدها ‎&self أو ‎&mut self لأنها تابع. تجعل القاعدة الثالثة من التوابع أسهل قراءةً لأنها تُغنينا عن استخدام الكثير من الرموز في تعريفها. لنفترض أننا المصرّف. دعنا نطبّق هذه القواعد لمعرفة دورات حياة المراجع في بصمة الدالة first_word في الشيفرة 25. تبدأ بصمة الدالة دون أي دورات حياة مرتبطة بالمراجع: 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 التي لا تحتوي على معاملات دورة حياة عندما بدأنا بكتابتها في الشيفرة 20 سابقًا: fn longest(x: &str, y: &str) -> &str { نطبّق القاعدة الأولى: يحصل كل معامل على دورة حياة خاصة به. لدينا الآن في هذه الحالة معاملين بدلًا من واحد، لذا سنحصل على دورتين حياة: fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { يمكنك رؤية أن القاعدة الثانية لا تنطبق على هذه الحالة لوجود أكثر من دورة حياة دخل واحدة، كما أن القاعدة الثالثة لا تنطبق لأن longest دالة وليست تابع، إذًا لا يوجد في معاملاتها self. لم نتوصل إلى دورة حياة النوع المُعادة بعد تطبيق القواعد الثلاث، وهذا هو السبب في حصولنا على خطأ عند محاولة تصريف الشيفرة 20، إذ أن المصرف تحقق من قواعد إخفاء دورة الحياة ولكنه لم يتعرف على جميع دورات حياة المراجع في بصمة الدالة. سننظر إلى دورات الحياة في سياق التوابع بما أن القاعدة الثالثة تنطبق فقط في بصمات التوابع، مما سيكشف لنا السبب في كون توصيف دورات الحياة ضمن التوابع غير مُستخدم معظم الأحيان. توصيف دورة الحياة في تعاريف التابع نستخدم طريقة الكتابة الخاصة بمعاملات الأنواع المعممة ذاتها عند تطبيق التوابع ضمن هياكل تحتوي على دورات حياة، إذ نصرح ونستخدم معاملات دورة الحياة بناءً على ارتباطها بحقول الهيكل أو معاملات التابع والقيم المُعادة، إذ يجب أن يُصرَّح عن أسماء دورات الحياة الخاصة بحقول الهياكل بعد الكلمة المفتاحية impl ومن ثم استخدامها بعد اسم الهيكل لأن دورات الحياة هذه تشكل جزءًا من نوع الهيكل. قد ترتبط المراجع في بصمة التابع داخل الكتلة impl بدورات حياة المراجع الخاصة بحقول الهيكل، وقد تكون مستقلةً عن بعضها الآخر، كما أن قوانين إخفاء دورة الحياة تجعل من توصيف دورات الحياة غير ضروري في بصمات التابع معظم الأحيان. دعنا ننظر إلى بعض الأمثلة باستخدام هيكل يدعى ImportantExcerpt وهو هيكل عرّفناه سابقًا في الشيفرة 24. لنستخدم أولًا تابعًا يدعى level يحتوي على معامل واحد يمثل مرجعًا إلى self ويُعيد قيمة من النوع i32 (أي لا تمثّل مرجعًا): impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } التصريح عن معامل دورة الحياة بعد impl واستخدامه بعد اسم النوع مطلوب، إلا أنه من غير المطلوب توصيف دورة حياة مرجع self بفضل قاعدة إخفاء دورة الحياة الأولى. إليك مثالًا ينطبق عليه قاعدة إخفاء دورة الحياة الثالثة: impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {}", announcement); self.part } } هناك دورتا حياة دخل، لذا يطبق رست القاعدة الأولى ويمنح لكل من ‎&self و announcment دورة حياة خاصة بهما، ومن ثم يحصل النوع المُعادة على دورة الحياة ‎&self لأن إحدى معاملات التابع قيمته ‎&self، وبهذا يجري التعرف على جميع دورات الحياة الموجودة. دورة الحياة الساكنة يجب أن نناقش واحدةً من دورات الحياة المميزة ألا وهي static' وهي تُشير إلى أن المرجع يمكن أن يعيش طوال فترة البرنامج، ولدى جميع السلاسل النصية نوع دورة الحياة الساكنة static'، ويمكننا توصيفه بالشكل التالي: let s: &'static str = "I have a static lifetime."; يُخزّن النص الموجود في السلسلة النصية في ملف البرنامج التنفيذي مباشرةً أي أنه مرئي طوال الوقت، بالتالي فإن دورة حياة جميع السلاسل النصية المجردة literals هي static'. قد تجد اقتراحات لاستخدام دورة الحياة 'static في رسائل الخطأ، إلا أنه يجب عليك أن تفكر فيما إذا كان المرجع بذاته يعيش طيلة دورة حياة البرنامج أم لا إذا أردت اتباع هذا الاقتراح وفيما إذا كنت تريد هذا الشيء حقًا أم لا، وتنتج رسالة الخطأ التي تقترح دورة حياة static' معظم الأحيان من محاولة إنشاء مرجع معلّق أو حالة عدم تطابق ما بين دورات الحياة الموجودة، وفي هذه الحالة فالحل الأمثل هو بحل هذه المشاكل وليس بتحديد دورة الحياة الساكنة static'. معاملات الأنواع المعممة وحدود السمة ودورات الحياة معا دعنا ننظر إلى طريقة تحديد معاملات الأنواع المعممة وحدود السمة ودورات الحياة في دالة واحدة سويًا. 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 من الشيفرة 21 سابقًا التي تُعيد أطول شريحة نصية من شريحتين نصيتين، إلا أننا أضفنا هنا معاملًا جديدًا يدعى ann من نوع معمّم T الذي يُمكن أن يُملأ بأي نوع يطبّق السمة Display كما هو محدد في بنية where، وسيُطبع هذا المعامل الإضافي باستخدام {} وهذا هو السبب في جعل حدود السمة Display ضرورية. نكتب كل من تصاريح معاملات دورة الحياة ‎'a ومعامل النوع المعمم T في القائمة ذاتها داخل الأقواس المثلثة بعد اسم الدالة وذلك لأن دورات الحياة هي نوع من الأنواع المعمّمة. ترجمة -وبتصرف- لقسم من الفصل Generic Types, Traits, and Lifetimes من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: كتابة الاختبارات في لغة رست Rust المقال السابق: السمات Traits في لغة رست Rust مقدمة إلى مفهوم الأنواع المعممة Generic Types في لغة Rust استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust
  23. يمكن أن تعرّف السمة وظيفة نوع محدد ويمكن مشاركتها مع عدّة أنواع، ويمكننا استخدام السمات لتعريف سلوك مشترك بطريقة مجردة، ويمكننا استخدام حدود السمة trait bounds لتحديد أن النوع المعمّم يمكن أن يكون أي نوع يمتلك سلوكًا محددًا. ملاحظة: السمات مشابهة لميزة تدعى الواجهات interfaces في لغات برمجة أخرى، إلا أن هناك بعض الاختلافات. تعريف سمة Trait يتكون سلوك النوع من توابع يمكننا استدعائها على هذا النوع، ونقول أن عدّة أنواع تشارك السلوك ذاته إذا أمكننا استدعاء التوابع ذاتها على جميع هذه الأنواع، ويُعد تعريف السمة طريقةً لجمع بصمات التوابع method signatures لتعريف مجموعة من السلوكيات المهمة لتحقيق غرض ما. على سبيل المثال، دعنا نفترض وجود عدّة هياكل تحمل أنواع وكميات مختلفة من النص، إذ يحمل الهيكل NewsArticle حقلًا لمحتوى إخباري في موقع معين، ويمكن أن تحتوي Tweet على نص طوله 280 محرفًا بالحد الأعظمي، إضافةً إلى البيانات الوصفية metadata التي تشير إلى كون التغريدة جديدة، أو إعادة تغريد retweet، أو رد على تغريدة أخرى. نريد أن نُنشئ وحدة مكتبة مصرَّفة library crate تجمع الأخبار تدعى aggregator، بحيث تعرض ملخصًا للبيانات التي قد تجدها في نسخ NewsArticle أو Tweet، ثمّ سنستدعي الملخص لأي من النسخ باستدعاء التابع summarize. توضح الشيفرة 12 تعريف السمة العامة Summary التي تعبّر عن هذا السلوك. اسم الملف: src/lib.rs pub trait Summary { fn summarize(&self) -> String; } الشيفرة 12: سمة Summary تتألف من السلوك الموجود في التابع summarize نصرّح هنا عن سمة باستخدام الكلمة المفتاحية trait متبوعةً باسم السمة وهي Summary في هذه الحالة، كما نصرّح أيضًا عن السمة بكونها عامة pub بحيث تستخدم الوحدات المصرّفة هذه الوحدة المصرفة كما سنرى في الأمثلة القادمة. نصرّح عن بصمات التابع داخل القوسين المعقوصين curly brackets، إذ تصف البصمات سلوك الأنواع التي تطبق هذه السمة، والتي هي في هذه الحالة fn summarize(&self) -> String. بعد التصريح عن بصمة التابع، يمكننا استخدام الفاصلة المنقوطة بدلًا من الأقواس المعقوصة. ويجب على كل نوع ينفّذ هذه السمة أن يوفّر سلوكه المخصص لمتن التابع. سيفرض المصرّف أن أي نوع له السمة Summary سيكون له تابع باسم summarize مُعرّف بتلك البصمة المُحدَّدة. يمكن أن تحتوي السمة عدّة توابع في متنها، إذ أن بصمات التوابع محتواة في كل سطر على حدة، وينتهي كل سطر بفاصلة منقوطة. تطبيق السمة على نوع الآن وبعد أن عرَّفنا البصمات المطلوبة لتوابع السمة Summary يمكننا تطبيقها على الأنواع الموجودة في مجمّع الوسائط media aggregator. توضح الشيفرة 13 تنفيذًا للسمة Summary في الهيكل NewsArticle الذي يستخدم كل من العنوان والمؤلف والمكان لإنشاء قيمة مُعادة من summarize. نعرّف من أجل الهيكل Tweet الدالة summarize بحيث تحصل على اسم المستخدم متبوعًا بالنص الكامل الموجود في التغريدة وذلك بفرض أن التغريدة محدودة بمقدار 280 محرف. اسم الملف: src/lib.rs pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String, } impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) } } pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } } الشيفرة 13: تطبيق السمة Summary على كل من النوعين NewsArticle و Tweet تطبيق سمة على نوع هي عملية مشابهة لتطبيق توابع اعتيادية، إلا أن الفارق هنا هو أننا نضع اسم السمة التي نريد تطبيقها بعد impl، ثم نستخدم الكلمة المفتاحية for ونحدد اسم النوع الذي نريد تطبيق السمة عليه. نضع داخل كتلة impl بصمات التابع المعرفة في تعريف السمة، وبدلًا من إضافة الفاصلة المنقوطة بعد كل بصمة سنستخدم الأقواس المعقوصة ونملأ داخلها متن التابع مع السلوك المخصص الذي نريد من توابع السمة أن تمتلكه لنوع معين. الآن وبعد أن طبّقنا السمة Summary في وحدة المكتبة المصرفة على NewsArtilce و Tweet، يمكن لمستخدمي الوحدة المصرّفة استدعاء توابع السمة على نسخٍ من NewsArticle و Tweet بالطريقة ذاتها التي نستدعي بها توابع اعتيادية، إلا أن الفارق الوحيد هنا هو أن المستخدم يجب أن يُضيف السمة إلى النطاق scope إضافةً إلى الأنواع. إليك مثالًا عن كيفية استخدام وحدة المكتبة المصرفة aggregator من قِبل وحدة ثنائية مصرّفة binary crate: use aggregator::{Summary, Tweet}; fn main() { let tweet = Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, }; println!("1 new tweet: {}", tweet.summarize()); } تطبع الشيفرة البرمجية السابقة ما يلي: 1 new tweet: horse_ebooks: of course, as you probably already know, people يُمكن أن تضيف الوحدات المصرّفة الأخرى المعتمدة على الوحدة المصرفة aggregator السمة Summary إلى النطاق لتطبيق Summary على أنواعها الخاصة، إلا أن القيد الوحيد هنا الذي يجب ملاحظته هو أنه يمكننا تطبيق السمة على نوع نريده فقط إذا كانت سمة واحدة على الأقل أو نوعًا واحدًا على الأقل محليًا local بالنسبة لوحدتنا المصرّفة؛ إذ يمكننا على سبيل المثال تطبيق سمات المكتبة القياسية مثل Display على نوع مخصص مثل Tweet بمثابة جزء من وظيفة وحدتنا المصرفة aggregator، لأن النوع Tweet هو محلي بالنسبة إلى الوحدة المصرفة aggregator، كما يمكننا أيضًا تطبيق Summary على النوع Vec<T>‎ في الوحدة المصرفة aggregator لأن السمة Summary هي سمة محلية بالنسبة لوحدتنا المصرفة aggregator. في المقابل، لا يمكننا تطبيق سمة خارجية على أنواع خارجية، فعلى سبيل المثال لا يمكننا تطبيق السمة Display على النوع Vec<T>‎ داخل الوحدة المصرفة aggregator، لأن Display و Vec<T>‎ ليستا معرفتين في المكتبة القياسية أو محليةً بالنسبة للوحدة المصرفة aggregator. يُعد هذا القيد جزءًا من خاصية تدعى الترابط المنطقي coherence وبالأخص قاعدة اليتيم orphan rule وتسمى القاعدة بهذا الاسم لأن نوع الأب غير موجود، وتتأكد هذه القاعدة من أن الشيفرة البرمجية الخاصة بالمبرمجين الآخرين لن تتسبب بعطل شيفرتك البرمجية والعكس صحيح، وبدون هذه القاعدة يمكن للوحدتين المصرفتين تطبيق السمة ذاتها على النوع ذاته، وعندها لن تستطيع رست معرفة أي من التنفيذين يجب استخدامه. التنفيذات الافتراضية من المفيد في بعض الأحيان تواجد سلوك افتراضي لبعض التوابع الموجودة في سمة ما أو جميعها بدلًا من طلب كتابة متن لكل التوابع ضمن كل نوع، بحيث يمكننا إعادة الكتابة على السلوك الافتراضي للتابع إذا أردنا تطبيق السمة على نوع معيّن. نحدد في الشيفرة 14 سلسلةً نصيةً افتراضية للتابع summarize ضمن السمة Summary بدلًا من تعريف بصمة التابع كما فعلنا في الشيفرة 12. اسم الملف: src/lib.rs pub trait Summary { fn summarize(&self) -> String { String::from("(Read more...)") } } الشيفرة 14: تعريف سمة Summary بتنفيذ افتراضي خاص بالتابع summarize نحدد كتلة impl فارغة بكتابة impl Summary for NewsArticle {}‎ لاستخدام التنفيذ الافتراضي لتلخيص نسخ NewsArticle. على الرغم من أننا لا نعرف بعد الآن التابع summarize على NewsArticle مباشرةً إلا أننا قدمنا متنًا افتراضيًا وحددنا أن NewsArticle تستخدم السمة Summary، ونتيجةً لذلك يمكننا استدعاء التابع summarize على نسخة من NewsArticle كما يلي: let article = NewsArticle { headline: String::from("Penguins win the Stanley Cup Championship!"), location: String::from("Pittsburgh, PA, USA"), author: String::from("Iceburgh"), content: String::from( "The Pittsburgh Penguins once again are the best \ hockey team in the NHL.", ), }; println!("New article available! {}", article.summarize()); تطبع الشيفرة البرمجية السابقة ما يلي: New article available! (Read more...)‎ لا يتطلب إنشاء تنفيذ افتراضي تعديل أي شيء بخصوص تنفيذ Summary على Tweet في الشيفرة 13، وذلك لأن طريقة الكتابة على التنفيذ الافتراضي مماثلة لصيغة تنفيذ تابع سمة لا يحتوي على تنفيذ افتراضي. يمكن أن تستدعي التنفيذات الافتراضية توابع أخرى في السمة ذاتها حتى لو كانت التوابع الأخرى لا تحتوي على تنفيذ افتراضي، وبذلك يمكن أن تقدم السمة الكثير من المزايا المفيدة باستخدامها لتنفيذٍ محدد في جزء صغير منها، على سبيل المثال يمكننا أن نعرف السمة Summary بحيث تحتوي على تابع summarize_author يحتوي على تنفيذ داخله ومن ثم تابع summarize يحتوي على تنفيذٍ افتراضي يستدعي التابع summarize_author: pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!("(Read more from {}...)", self.summarize_author()) } } لاستخدام هذا الإصدار من Summary علينا أن نعرف summarize_author عند تطبيق السمة على النوع: impl Summary for Tweet { fn summarize_author(&self) -> String { format!("@{}", self.username) } } يمكننا استدعاء summarize على نسخة من هيكل Tweet بعد تعريفنا التابع summarize_author، وعندها سيستدعي التنفيذ الافتراضي للتابع summarize تعريف التابع summarize_author الذي أضفناه، ولأننا كتبنا summarize_author فنحن منحنا للسمة Summary سلوكًا للتابع summarize دون كتابة المزيد من الأسطر البرمجية. let tweet = Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, }; println!("1 new tweet: {}", tweet.summarize()); تطبع الشيفرة البرمجية السابقة ما يلي: 1 new tweet: (Read more from @horse_ebooks...)‎ لاحظ أنه ليس من الممكن استدعاء التنفيذ الافتراضي من تنفيذٍ كتبنا فوقه override لنفس التابع. السمات مثل معاملات الآن، وبعد أن تعلمنا كيفية تعريف وتطبيق السمات، أصبح بإمكاننا النظر إلى كيفية استخدام السمات لتعريف الدوال التي تقبل العديد من الأنواع المختلفة، وسنستخدم هنا السمة Summary التي طبقناها على النوعين NewsArtilce و Tweet في الشيفرة 13 لتعريف الدالة notify التي تستدعي التابع summarize على المعامل item وهو نوع ينفّذ السمة Summary. لتحقيق ذلك علينا أن نكتب صيغة impl Trait بالشكل التالي: pub fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize()); } بدلًا من استخدام نوع ثابت للمعامل item نحدد الكلمة المفتاحية impl ومن ثم اسم السمة، إذ يقبل هذا المعامل أي نوع ينفّذ السمة التي حددناها. يمكننا استدعاء أي تابع في notify على item يحتوي على السمة Summary مثل summarize، إذ يمكننا استدعاء notify وتمرير أي نسخة من NewsArticle أو Tweet. لن تصرَّف الشيفرة البرمجية التي تستدعي الدالة باستخدام نوع آخر مثل String أو i32 وذلك لأن الأنواع هذه لا تنفّذ Summary. صيغة حدود السمة تكون صيغة impl Triat جيدة للاستخدامات البسيطة، إلا أنها طريقة مختصرة عن طريقة أطول تُعرف بحدود السمة trait bound، وتبدو على النحو التالي: pub fn notify<T: Summary>(item: &T) { println!("Breaking news! {}", item.summarize()); } تماثل هذه الكتابة الطويلة الكتابة في القسم السابق إلا أنها أطول، إذ أننا نضع حدود السمة في تصريح معاملات النوع المعمم بعد النقطتين وداخل أقواس مثلثة angle brackets. تُعد صيغة impl Trait مناسبة وتجعل من شيفرتنا البرمجية أبسط في العديد من الحالات البسيطة إلا أن كتابة حدود السمة بشكلها الكامل تسمح لنا بتحديد تفاصيل أدق في بعض الحالات، على سبيل المثال يمكننا كتابة معاملين ينفّذان السمة Summary وكتابة هذا الأمر بصيغة impl Trait، وسيبدو بهذا الشكل: pub fn notify(item1: &impl Summary, item2: &impl Summary) { يُعد استخدام صيغة impl Trait ملائمًا إذا أردنا لهذه الدالة السماح للمعاملين item1 و item2 أن يكونا من نوعين مختلفين (طالما ينفّذ كلاهما Summary). إذا أردنا إجبار المعاملين على استخدام النوع ذاته يجب أن نستخدم حدود السمة على النحو التالي: pub fn notify<T: Summary>(item1: &T, item2: &T) { يقيّد النوع المعمّم T المحدد على أنه نوع لكل من المعاملين item1 و item2 الدالة بأنه يجب عليها قبول القيمتين فقط إذا كان كل من item1 و item2 لهما النوع ذاته. تحديد حدود سمة عديدة باستخدام صيغة + يمكننا تحديد أكثر من حد سمة واحد، لنقل أننا نريد notify أن تستخدم تنسيق طباعة معيّن بالإضافة إلى summarize على item، عندها نحدد في تعريف notify أنه يجب على item أن تنفّذ كلًا من Display و Summary بنفس الوقت، ويمكننا فعل ذلك باستخدام الصيغة +: pub fn notify(item: &(impl Summary + Display)) { الصيغة + صالحة أيضًا مع حدود السمات على الأنواع المعممة: pub fn notify<T: Summary + Display>(item: &T) { يمكن لمتن الدالة notify أن يستدعي summarize مع استخدام {} لتنسيق item وذلك مع وجود حدّين للسمة. حدود سمة أوضح باستخدام بنى where لاستخدام حدود سمة عديدة بعض السلبيات إذ أن كل نوع معمم يحتوي على حد سمة خاص به، لذا من الممكن للدوال التي تحتوي على عدة أنواع معممة مثل معاملات أن تحتوي الكثير من المعلومات بخصوص حدود السمة بين اسم الدالة ولائحة معاملاتها مما يجعل بصمة الدالة صعبة القراءة، ولذلك تحتوي رست على طريقة كتابة بديلة لتحديد حدود السمة داخل بنية where بعد بصمة الدالة، وبالتالي يمكننا استخدام البنية where على النحو التالي: fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug, { بدلًا من كتابة التالي: fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { أصبحت الآن بصمة الدالة أكثر وضوحًا إذ تحتوي على اسم الدالة ولائحة معاملاتها والنوع الذي تُعيده على سطر واحد بصورةٍ مشابهة لدالة لا تحتوي على الكثير من حدود السمة. إعادة الأنواع التي تنفذ السمات يمكننا أيضًا استخدام صيغة impl Triat في مكان الإعادة لإعادة قيمة من نوع ما يطبّق سمة، كما هو موضح هنا: fn returns_summarizable() -> impl Summary { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, } } نستطيع تحديد أن الدالة returns_summarizable تُعيد نوعًا يطبق السمة Summary باستخدام impl Summary على أنه نوع مُعاد دون تسمية النوع الثابت، وفي هذه الحالة تعيد الدالة returns_summarizable القيمة Tweet إلا أنه ليس من الضروري أن تعلم الشيفرة التي تستدعي الدالة بذلك. إمكانية تحديد قيمة مُعادة فقط عن طريق السمة التي تطبقها مفيد جدًا، بالأخص في سياق المغلفات closures والمكررات iterators وهما مفهومان سنتكلم عنهما لاحقًا، إذ تُنشئ المغلفات والمكررات أنواعًا يعرفها المصرف فقط، أو أنواعًا يتطلب تحديدها كتابةً طويلةً إلا أن الصيغة impl Trait تسمح لك بتحديد أن الدالة تُعيد نوعًا ما يطبّق السمة Iterator دون الحاجة لكتابة نوع طويل. يمكنك استخدام impl Trait فقط في حال إعادتك لنوع واحد، على سبيل المثال تُعيد الشيفرة البرمجية التالية إما NewsArticle، أو Tweet بتحديد النوع المُعاد باستخدام impl Summary إلا أن ذلك لا ينجح: fn returns_summarizable(switch: bool) -> impl Summary { if switch { NewsArticle { headline: String::from( "Penguins win the Stanley Cup Championship!", ), location: String::from("Pittsburgh, PA, USA"), author: String::from("Iceburgh"), content: String::from( "The Pittsburgh Penguins once again are the best \ hockey team in the NHL.", ), } } else { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, } } } إعادة إما NewsArticle أو Tweet ليس مسموحًا بسبب القيود التي يفرضها استخدام الصيغة impl Trait وكيفية تنفيذها في المصرّف، وسنتكلم لاحقًا عن كيفية كتابة دالة تحقق هذا السلوك لاحقًا. استخدام حدود السمة لتنفيذ التوابع شرطيا يمكننا تنفيذ التوابع شرطيًا للأنواع التي تنفّذ سمةً ما عند استخدام هذه السمة بواسطة كتلة impl التي تستخدم الأنواع المعممة مثل معاملات. على سبيل المثال، ينفّذ النوع Pair<T>‎ في الشيفرة 15 الدالة new دومًا لإعادة نسخةٍ جديدة من Pair<T>‎ (تذكر أن self هو اسم نوع مستعار للنوع الموجود في الكتلة impl وهو Pair<T>‎ في هذه الحالة)، إلا أنه في كتلة impl التالية ينفّذ Pair<T>‎ التابع cmp_display فقط إذا كان النوع T الداخلي ينفّذ السمة PartialOrd التي تمكّن المقارنة بالإضافة إلى سمة `Display التي تمكّن الطباعة. اسم الملف: src/lib.rs use std::fmt::Display; struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y } } } impl<T: Display + PartialOrd> Pair<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } } } الشيفرة 15: تنفيذ توابع شرطيًا على نوع معمّم بحسب حدود السمة يمكننا أيضًا تنفيذ سمة شرطيًا لأي نوع ينفّذ سمةً أخرى، وتنفيذ السمة على أي نوع يحقق حدود السمة يسمّى بالتنفيذات الشاملة blanket implementations ويُستخدم بكثرة في مكتبة رست القياسية؛ على سبيل المثال تنفِّذ المكتبة القياسية السمة ToString على أي نوع ينفّذ السمة Display، وتبدو كتلة impl في المكتبة القياسية بصورةٍ مشابهة لما يلي: impl<T: Display> ToString for T { // --snip-- } ولأن المكتبة القياسية تستخدم التنفيذ الشامل هذا فيمكننا استدعاء التابع to_string المعرف باستخدام السمة ToString على أي نوع ينفّذ السمة Display على سبيل المثال يمكننا تحويل الأعداد الصحيحة إلى قيمة موافقة لها في النوع String وذلك لأن الأعداد الصحيحة تنفّذ السمة Display: let s = 3.to_string(); يمكنك ملاحظة التنفيذات الشاملة في توثيق السمة في قسم "المنفّذين implementors". تسمح لنا السمات وحدود السمات بكتابة شيفرة برمجية تستخدم الأنواع المعممة مثل معاملات، وذلك للتقليل من تكرار الشيفرة البرمجية، إضافةً إلى تحديدنا للمصرف بأننا نريد لقيمة معممة أن يكون لها سلوك معين، ويمكن للمصرف عندئذ استخدام معلومات حدود السمة للتحقق من أن جميع الأنواع الثابتة المستخدمة في شيفرتنا البرمجية تحتوي على السلوك الصحيح. سنحصل في لغات البرمجة المكتوبة ديناميكيًا dynamically typed على خطأ عند وقت التشغيل runtime إذا استدعينا تابعًا على نوع لم يعرّف هذا التابع، إلا أن رست تنقل هذه الأخطاء إلى وقت التصريف بحيث تجبرنا على تصحيح المشاكل قبل أن تُنفَّذ شيفرتنا البرمجية. إضافةً لما سبق، لا يتوجب علينا كتابة شيفرة برمجية تتحقق من السلوك عند وقت التشغيل لأننا تحققنا من السلوك عند وقت التصريف، ويحسّن ذلك أداء الشيفرة البرمجية دون الحاجة للتخلي عن مرونة استخدام الأنواع المعممة. ترجمة -وبتصرف- لقسم من الفصل Generic Types, Traits, and Lifetimes من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: التحقق من المراجع References عبر دورات الحياة Lifetimes في لغة رست المقال السابق: كيفية استخدام أنواع البيانات المعممة Generic Data Types في لغة رست Rust أنواع البيانات Data Types في لغة رست Rust الحزم packages والوحدات المصرفة crates في لغة رست Rust
  24. نستخدم الأنواع المُعمَّمة لإنشاء تعاريف لعناصر مثل بصمات الدوال function signatures أو الهياكل structs، بحيث تمكننا من استخدام عدّة أنواع بيانات ثابتة. دعنا ننظر أولًا إلى كيفية تعريف الدوال والهياكل والمُعدّدات enums والتوابع methods باستخدام الأنواع المعممة، ثم سنناقش كيف تؤثر الأنواع المعممة على أداء الشيفرة البرمجية. في تعاريف الدوال نضع الأنواع المعممة عند تعريف دالة تستخدمها في بصمة الدالة function signature، وهو المكان الذي نحدد فيه عادةً أنواع بيانات المعاملات ونوع القيمة المُعادة، إذ يكسب ذلك شيفرتنا البرمجية مرونةً أكبر ويقدم مزايا أكثر للشيفرة البرمجية المُستدعية لدالتنا مع منع تكرار الشيفرة البرمجية في الوقت ذاته. لنستمرّ في مثال الدالة largest من المقالة السابقة: توضح الشيفرة 4 دالتين يعثران على أكبر قيمة في شريحة slice ما، وسنجمع هاتين الدالتين في دالة واحدة تستخدم الأنواع المعممة. اسم الملف: src/main.rs fn largest_i32(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn largest_char(list: &[char]) -> &char { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest_i32(&number_list); println!("The largest number is {}", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest_char(&char_list); println!("The largest char is {}", result); } [الشيفرة 4: دالتان تختلفان عن بعضهما بالاسم ونوع البيانات في بصمتهما] الدالة largest_i32 هي الدالة التي استخرجناها من الشيفرة 3 (في المقال السابق) التي تعثر على أكبر قيمة i32 في شريحة، بينما تعثر الدالة largest_char على أكبر قيمة char في شريحة، ولدى الدالتين المحتوى ذاته، لذا دعنا نتخلص من التكرار باستخدام الأنواع المعممة مثل معاملات في دالة وحيدة. نحتاج إلى تسمية نوع المعامل حتى نكون قادرين على استخدام عدة أنواع في دالة واحدة جديدة، كما نفعل عندما نسمّي قيم معاملات الدالة، ويمكنك هنا استخدام معرّف بمثابة اسم نوع معامل، إلا أننا سنستخدم T لأن أسماء المعاملات في لغة رست قصيرة اصطلاحًا وغالبًا ما تكون حرفًا واحدًا، كما أن اصطلاح رست في تسمية الأنواع قائمٌ على نمط سنام الجمل CamelCase، وتسمية النوع T هو اختصار لكلمة النوع "type" وهو الخيار الشائع لمبرمجي لغة رست. علينا أن نصرّح عن اسم المعامل عندما نستخدمه في متن الدالة وذلك في بصمة الدالة حتى يعرف المصرّف معنى الاسم، كما ينبغي علينا بصورةٍ مشابهة تعريف اسم نوع المعامل في بصمة الدالة قبل أن نستطيع استخدامه داخلها. لتعريف الدالة المعممة largest نضع تصاريح اسم النوع داخل قوسين مثلثين <> بين اسم الدالة ولائحة المعاملات بالشكل التالي: fn largest<T>(list: &[T]) -> &T { نقرأ التعريف السابق كما يلي: الدالة largest هي دالة معممة تستخدم نوعًا ما اسمه T، ولدى هذه الدالة معاملٌ واحدٌ يدعى list وهو قائمة من القيم نوعها T، وتعيد الدالة largest مرجعًا إلى قيمة نوعها أيضًا T. توضح الشيفرة 5 تعريف الدالة المُدمجة باستخدام نوع البيانات المعمم في بصمتها، كما توضح الشيفرة أيضًا كيفية استدعاء الدالة باستخدام شريحة من قيم i32 أو من قيم char. لاحظ أن الشيفرة البرمجية لم تُصرّف بعد، إلا أننا سنصلح ذلك لاحقًا. اسم الملف: src/main.rs fn largest<T>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {}", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest(&char_list); println!("The largest char is {}", result); } [الشيفرة 5: دالة largest تستخدم معاملات من أنواع معممة؛ إلا أن الشيفرة لا تُصرَّف بنجاح بعد] إذا صرّفنا الشيفرة البرمجية السابقة، سنحصل على الخطأ التالي: $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10) error[E0369]: binary operation `>` cannot be applied to type `&T` --> src/main.rs:5:17 | 5 | if item > largest { | ---- ^ ------- &T | | | &T | help: consider restricting type parameter `T` | 1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T { | ++++++++++++++++++++++ For more information about this error, try `rustc --explain E0369`. error: could not compile `chapter10` due to previous error تذكر رسالة الخطأ المساعدة std::cmp::PartialOrd وهي سمة trait، وسنتحدث عن السمات لاحقًا. يكفي معرفتك حتى اللحظة أن مفاد الخطأ هو أن محتوى الدالة largest لن يعمل لجميع الأنواع المحتملة للنوع T، وذلك لأننا نريد مقارنة قيم النوع T في محتوى الدالة ويمكننا الآن استخدام أنواع يمكن لقيمها أن تُرتَّب. يمكننا لتمكين المقارنات استخدام السمة std::cmp::PartialOrd في المكتبة القياسية على الأنواع. إذا اتبعنا النصيحة الموجودة في رسالة الخطأ فسنحدّ من الأنواع الصالحة في T إلى الأنواع التي تطبّق السمة PartialOrd، وسيُصرَّف المثال بنجاح لأن المكتبة القياسية تطبّق السمة PartialOrd على كلٍ من النوعين i32 و char. في تعاريف الهياكل يمكننا أيضًا تعريف الهياكل، بحيث تستخدم أنواع معممة مثل معامل ضمن حقل أو أكثر باستخدام <>. نعرّف في الشيفرة 6 هيكل Point<T>‎ يحتوي على الحقلين x و y وهي قيم إحداثيات من أي نوع. اسم الملف: src/main.rs struct Point<T> { x: T, y: T, } fn main() { let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 }; } [الشيفرة 6: هيكل Point<T>‎ يخزن بداخله القيمتين x و y نوعهما T] طريقة الكتابة في استخدام الأنواع المعممة في تعريف الهيكل مشابهة لطريقة الكتابة المستخدمة في تعاريف الدالة سابقًا، إذ نصرح أولًا عن اسم نوع المعامل داخل أقواس مثلثة بعد اسم الهيكل، ثم نستخدم النوع المعمم في تعريف الهيكل في المواضع التي نحدد فيها أنواع بيانات ثابتة في حالات أخرى. لاحظ أننا استخدمنا نوعًا معممًا واحدًًا فقط لتعريف Point<T>‎ وبالتالي يخبرنا هذا التعريف أن الهيكل Point<T>‎ هو هيكل معمم باستخدام نوع T وأن الحقلين x و y يحملان النوع ذاته أيًا يكن. لن تُصرَّف الشيفرة البرمجية إذا أردنا إنشاء نسخة من الهيكل Point<T>‎ يحمل قيمًا من أنواع مختلفة كما نفعل في الشيفرة 7. اسم الملف: src/main.rs struct Point<T> { x: T, y: T, } fn main() { let wont_work = Point { x: 5, y: 4.0 }; } [الشيفرة 7: يجب أن يكون للحقلين x و y النوع ذاته لأنهما يحملان النوع المعمم ذاته T] نخبر المصرف في هذا المثال عند إسنادنا القيمة العددية الصحيحة "5" إلى x أن النوع المعمم T سيكون عددًا صحيحًا لهذه النسخة من Point<T>‎. نحصل على خطأ عدم مطابقة النوع التالي عندما نحدد أن y قيمتها "4.0" وهي معرّفة أيضًا بحيث تحمل قيمة x ذاتها: $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10) error[E0308]: mismatched types --> src/main.rs:7:38 | 7 | let wont_work = Point { x: 5, y: 4.0 }; | ^^^ expected integer, found floating-point number For more information about this error, try `rustc --explain E0308`. error: could not compile `chapter10` due to previous error نستخدم معاملات الأنواع المعممة المتعددة لتعريف الهيكل Point بحيث يكون كلًا من x و y من نوع معمم ولكن مختلف. على سبيل المثال، نغيّر في الشيفرة 8 تعريف Point لتصبح دالةً معممةً تحتوي النوعين T و U، إذ يكون نوع x هو T و y من النوع U. اسم الملف: src/main.rs struct Point<T, U> { x: T, y: U, } fn main() { let both_integer = Point { x: 5, y: 10 }; let both_float = Point { x: 1.0, y: 4.0 }; let integer_and_float = Point { x: 5, y: 4.0 }; } [الشيفرة 8: دالة Point<T, U>‎ المعممة التي تحتوي على نوعين بحيث يكون لكلٍ من المتغيرين x و y نوع مختلف] جميع نسخ Point الآن مسموحة، ويمكنك استخدام عدّة أنواع معممة مثل معاملات في تعريف الدالة إلا أن استخدام الكثير منها يجعل شيفرتك البرمجية صعبة القراءة. إذا احتجت كثيرًا من الأنواع المعممة في شيفرتك البرمجية فهذا يعني أنه عليك إعادة هيكلة شيفرتك البرمجية إلى أجزاء أصغر. في تعاريف المعدد نستطيع تعريف المعددات، بحيث تحمل أنواع بيانات معممة في متغايراتها variants كما هو الأمر في الهياكل. دعنا ننظر إلى مثال آخر باستخدام المعدد Option<T>‎ الموجود ضمن المكتبة القياسية الذي ناقشناه سابقًا: enum Option<T> { Some(T), None, } يجب أن تفهم هذا التعريف بحلول هذه النقطة بمفردك، فكما ترى معدّد Option<T>‎ هو معدد معمم يحتوي على النوع T ولديه متغايران: Some الذي يحمل قيمةً واحدةً من النوع T و None الذي لا يحمل أي قيمة. يمكننا التعبير عن المفهوم المجرّد للقيمة الاختيارية باستخدام المعدد Option<T>‎، ولأن Option<T>‎ هو معدد معمم، فهذا يعني أنه يمكننا استخدامه بصورةٍ مجرّدة بغض النظر عن النوع الخاص بالقيمة الاختيارية. يمكن للمعددات أن تستخدم أنواعًا معممةً متعددة أيضًا، والمعدد Result الذي استخدمناه في مقال الأخطاء والتعامل معها هو مثال على ذلك: enum Result<T, E> { Ok(T), Err(E), } المعدد Result هو معدد مُعمم يحتوي على نوعين، هما: T و E، كما يحتوي على متغايرين، هما: Ok الذي يحمل قيمة من النوع T و Err الذي يحمل قيمة من النوع E، يسهّل هذا التعريف عملية استخدام المعدد Result في أي مكان يوجد فيه عملية قد تنجح (في هذه الحالة إعادة قيمة من نوع ما T)، أو قد تفشل (في هذه الحالة إعادة خطأ من قيمة ما E)، وهذا هو ما استخدمناه لنفتح الملف في مقال الأخطاء والتعامل معها في رست عندما كان النوع T يحتوي على النوع std::fs::File عند فتح الملف بنجاح وكان يحتوي E على النوع std::io::Error عند ظهور مشاكل في فتح الملف. يمكنك اختصار حالات التكرار عندما تصادف حالات في شيفرتك البرمجية تحتوي على تعاريف هياكل ومعددات مختلفة فقط بنوع القيمة التي يحمل كل منها، وذلك عن طريق استخدام الأنواع المعممّة عوضًا عنها. في تعاريف التابع يمكننا تطبيق التوابع على الهياكل والمعددات (كما فعلنا سابقًا في مقال استخدام الهياكل structs لتنظيم البيانات) واستخدام الأنواع المعممة في تعريفها أيضًا. توضح الشيفرة 9 الهيكل Point<T>‎ الذي عرفناه في الشيفرة 6 مصحوبًا بتابع يدعى x داخله. اسم الملف: src/main.rs struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } fn main() { let p = Point { x: 5, y: 10 }; println!("p.x = {}", p.x()); } [الشيفرة 9: تطبيق تابع تدعى x على الهيكل Point<T>‎ وهو تابع يعيد مرجعًا إلى الحقل x الذي نوعه T] عرّفنا هنا تابعًا يدعى x داخل Point<T>‎ يعيد مرجعًا إلى البيانات الموجودة في الحقل x. لاحظ أنه علينا التصريح عن T قبل impl حتى يتسنى لنا استخدام T لتحديد أننا نطبّق التوابع الموجودة في النوع Point<T>‎. تتعرّف رست على وجود النوع بين أقواس مثلثة في Point على أنه نوع معمّم وذلك بالتصريح عن T على أنه نوع مُعمّم بعد impl بدلًا عن النظر إلى النوع على أنه نوع ثابت. يمكننا اختيار اسم مختلف عن اسم معامل النوع المعمم المصرح في تعريف الهيكل لمعامل النوع المعمم هذا، إلا أن استخدام الاسم ذاته هي الطريقة الاصطلاحية. تُعرَّف التوابع المكتوبة ضمن impl التي تصرّح عن النوع المعمّم ضمن أي نسخة من هذا النوع بغض النظر عن النوع الثابت الذي يستبدل هذا النوع المعمم في نهاية المطاف. يمكننا أيضًا تحديد بعض القيود على الأنواع المعممة عند تعريف التوابع الخاصة بالنوع، فيمكننا مثلًا تطبيق تابع على نسخ Point<f32>‎ فقط بدلًا من نسخ Point<T>‎ التي تحتوي على أي نوع مُعمّم. نستخدم في الشيفرة 10 النوع الثابت f32 وبالتالي لا نصرّح عن أي نوع بعد impl. اسم الملف: src/main.rs impl Point<f32> { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() } } [الشيفرة 10: كتلة impl تُطبَّق فقط على هيكل بنوع ثابت معين موجود في معامل النوع المعمم T] تشير الشيفرة البرمجية السابقة إلى أن النوع Point<f32‎>‎ سيتضمن التابع distance_from_origin، لكن لن تحتوي النسخ الأخرى من Point<T>‎، إذ تمثّل T نوعًا آخر ليس f32 على تعريف هذا التابع داخلها. يقيس هذا التابع مسافة النقطة عن مبدأ الإحداثيات (0.0 ,0.0) ويستخدم عمليات حسابية متاحة فقط لأنواع قيم العدد العشري floating point. لا تطابق معاملات النوع المُعمم في تعريف الهيكل معاملات النوع المعمم الموجودة في بصمة الهيكل نفسه دومًا. لاحظ أننا نستخدم النوعين المعمّمين X1 و Y1 في الشيفرة 11 اللذين ينتميان إلى الهيكل Point و X2 و Y2 لبصمة التابع mixup لتوضيح المثال أكثر. تُنشئ نسخة Point جديدة باستخدام قيمة x من self Point (ذات النوع X1) وقيمة y من النسخة Point التي مرّرناها (ذات النوع Y2). اسم الملف: src/main.rs struct Point<X1, Y1> { x: X1, y: Y1, } impl<X1, Y1> Point<X1, Y1> { fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> { Point { x: self.x, y: other.y, } } } fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: "Hello", y: 'c' }; let p3 = p1.mixup(p2); println!("p3.x = {}, p3.y = {}", p3.x, p3.y); } [الشيفرة 11: تابع يستخدم أنواع معممة مختلفة من تعريف الهيكل] عرّفنا في main الهيكل Point الذي يحتوي على النوع i32 للحقل x بقيمة 5، وحقل من النوع f64 يدعى y بقيمة 10.4. يمثل المتغير p2 هيكلًا من النوع Point يحتوي على شريحة سلسلة نصية string slice داخله في الحقل x بقيمة "Hello"، وقيمة من النوع char في الحقل y بقيمة c. يعطينا استدعاء mixup على النسخة p1 باستخدام p2 مثل معامل p3، وهو هيكل سيحتوي داخله على قيمة من النوع i32 في الحقل x لأن x أتى من p1، وسيحتوي p3 على حقل y داخله قيمة من نوع char لأن y أتى من p2، وبالتالي سيطبع استدعاء الماكرو println!‎ التالي: p3.x = 5, p3.y = c كان الهدف من هذا المثال توضيح حالة يكون فيها المعاملات المعمّمة مصرّح عنها في impl وبعضها الآخر مصرّح عنها في تعريف التابع، إذ أنّ المعاملات المعممة X1 وY1 مصرّحٌ عنهما هنا بعد impl لأنهما يندرجان تحت تعريف الهيكل، بينما تصريح المعاملين X2 و Y2 كان بعد fn mixup لأنهما متعلقان بالتابع فقط. تأثير استخدام المعاملات المعممة على أداء الشيفرة البرمجية قد تتسائل عمّا إذا كان هناك تراجع في أداء البرنامج عند استخدام الأنواع المعمّاة مثل معاملات، والخبر الجيد هنا أن استخدام الأنواع المعمّاة لن يجعل من البرنامج أبطأ ممّا سيكون عليه إذا استخدمت أنواعًا ثابتة. تنجح رست بتحقيق ذلك عن طريق إجراء عملية توحيد شكل monomorphization الشيفرة البرمجية باستخدام الأنواع المعماة وقت التصريف؛ وعملية توحيد الشكل هي عملية تحويل الشيفرة البرمجية المعممة إلى شيفرة برمجية محددة عن طريق ملئها بالأنواع الثابتة المستخدمة عند التصريف، ويعكس المصرف في هذه المرحلة ما يفعله عندما يُنشئ دالة معمّاة في الشيفرة 5؛ إذ ينظر المصرّف إلى الأماكن التي يوجد بها شيفرة برمجية معماة ويولّد شيفرة برمجية تحتوي على أنواع ثابتة تُستدعى منها الشيفرة البرمجية المعمّاة. دعنا ننظر إلى كيفية عمل هذه الخطوة باستخدام المعدد المعمم Option<T>‎ الموجود في المكتبة القياسية: let integer = Some(5); let float = Some(5.0); تُجري رست عملية توحيد الشكل عندما تصرَّف الشيفرة البرمجية السابقة، ويقرأ المصرف خلال العملية القيم التي استُخدمت في نسخ Option<T>‎ ويتعرف على نوعين مختلفين من Option<T>‎ أحدهما i32 والآخر f64، وبالتالي يتحول التعريف المعمم للنوع Option<T>‎ إلى تعريفين، أحدهما تعريف للنوع i32 والآخر للنوع f64 ويُستبدل التعريفان بالتعريف المعمّم. هذا ما تبدو عليه الشيفرة البرمجية السابقة بعد إجراء عملية توحيد الشكل (يستخدم المصرف أسماءً مختلفة عمّا نستخدم هنا في المثال التوضيحي): اسم الملف: src/main.rs enum Option_i32 { Some(i32), None, } enum Option_f64 { Some(f64), None, } fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0); } يُستبدل النوع المعمم Option<T>‎ بتعاريف الأنواع المحددة عن طريق المصرف، ولأن رست تُصرف الشيفرة البرمجية المعممة إلى شيفرة برمجية ذات نوع ثابت لكل نسخة فلا يوجد هناك أي تراجع في أداء الشيفرة البرمجية عند استخدام الأنواع المعممة، إذ تعمل الشيفرة البرمجية عند تشغيلها بأداء مماثل لما قد يكون عليه أداء الشيفرة البرمجية التي تكرّر كل تعريف يدويًا، وتجعل عملية توحيد الشكل من الأنواع المعممة في رست ميزة فعّالة جدًا عند وقت التشغيل. ترجمة -وبتصرف- لقسم من الفصل Generic Types, Traits, and Lifetimes من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: السمات Traits في لغة رست Rust المقال السابق: مقدمة إلى مفهوم الأنواع المعممة Generic Types في لغة رست Rust الحزم packages والوحدات المصرفة crates في لغة رست Rust استخدام التوابع methods ضمن الهياكل structs في لغة رست Rust
  25. تحتوي كل لغة برمجة على عدد من الأدوات للتعامل مع تكرار المفاهيم بفعالية، وتمثّل الأنواع المعممة generic types في لغة رست هذه الأداة، والتي تتضمن بدائل مجرّدة لأنواع حقيقية concrete أو خاصيات أخرى. يمكننا التعبير عن سلوك الأنواع المعممة أو كيف ترتبط مع أنواع معممة أخرى دون معرفة نوع القيمة التي ستكون بداخلها عند تصريف وتشغيل الشيفرة البرمجية. يمكن أن تأخذ الدوال بعض الأنواع المعممة معاملات لها بدلًا من أنواع حقيقية، مثل i32 أو String بطريقة مماثلة لما ستكون عليه دالة تأخذ معاملات بقيم غير معروفة لتشغيل الشيفرة البرمجية ذاتها باستخدام عدّة قيم حقيقية. استخدمنا في الحقيقة الأنواع المعممة سابقًا عندما تكلمنا عن Option<T>‎ وفي الفصل الثامن عن Vec<T>‎ و HashMap<K, V>‎ وفي الفصل التاسع عن Result<T, E>‎ في هذه السلسلة البرمجة بلغة رست. سننظر في هذا الفصل إلى كيفية تعريف نوع أو دالة أو تابع خاص بك باستخدام الأنواع المعممة. دعنا ننظر أولًا إلى كيفية استخراج دالة ما للتقليل من عملية تكرار الشيفرات البرمجية، ثمّ سنستخدم الطريقة ذاتها لإنشاء دالة معمّمة باستخدام دالتين مختلفتين فقط بأنواع معاملاتهما، كما سنشرح أيضًا كيفية استخدام الأنواع المعممة ضمن تعريف هيكل struct أو تعريف مُعدّد enum. سنتعلّم بعدها كيفية استخدام السمات traits لتعريف السلوك في سياق الأنواع المعممة، إذ يمكنك استخدام السمات مع الأنواع المعممة لتقييد قبول أنواع تحتوي على سلوك معين بدلًا من احتوائها لأي نوع. وأخيرًا، سنناقش دورات الحياة lifetimes وهي مجموعة من الأنواع المعممة التي تعطي المصرّف معلومات حول ارتباط المراجع references ببعضها بعضًا، وتسمح لنا دورات الحياة بإعطاء المصرف كمًا كافيًا من المعلومات عن القيم المُستعارة borrowed values حتى يتسنّى له التأكد من المراجع التي ستكون صالحة في أكثر من موضع مقارنةً بالمواضع التي يمكن للمصرف التحقق منها بنفسه دون مساعدتنا. إزالة التكرار باستخراج الدالة تسمح لنا الأنواع المعممة باستبدال أنواع محددة مع موضع مؤقت placeholder يمثل أنواع عدّة بهدف التخلص من الشيفرة البرمجية المتكررة. دعنا ننظر إلى كيفية التخلص من الشيفرات البرمجية المتكررة دون استخدام الأنواع المعممة قبل أن نتكلم عن كيفية كتابتها، وذلك عن طريق استخراج الدالة التي ستستبدل قيمًا معيّنة بموضع مؤقت يمثّل قيمًا متعددة، ثم نطبق الطريقة ذاتها لاستخراج دالة معممة. سنبدأ بالتعرف على الشيفرات البرمجية المتكررة التي يمكن أن تستخدم الأنواع المعممة عن طريق التعرف على الشيفرات البرمجية المتكررة الممكن استخراجها إلى دالة. دعنا نبدأ ببرنامج قصير موضّح في الشيفرة 1، إذ يعثر هذا البرنامج على أكبر رقم موجود في قائمة ما. اسم الملف: src/main.rs fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!("The largest number is {}", largest); } الشيفرة 1: العثور على أكبر رقم في قائمة من الأرقام نخزّن هنا قائمة من الأرقام الصحيحة في المتغير number_list ونعيّن مرجعًا إلى العنصر الأول في القائمة ضمن متغير يدعى largest، ثمّ نمرّ على العناصر الموجودة في القائمة بالترتيب ونفحص إذا كان الرقم الحالي أكبر من الرقم الذي خزّننا مرجعه في largest؛ فإذا كانت الإجابة نعم، نستبدل المرجع السابق بمرجع الرقم الحالي؛ وإلا -إذا كان الرقم الحالي أصغر أو تساوي من الرقم largest- لا نغير قيمة المتغير وننتقل إلى الرقم الذي يليه في القائمة. يجب أن يمثل largest مرجعًا لأكبر رقم في القائمة بعد النظر إلى كل الأرقام، وهو في هذه الحالة 100. تغيّرت مهمتنا الآن: علينا كتابة برنامج يفحص أكبر رقم ضمن قائمتين مختلفتين من الأرقام، ولفعل ذلك يمكننا نسخ الشيفرة البرمجية في الشيفرة 1 مرةً أخرى واستخدام المنطق ذاته في موضعين مختلفين ضمن البرنامج كما هو موضح في الشيفرة 2. اسم الملف: src/main.rs fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!("The largest number is {}", largest); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!("The largest number is {}", largest); } الشيفرة 2: شيفرة برمجية تجد أكبر رقم في قائمتين من الأرقام على الرغم من أن الشيفرة البرمجية السابقة تعمل بنجاح إلا أن نسخ الشيفرة البرمجية عملية رتيبة ومعرضة للأخطاء، علينا أيضًا أن نتذكر تعديل الشيفرة البرمجية في عدة مواضع إذا أردنا التعديل على منطق البرنامج. لنُنشئ حلًا مجرّدًا للتخلص من التكرار وذلك بتعريف دالة تعمل على أي قائمة من الأرقام الصحيحة بتمريرها مثل معامل للدالة. يجعل هذا الحل من شيفرتنا البرمجية أكثر وضوحًا ويسمح لنا بالتعبير عن مفهوم العثور على أكبر رقم ضمن قائمة ما بصورةٍ مجرّدة. نستخرج الشيفرة البرمجية التي تبحث عن أكبر عدد إلى دالة تدعى largest في الشيفرة 3، من ثم نستدعي الدالة لإيجاد أكبر عدد في القائمتين الموجودتين في الشيفرة 2، مما يمكننا من استخدام الدالة على أي قائمة تحمل عناصر من النوع i32. اسم الملف: src/main.rs fn largest(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {}", result); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let result = largest(&number_list); println!("The largest number is {}", result); } الشيفرة 3: شيفرة برمجية مجردة لإيجاد أكبر رقم ضمن قائمتين تقبل الدالة largest معاملًا يدعى list ويمثّل أي شريحة slice حقيقية من قيم i32، ونتيجةً لذلك يمكننا استدعاء الدالة وتنفيذ الشيفرة البرمجية بحسب القيم المحددة التي نمررها للدالة. اختصارًا لما سبق، إليك الخطوات التي اتبعناها للوصول من الشيفرة 2 إلى الشيفرة 3: التعرف على الشيفرة البرمجية المتكررة. استخراج الشيفرة البرمجية المتكررة إلى محتوى دالة وتحديد القيم التي نمررها للدالة والقيم التي تعيدها الدالة في بصمة الدالة signature. تحديث مواضع نسخ الشيفرة البرمجية لتستدعي الدالة بدلًا من ذلك. سنستخدم الخطوات ذاتها لاحقًا مع الأنواع المعممة للتقليل من الشيفرات البرمجية المكررة، إذ تسمح الأنواع المعممة للشيفرة البرمجية بالعمل على الأنواع المجردة بالطريقة ذاتها التي يتعامل فيها محتوى الدالة على قائمة مجرّدة بدلًا من قيم محددة. على سبيل المثال، دعنا نفترض وجود دالتين: دالة تعثر على أكبر رقم في شريحة من قيم i32 وأخرى تعثر على أكبر قيمة في شريحة من قيم char، كيف يمكننا التخلص من التكرار هنا؟ هذا ما سنناقشه تاليًا. ترجمة -وبتصرف- لقسم من الفصل Generic Types, Traits, and Lifetimes من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: كيفية استخدام أنواع البيانات المعممة Generic Data Types في لغة رست Rust المقال السابق: الاختيار ما بين الماكرو panic!‎ والنوع Result للتعامل مع الأخطاء في لغة رست Rust كتابة أصناف وتوابع معممة في جافا أنواع البيانات Data Types في لغة رست Rust
×
×
  • أضف...