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

Naser Dakhel

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

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

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

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

  1. بدأنا عملية برمجة أداة سطر الأوامر المشابهة لأداة 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. اقرأ أيضًا المقال السابق: كتابة برنامج سطر أوامر بلغة رست: اختبار البرنامج ما هو سطر الأوامر ؟ كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج
  2. بدأنا عملية برمجة أداة سطر الأوامر المشابهة لأداة 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: التعامل مع الدخل والخرج
  3. بدأنا في المقال السابق كتابة برنامج سطر أوامر 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 حل المشكلات وأهميتها في احتراف البرمجة
  4. يمثّل هذا المقال تطبيقًا لجميع المهارات التي تعلمتها حتى الآن باتباعك لهذه السلسلة البرمجة بلغة رست، ونظرةً عمليةً إلى المزيد من المزايا الموجودة في المكتبة القياسية 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. اقرأ أيضًا المقال السابق: تنظيم الاختبارات Tests في لغة رست Rust ما هو سطر الأوامر 10 أمثلة عملية على استخدام الأداة Grep تفيد المبرمجين
  5. يُعد اختبار الشيفرة البرمجية كما ذكرنا سابقًا في مقال كتابة الاختبارات في لغة رست 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. اقرأ أيضًا المقال السابق: التحكم بتنفيذ الاختبارات في لغة رست Rust المسارات paths وشجرة الوحدة module tree في رست Rust الاختيار ما بين الماكرو panic!‎ والنوع Result للتعامل مع الأخطاء في لغة Rust
  6. يصرّف 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. اقرأ أيضًا المقال السابق: كتابة الاختبارات في لغة رست Rust كيفية كتابة الدوال Functions والتعليقات Comments في لغة راست Rust
  7. الاختبارات 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. اقرأ أيضًا المقال السابق: التحقق من المراجع References باستخدام دورات الحياة Lifetimes في لغة رست تعلم لغة رست Rust: البدايات استراتيجيات اختبارات مشاريع الويب للتوافق مع المتصفحات
  8. تعدّ دورات الحياة نوعًا آخر من الأنواع المعمّمة 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. اقرأ أيضًا المقال السابق: السمات Traits في لغة رست Rust مقدمة إلى مفهوم الأنواع المعممة Generic Types في لغة Rust استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust
  9. يمكن أن تعرّف السمة وظيفة نوع محدد ويمكن مشاركتها مع عدّة أنواع، ويمكننا استخدام السمات لتعريف سلوك مشترك بطريقة مجردة، ويمكننا استخدام حدود السمة 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. اقرأ أيضًا المقال السابق: كيفية استخدام أنواع البيانات المعممة Generic Data Types في لغة رست Rust أنواع البيانات Data Types في لغة رست Rust الحزم packages والوحدات المصرفة crates في لغة رست Rust
  10. نستخدم الأنواع المُعمَّمة لإنشاء تعاريف لعناصر مثل بصمات الدوال 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. اقرأ أيضًا المقال السابق: مقدمة إلى مفهوم الأنواع المعممة Generic Types في لغة رست Rust الحزم packages والوحدات المصرفة crates في لغة رست Rust استخدام التوابع methods ضمن الهياكل structs في لغة رست Rust
  11. تحتوي كل لغة برمجة على عدد من الأدوات للتعامل مع تكرار المفاهيم بفعالية، وتمثّل الأنواع المعممة 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. اقرأ أيضًا المقال السابق: الاختيار ما بين الماكرو panic!‎ والنوع Result للتعامل مع الأخطاء في لغة رست Rust المقال التالي: كيفية استخدام أنواع البيانات المعممة Generic Data Types في لغة رست Rust كتابة أصناف وتوابع معممة في جافا أنواع البيانات Data Types في لغة رست Rust
  12. كيف يمكننا الاختيار ما بين استدعاء الماكرو panic!‎ وإعادة القيمة Result عند حدوث الأخطاء؟ عندما تهلع الشيفرة البرمجية (أي عند استدعاء الماكرو panic!‎)، فليس هناك أي طريقة لحل ذلك الخطأ، ويمكنك استدعاء panic!‎ لأي خطأ كان. وسواءٌ كان خطأ يُمكن حلّه أو لا، فأنت من يتخذ القرار بجعل الخطأ هذا قابلًا للحل في شيفرتك البرمجية أم لا؛ فعندما تختار إعادة القيمة Result، فأنت تحاول منح الشيفرة البرمجية التي استدعت ذلك الفعل الذي تسبب بالخطأ (دالةٌ ما) بعض الخيارات للتعامل مع ذلك الخطأ بحيث تحاول الشيفرة البرمجية حله بطريقة ملائمة لكل حالة، أو أن تحدد أن قيمة الخطأ Err غير قابلة للحل مما يتسبب باستدعاء الماكرو panic!‎ وبالتالي جعل الخطأ القابل للحل خطأ غير قابل للحل. إذًا، إعادة القيمة Result هي خيار افتراضي جيّد عندما تعرّف دالة ما قد تفشل في بعض الأحيان. من المحبّذ في حالات كتابة الأمثلة والشيفرات البرمجية التجريبية كتابة شيفرة برمجية تهلع بدلًا من إعادة القيمة Result، دعنا ننظر إلى السبب ومن ثم نناقش الحالات التي قد لا يستطيع فيها المصرّف إخبارنا بإمكانية حدوث خطأ ما، إلا أنك تستطيع فعل ذلك. سنختتم أيضًا هذا المقال بتوجيهات عامة تساعدك في تحديد الحالات التي يكون فيها جعل الشيفرة البرمجية تهلع بصورةٍ أفضل. أمثلة وشيفرات برمجية تجريبية واختبارات استخدام شيفرة برمجية تتعامل مع الأخطاء بصورةٍ جيدة عندما تكتب مثالًا ما لتوضيح مفهوم معين يجعل من المثال أقل وضوحًا، ونستدعي عادةً في الأمثلة تابعًا مثل unwrap يمكن أن يهلع مثل موضع مؤقت placeholder من أجل الطريقة التي تريد التعامل مع الأخطاء في تطبيقك، والتي قد تختلف بناءً على ما تفعله بقية الشيفرة البرمجية. التابعان unwrap و expect مفيدان جدًا أيضًا عند كتابة الشيفرات البرمجية التجريبية قبل أن تتخذ قرار بخصوص التعامل مع الأخطاء، إذ يساعدك التابعان بترك علامات واضحة في شيفرتك البرمجية انتظارًا للوقت الذي تريد فيه جعل برنامجك أكثر متانةً بالتعامل مع الأخطاء. إذا فشل استدعاء تابع ما ضمن اختبار فمن الأفضل جعل كل الاختبار يفشل حتى لو كان التابع ذلك غير مضمّنٍ في الاختبار الأساسي، ولأن الماكرو panic!‎ هو بمثابة إشارة إلى أن الاختبار سيفشل، فإن استدعاء unwrap أو expect هو ما يجب حدوثه. الحالات التي تعرف فيها معلومات أكثر من المصرف من الملائم أيضًا استدعاء unwrap أو expect عند تواجد منطق ما يضمن أن Result ستحتوي على قيمة Ok إلا أن هذا المنطق لا يمكن فهمه من قبل المصرّف، إذ ستتواجد قيمة Result بحاجة للتعامل معها وفحصها، فأي عملية تستدعيها قد تفشل عمومًا على الرغم من أن الأمر مستحيل منطقيًا في هذه الحالة. من المقبول استدعاء unwrap إذا استطعت أن تتأكد بفحص الشيفرة البرمجية يدويًا أنها لن تحتوي على متغاير Err أبدًا، والأفضل في هذه الحالة أن توثّق السبب الذي تعتقد أنك لن تحصل فيه على متغاير Err في نص expect. إليك مثالًا: fn main() { use std::net::IpAddr; let home: IpAddr = "127.0.0.1" .parse() .expect("Hardcoded IP address should be valid"); } نُنشئ هنا نسخةً من IpAddr بالمرور على السلسلة النصية المكتوبة في الشيفرة البرمجية، ويمكن ملاحظة أن "127.0.0.1" هو عنوان IP صالح وبالتالي من المقبول استخدام expect هنا، إلا أن وجود سلسلة نصية صالحة مكتوبة في الشيفرة البرمجية لا يغير من النوع المعاد للتابع parse، إذ أننا ما زلنا نحصل على قيمة Result وسيجبرنا المصرف على التعامل مع Result لأنه يفترض أن وجود المتغاير Err بداخل Result أمر ممكن الحدوث وذلك لأن المصرف ليس ذكي بالقدر الكافي ليرى أن السلسلة النصية تمثل عنوان IP صالح دومًا. يوجد احتمال بوجود المتغاير Err إذا أتى عنوان IP مثل دخل من المستخدم بدلًا من كتابته مباشرةً في الشيفرة البرمجية وعندها علينا أن نتعامل مع Result بطريقة شاملة بدلًا من الطريقة الحالية. ذكر الافتراض أن عنوان IP هذا مكتوب في الشيفرة البرمجية وبالتالي سيُطلب منّا تغيير expect للحصول على شيفرة برمجية تتعامل مع الأخطاء بصورةٍ أفضل في المستقبل، وبالتالي فنحن بحاجة للحصول على عنوان IP من مصادر أخرى. توجيهات للتعامل مع الأخطاء يُنصح بجعل الشيفرة البرمجية تهلع عندما يمكن أن يؤدي الخطأ إلى حالة سيئة bad state للشيفرة البرمجية، ونقصد هنا بالحالة السيئة الحالة التي يتغير فيها افتراض assumption، أو ضمان guarantee، أو عقد contract، أو ثابت invariant، مثل الحصول على قيم غير صحيحة أو متناقضة أو مفقودة، إضافةً إلى واحدة أو أكثر من الحالات التالية: الحالة السيئة هي حالة غير متوقعة لا تحدث عادةً، مثل إدخال المستخدم بياناته بتنسيق خاطئ. يجب أن تعتمد شيفرتك البرمجية بعد حدوث هذه الحالة على فحص المشكلة بعد كل خطوة بدلًا من اتخاذ معطيات الحالة السيئة هذه. لا توجد طريقة مثالية لتمثيل هذه البيانات في الأنواع التي تستخدمها، وسننظر إلى مثال على ذلك في المقالات القادمة. إذا استدعى أحدهم شيفرتك البرمجية ومرّر لها القيم التي تؤدي إلى حالة سيئة، فمن الأفضل إعادة الخطأ إذا استطعت، حتى يقرّر مستخدم المكتبة الإجراء الذي يريد اتخاذه في هذه الحالة، إلا أن الاستمرار بتنفيذ الشيفرة البرمجية في بعض الأحيان قد يكون غير آمن أو ضار، وفي هذه الحالة يكون استدعاء الماكرو panic!‎ أفضل خيار لتنبيه المستخدم الذي يستخدم المكتبة بالخطأ الموجود في شيفرته البرمجية وكيف يستطيع إصلاحه خلال عملية التطوير. استخدام panic!‎ أيضًا مناسب في حال استدعاء الشيفرة البرمجية الخارجية التي لا تستطيع التحكم بها وإعادة حالة غير صالحة لا يمكنك إصلاحها. من الملائم عند الحالات التي تتوقع فيها حدوث فشل أن تُعيد القيمة Result بدلًا من استدعاء panic!‎، مثل تمرير بيانات مشوّهة وتحليلها، أو طلب HTTP يُعيد حالة تُشير إلى وصول حدٍ معدّل ما rate limit، وفي هذه الحالة تُشير إعادة القيمة Result إلى أن الخطأ الذي حصل هو خطأ متوقع حدوثه عند استدعاء الشيفرة البرمجية، التي ستحدّد كيفية التعامل مع الخطأ. يجب أن تتأكد شيفرتك البرمجية من القيم الصالحة أولًا وتهلع إذا لم تكن القيم صالحةً عندما تُجري الشيفرة عمليةً يمكن أن تضع المستخدم في خطرٍ ما عند استدعائها باستخدام قيم غير صالحة، وذلك لأسباب تتعلق بالأمان، إذ أن محاولة إجراء عمليةٍ على بيانات غير صالحة قد تعرّض شيفرتك البرمجية لثغرات أمنية، وهذا هو السبب الرئيس لاستدعاء المكتبة القياسية للماكرو panic!‎ إذا حاولت الوصول إلى عنصر يقع خارج حدود الذاكرة out-of-bound memory access، مثل محاولة الوصول إلى حيز ذاكرة لا ينتمي إلى هيكل البيانات الحالي، وهي مشكلة أمنية شائعة. تحتوي الدوال عادةً على ما يُدعى بالعقود contracts، إذ أن تصرُّف الدوال مضمونٌ فقط في حال كان الدخل يوافق بعض المتطلبات، فإذا أُخلّ بالعقد، سنحصل على حالة هلع وهذا الأمر منطقي، لأن الإخلال بالعقد يعني أن هناك خطأ من طرف مستدعي الشيفرة البرمجية، وهو نوع من الأخطاء لا تجد للشيفرة البرمجية المستدعية ضرورةً لمعالجته، وفي الحقيقة لا توجد هناك أي طريقة منطقية حتى تتعافى الشيفرة البرمجية المُستدعية، إذ يقع إصلاح الخطأ على عاتق المبرمج الذي استدعى هذه الشيفرة البرمجية. تسبب عقود الدوال عند خرقها حالة هلع، وتُشرح العقود عادةً في توثيق الواجهة البرمجية API الخاص بالدالة. الحصول على الكثير من الأخطاء في جميع الدوال أمرٌ رتيب ومزعج، ولحس الحظ يمكننا استخدام نظام أنواع لغة رست (وبالتالي التحقق من الأنواع من المصرّف) لإجراء العديد من الفحوصات نيابةً عنك. إذا كانت الدالة تقبل نوعًا محددًا للمعامل، فيمكنك متابعة منطق شيفرتك البرمجية على أساس هذا النوع وأنت مطمئن، لأن المصرف سيتأكد من استخدام النوع الصحيح دومًا، فعلى سبيل المثال سيتوقع برنامجك قيمةً ما بدلًا من عدم وجود قيمة إذا كان لديك نوع معيّن بدلًا من Option، وعندها لن يتوجب على شيفرتك البرمجية أن تتعامل مع متغاير Some و None على حدى، بل ستتحقق من حالة واحدة فقط وهو وجود قيمة، وبالتالي لن تستطيع تصريف أي شيفرة برمجية لا تمرر قيمة إلى الدالة ولن يتوجب عندها التحقق من أن الدالة تحصل على قيمة خلال وقت التشغيل. وفي مثال على ما سبق، يمكن الإشارة إلى استخدام نوع العدد الصحيح عديم الإشارة unsigned integer مثل u32، وهذا يضمن لك أن تكون قيمة المعامل موجبةً دومًا. إنشاء أنواع مخصصة بهدف التحقق دعنا نتطرق إلى فكرة استخدام نظام أنواع رست للتأكد من أن القيمة صالحة، وذلك بإنشاء نوع مخصص للتحقق. تذكّر لعبة تخمين الأرقام التي أنشأناها سابقًا، إذ طلبنا من المستخدم حينها تخمين رقم يقع بين 1 و100، ولم نتحقق حينها من أن تخمين المستخدم كان ضمن هذا المجال قبل أن نتحقق من أن التخمين يطابق الرقم السري (الإجابة الصحيحة)، إذ أننا تأكدنا وقتها من أن القيمة موجبة فقط، وفي هذه الحالة فإن البرنامج لن يكون خاطئًا، لأن طباعة "تخمينك كبير" أو "تخمينك صغير" سيعمل كم هو مطلوب، إلا أنه من الأفضل تحسين البرنامج بحيث يرشد المستخدم نحو تخمين صحيح في حال كان تخمينه ضمن المجال ويختلف سلوك البرنامج في حال كان التخمين خارج المجال المطلوب، كما هو الحال عندما يكتب المستخدم أحرفًا بدل أرقام. نستطيع تحقيق ذلك عن طريق النظر إلى تخمين المستخدم على أنه نوع i32 بدلًا من u32 للسماح بقيم سالبة ومن ثم التأكد من أن الرقم متواجد ضمن المجال كما يلي: use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..=100); loop { // --snip-- println!("Please input your guess."); let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("Failed to read line"); let guess: i32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; if guess < 1 || guess > 100 { println!("The secret number will be between 1 and 100."); continue; } match guess.cmp(&secret_number) { // --snip-- Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break; } } } } يتحقق تعبير if فيما إذا كانت القيمة خارج المجال ويخبر المستخدم بالمشكلة، ثم يستدعي continue للبدء بالتكرار التالي من الحلقة لسؤال المستخدم عن التخمين مرةً أخرى. يمكننا الاستمرار بتنفيذ المقارنات بعد تعبير if وذلك بين guess والرقم السري بمعرفة أن guess الآن هي ضمن المجال من 1 إلى 100، إلا أن الحل السابق ليس بالحل المثالي، فوجود عملية تحقق مثل هذه في برنامج يعمل فقط على القيم التي تقع ما بين 1 و100 عملية رتيبة، إذ علينا تكرار عملية التحقق داخل كل دالة في هذا البرنامج، كما قد يؤثر ذلك على أداء البرنامج. يمكننا إنشاء نوع جديد عوضًا عمّا سبق وأن نضع عملية التحقق في دالة، ومن ثم يمكننا إنشاء نسخة عن النوع بدلًا من تكرار عملية التحقق في كل مكان، وبهذه الطريقة يصبح استخدام الدوال للنوع الجديد أكثر أمانًا في بصمتها signature، إذ أنها ستستخدم القيم المسموحة في البرنامج فقط. توضح الشيفرة 13 طريقةً لتعريف النوع Guess الذي يُنشئ نسخةً من Guess إذا استقبلت الدالة new قيمةً ما بين 1 و100. #![allow(unused)] fn main() { 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 } } pub fn value(&self) -> i32 { self.value } } } الشيفرة 13: نوع Guess سيستمر فقط في حال كانت القيم بين 1 و100 نعرّف هيكلًا أولًا باسم Guess يحتوي على الحقل field value الذي يخزن بداخله قيمة من النوع i32، وهو الحقل الذي سنخزّن فيه تخمين المستخدم. نطبّق بعدها دالةً مرتبطة بالهيكل‏ Guess تدعى new، إذ تُنشئ هذه الدالة نسخةً من قيم Guess وهي معرفة بحيث تتلقى معاملًا واحدًا يُدعى value نوعه i32 وأن تعيد هيكلًا من النوع Guess. تفحص الشيفرة البرمجية داخل الدالة new القيمة value لتتأكد من أنها تقع ما بين 1 و100، وإذا لم تحقق value هذا الشرط، نستدعي الماكرو panic!‎ الذي سينبّه المبرمج الذي يكتب الشيفرة البرمجية المُستدعية للدالة أن شيفرته البرمجية تحتوي على خطأ يجب إصلاحه لأن إنشاء Guess بقيمة value خارج النطاق المحدد سيخرق الاتفاق الذي تعتمد عليه الدالة Guess::new. يجب أن تُناقش الحالات التي قد تهلع فيها الدالة Guess::new في توثيق الواجهة البرمجية العلني الخاص بالدالة، وسنغطّي اصطلاحات التوثيق التي تُشير فيها لاحتمال حصول حالة هلع panic!‎ في توثيق الواجهة البرمجية API لاحقًا. نُنشئ هيكل Guess جديد بحقل value قيمته مساوية إلى المعامل value ونُعيد Guess إذا لم تتخطى value الاختبار، ثم نطبّق تابعًا يدعى value يستعير self ولا يمتلك أي معاملات أخرى، ويعيد قيمةً من نوع i32، ويُدعى هذا النوع من التوابع بالجالب getter لأن الهدف منه يكمن في الحصول على بيانات من حقوله وإعادتها. هذا التابع العام public method مهم لأن الحقل value في الهيكل Guess هو هيكل خاص private، ومن المهم لحقل value أن يكون خاصًا بحيث لا يُسمح للشيفرة البرمجية التي تستخدم الهيكل Guess بأن تضبط قيمة value مباشرةً؛ إذ يجب على الشيفرة البرمجية التي تقع خارج الوحدة module أن تستخدم الدالة Guess::new لإنشاء نسخة من Guess للتأكد من أنه لا توجد أي طريقة أن يحتوي الهيكل Guess على قيمة حقل value دون فحصها ومطابقتها للشروط ضمن الدالة Guess::new. يمكن لدالة تحتوي معاملًا أو تعيد أرقام ضمن المجال 1 إلى 100 التصريح ضمن بصمتها بأنها أنها تأخذ أو تعيد Guess بدلًا عن i32 وعندها لن تحتاج الدالة إلى إجراء أي عمليات تحقّق إضافية ضمنها. ترجمة -وبتصرف- لقسم من الفصل Error Handling من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: الأخطاء والتعامل معها في لغة رست Rust التحكم بسير تنفيذ برامج راست Rust تخزين النصوص بترميز UTF-8 داخل السلاسل النصية في لغة رست Rust
  13. لا مهرب من الأخطاء في دورة تطوير البرمجيات، لذا توفّر رست عددًا من المزايا للتعامل مع الحالات التي يحدث فيها شيء خاطئ، وتطلب رست منك في العديد من الحالات معرفتك باحتمالية حدوث الخطأ واتخاذ فعل ما قبل أن تُصرَّف compile الشيفرة البرمجية، ويجعل ذلك من برنامجك أكثر قوة بالتأكد من أنك ستكتشف الخطأ وستتعامل معه على نحوٍ مناسب قبل إطلاق شيفرتك البرمجية إلى مرحلة الإنتاج. تصنِّف رست الأخطاء ضمن مجموعتين: الأخطاء القابلة للحل recoverable errors الأخطاء غير القابلة للحل unrecoverable errors بالنسبة للأخطاء القابلة للحل، فهي أخطاء الهدف منها إعلام المستخدم بالمشكلة وإعادة محاولة العملية ذاتها مثل خطأ "لم يُعثَر على الملف file not found"، بينما تدل الأخطاء غير القابلة للحل دائمًا على أخطاء في الشيفرة البرمجية مثل محاولة الوصول إلى موقع يقع خارج نهاية مصفوفة وهذا يعني أننا نريد إيقاف تنفيذ البرنامج مباشرةً. لا تميّز معظم لغات البرمجة بين النوعين السابقين وتتعامل معهما بنقس الطريقة باستخدام الاستثناءات exceptions، إلا أن رست لا تحتوي على الاستثناءات بل تحتوي على النوع Result<T, E>‎ للأخطاء القابلة للحل والماكرو panic!‎ الذي يوقف تنفيذ البرنامج عندما يصادف خطئًا غير قابل للحل، وسنغطّي في هذه المقال كلًا من استدعاء الماكرو panic!‎ والحصول على قيم النوع Result<T, E>‎. قبل التعرف على أنواع الأخطاء وكيفية التعامل معها في لغة رست Rust، ندعوك للتعرف على الأخطاء البرمجية عامةً أولًا والتعرف على كيفية التعامل معها: الأخطاء غير القابلة للحل باستخدام الماكرو panic!‎ قد تحدث بعض الأخطاء من حين إلى الآخر في شيفرتك البرمجية، ولا يوجد أي شيء تستطيع فعله لتمنع ظهورها، وفي هذه الحالة توفر لك رست الماكرو panic!‎. هناك طريقتان لبدء حالة هلع panic، هما: فعل شيء يتسبب بهلع الشيفرة البرمجية، مثل محاولة الوصول إلى مكان خارج نطاق مصفوفة. أو استدعاء الماكرو panic!‎ مباشرةً. تتسبب الحالتين السابقتين بحالة هلع لبرنامجنا وتطبع حالات الهلع هذه افتراضيًا رسالة فشل ومن ثم تفرّغ محتويات المكدس stack وتغادر البرنامج. يمكنك عرض محتويات استدعاء المكدس عند حدوث حالة الهلع في لغة رست باستخدام متغير بيئة environment variable وذلك حتى تصبح مهمة تتبع مصدر حالة الهلع أسهل. كيفية الاستجابة إلى حالة هلع panic يبدأ البرنامج باستعادة الحالة الأولية unwinding افتراضيًا عند حدوث حالة هلع، وهذا يعني أن رست تسترجع القيم الموجودة في المكدس وتفرغها، وتتضمن هذه العملية الكثير من العمل، لذلك تسمح لك رست باختيار الحل الثاني ألا وهو الخروج من البرنامج مباشرةً مما يُنهي تنفيذ البرنامج دون تفريغ المكدس. عندها، تقع مسؤولية تحرير البيانات المستخدمة من البرنامج على عاتق نظام التشغيل. إذا أردت الحصول على ملف تنفيذي في مشروعك بحجم صغير قدر الإمكان فعليك عندها التحويل من استعادة الحالة الأولية إلى الخروج من البرنامج فور حدوث حالة هلع بكتابة panic = 'abort'‎ في قسم [profile] المناسب في ملف Cargo.toml. على سبيل المثال، إذا أردت الخروج من البرنامج فور حدوث حالة هلع في نمط الإطلاق release mode، فعليك بإضافة التالي: [profile.release] panic = 'abort' دعنا نجرّب استدعاء الماكرو panic!‎ في برنامج بسيط: اسم الملف: src/main.rs fn main() { panic!("crash and burn"); } عند تشغيل البرنامج ستحصل على خرج مشابه لما يلي: $ cargo run Compiling panic v0.1.0 (file:///projects/panic) Finished dev [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/panic` thread 'main' panicked at 'crash and burn', src/main.rs:2:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace يتسبب استدعاء الماكرو panic!‎ برسالة الخطأ السابقة والموضحة في السطرين الأخيرين. يوضح السطر الأول رسالة الهلع ومكان حدوثه في شيفرتنا البرمجية، إذ يدل "src/main.rs:2:5" على أن حالة الهلع حدثت في السطر الثاني في المحرف الخامس ضمن الملف src/main.rs، ويكون السطر المشار إليه هو سطر ضمن شيفرتنا البرمجية التي كتبناها، وإذا ذهبنا إلى المكان المُحدّد فسنجد استدعاء الماكرو panic!‎. قد يكون استدعاء الماكرو في حالات أخرى ضمن شيفرة برمجية أخرى تستدعيها شيفرتنا البرمجية وحينها سيكون اسم الملف ورقم السطر في رسالة الخطأ عائدين لشيفرة برمجية خاصة مكتوبة من قبل شخص آخر غيرنا وليس السطر الخاص بشيفرتنا البرمجية الذي أدى لاستدعاء panic!‎. يمكننا تتبع مسار backtrace الدالة التي استدعت panic!‎ لمعرفة الجزء الذي تسبب بالمشكلة ضمن شيفرتنا البرمجية، وسنناقش تتبع مسار الخطأ بالتفصيل تاليًا. تتبع مسار panic!‎ دعنا ننظر إلى مثال آخر لرؤية ما الذي يحدث عندما يُستدعى الماكرو panic!‎ من مكتبة بسبب خطأ في شيفرتنا البرمجية، وذلك بدلًا من استدعائه من ضمن شيفرتنا البرمجية مباشرةً. توضح الشيفرة 1 محاولة الوصول إلى دليل في شعاع خارج نطاق الأدلة الصالحة. اسم الملف: src/main.rs fn main() { let v = vec![1, 2, 3]; v[99]; } [شيفرة 1: محاولة الوصول إلى عنصر يقع خارج نهاية شعاع مما سيتسبب باستدعاء الماكرو panic!‎] نحاول هنا الوصول إلى العنصر المئة في الشعاع (وهو العنصر ذو الدليل 99 لأن عدّ الأدلة يبدأ من الصفر)، إلا أن الشعاع يحتوي على ثلاثة عناصر فقط، وفي هذه الحالة تهلع رست؛ إذ من المفترض أن استخدام [] سيعيد قيمة عنصر إلا أن تمرير دليل غير صالح يتسبب بهلع رست لأنها لا تعلم القيمة التي يجب أن تُعيدها بصورةٍ صحيحة. تتسبب هذه المحاولة في لغة سي C بسلوك غير معرف undefined behaviour، إذ من الممكن أن تحصل على قيمة عشوائية في مكان الذاكرة تلك على الرغم من أن حيز الذاكرة ذلك لا ينتمي إلى هيكل البيانات ويُدعى هذا الأمر بتجاوز المخزن المؤقت buffer overread، ويمكن أن يتسبب بخطورات أمنية إذا استطاع المهاجم التلاعب بالدليل بطريقة تمكّنه من قراءة معلومات لا يُفترض له أن يقرأها بحيث تكون مخزّنة بعد هيكل البيانات. لحماية برنامجك من هذا النوع من الثغرات، توقف رست تنفيذ البرنامج وترفض المتابعة إذا حاولت قراءة عنصر موجود في دليل خارج النطاق، دعنا نجرّب ذلك ونرى ما الذي يحدث: $ cargo run Compiling panic v0.1.0 (file:///projects/panic) Finished dev [unoptimized + debuginfo] target(s) in 0.27s Running `target/debug/panic` thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace تُشير رسالة الخطأ إلى السطر 4 ضمن ملف "main.rs" وذلك هو السطر الذي نحاول عنده الوصول إلى الدليل 99. تُخبرنا الملاحظة التالية أنه يمكننا ضبط متغير البيئة RUST_BACKTRACE للحصول على مسار تتبع الخطأ ومعرفة سبب حدوثه، إذ يمثّل مسار تتبع الخطأ لائحةً من جميع الدوال التي استُدعيت إلى نقطة حدوث حالة الهلع، ويعمل في رست على نحوٍ مماثل للغات البرمجة الأخرى كما يلي: المفتاح في قراءة مسار تتبع الخطأ هو البدء من البداية إلى نقطة وصولك للملفات التي كتبتها إذ أن الملفات التي كتبتها ستكون نقطة ظهور المشكلة، والسطور التي تقع قبل تلك النقطة هي السطور التي استدعتها شيفرتك البرمجية والسطور التي تلي تلك النقطة هي السطور التي استدعَت شيفرتك البرمجية، وقد تتضمن كل من هذه السطور شيفرة برمجية خاصة برست أو شيفرة برمجية خاصة بالمكتبة القياسية أو وحدات مُصرّفة crates تستخدمها. دعنا نجرب الحصول على مسار تتبع الخطأ بضبط متغير البيئة RUST_BACKTRACE إلى أي قيمة عدا 0، وسيكون خرج الشيفرة 2 التالي مشابهًا لما ستحصل عليه عندها. $ RUST_BACKTRACE=1 cargo run thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 stack backtrace: 0: rust_begin_unwind at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5 1: core::panicking::panic_fmt at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14 2: core::panicking::panic_bounds_check at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5 3: <usize as core::slice::index::SliceIndex<[T]>>::index at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10 4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9 5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9 6: panic::main at ./src/main.rs:4:5 7: core::ops::function::FnOnce::call_once at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5 note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. [الشيفرة 2: مسار تتبع الخطأ المولّد من استدعاء الماكرو panic!‎ والذي يُعرض عند ضبط متغير البيئة RUST_BACKTRACE] هناك الكثبهسير من المعلومات في الخرج، وقد يكون الخرج الذي تراه أمامك مختلفًا عمّا ستحصل عليه بحسب نظام تشغيلك وإصدار رست. علينا تمكين رموز تنقيح الأخطاء debug symbols للحصول على مسار تتبع الأخطاء بالتفاصيل هذه، إذ تكون رموز تنقيح الأخطاء مفعّلة افتراضيًا باستخدام cargo build أو cargo run دون استخدام الراية ‎--release كما هو الحال هنا. يدل السطر 6 في الشيفرة 2 إلى أن مسار تتبع الأخطاء يشير إلى السطر المسبب للمشكلة في مشروعنا ألا وهو السطر 4 في الملف src/main.rs، وإن لم نرد لبرنامجنا أن يهلع فعلينا البدء بالنظر إلى ذلك المكان المحدد في السطر الأول الذي يذكر الملف الذي كتبناه وهو الشيفرة 1 الذي يحتوي على شيفرة برمجية تتسبب بالهلع عمدًا، وتكمن طريقة حل حالة الهلع هذه في عدم محاولة الوصول إلى عنصر يقع خارج نطاق أدلة الشعاع. عليك أن تكتشف العمل الذي يتسبب بحالة الهلع في برنامجك في المستقبل وذلك بالنظر إلى القيم التي تتسبب بحالة الهلع ومن ثم النظر إلى الشيفرة البرمجية التي تسببت بها وتعديلها. سنعود لاحقًا إلى الماكرو panic!‎ وإلى الحالات الواجب عدم استخدامها للتعامل مع الأخطاء في المقال التالي لاحقًا، إلا أننا سننتقل حاليًا إلى كيفية الحل من الأخطاء باستخدام Result. الأخطاء القابلة للحل باستخدام Result ليست معظم الأخطاء خطيرة وتتطلب إيقاف كامل البرنامج عند حدوثها، ففي بعض الأحيان يدل فشل عمل دالة ما على سبب ما يتطلب انتباهك واستجابتك له، فعلى سبيل المثال إذا فشلت عملية فتح ملف ما فهذا يعني غالبًا أن الملف الذي حددته غير موجود ولعلّك تفضّل إنشاء الملف وإعادة المحاولة بدلًا من إنهاء البرنامج كاملًا. تذكر أننا ذكرنا سابقًا في مقال برمجة لعبة تخمين الأرقام بلغة رست Rust أن المعدد enum الذي يُدعى Result معرّف وداخله متغايرَان variants، هما Ok و Err كما يلي: enum Result<T, E> { Ok(T), Err(E), } يمثّل كل من T و E معاملات نوع معمّم generic، وسنناقش الأنواع المعممة بتفصيل أكثر لاحقًا، ويكفي الآن معرفة أن T يمثّل نوع القيمة التي ستُعاد في حالة النجاح مع المتغاير Ok، بينما يمثل E نوع الخطأ الذي سيُعاد في حالة الفشل مع المتغاير Err، ولأن Result تحتوي على معاملات النوع المعمم فيمكننا استخدام النوع Result والدوال المعرفة ضمنها في العديد من الحالات، إذ تختلف كل من قيمة النجاح وقيمة الخطأ التي نريد أن نُعيدها. دعنا نستدعي دالةً تُعيد القيمة Result لأن الدالة قد تفشل. نحاول في الشيفرة 3 فتح ملف. اسم الملف: src/main.rs use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); } [الشيفرة 3: فتح ملف] النوع المُعاد من File::open هو Result<T, E>‎. يُملأ المعامل المعمّم T من خلال تنفيذ File::open مع نوع قيمة النجاح ألا وهي std::fs::File والتي تمثّل مقبض الملف file handle، بينما يُستخدم النوع E لتخزين قيمة الخطأ std::io::Error. يعني النوع المُعاد هذا أن استدعاء File::open قد ينجح ويُعيد مقبض ملف يمكن الكتابة إليه أو القراءة منه، إلا أن استدعاء الدالة قد يفشل في حال لم يكن الملف موجودًا على سبيل المثال، أو عند عدم توافر الصلاحيات المناسبة للوصول إليه؛ وبالتالي، ينبغي على الدالة File::open أن تمتلك القدرة على إخبارنا فيما إذا نجحت العملية ومنحنا مقبض الملف، أو إذا فشلت وتوفّر معلومات مناسبة عن الخطأ بنفس الوقت، وهذه هي المعلومات الموجودة فعلًا في المعدّد Result. ستكون قيمة المتغير greeting_file_result في حال نجاح File::open نسخةً من Ok تحتوي على مقبض الملف، وإلا فستكون قيمة المتغير greeting_file_result في حال الفشل نسخةً من Err تحتوي على المزيد من المعلومات حول نوع الخطأ الذي حدث. نحتاج الإضافة إلى الشيفرة 3 لتتخّذ بعض الإجراءات المختلفة بحسب قيمة File::open المُعادة، وتوضح الشيفرة 4 طريقة من الطرق للتعامل مع Result باستخدام أداة بسيطة ألا وهي تعبير match الذي ناقشناه سابقًا. اسم الملف: src/main.rs use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => panic!("Problem opening the file: {:?}", error), }; } [الشيفرة 4: استخدام تعبير match للتعامل مع متغايرات Result] لاحظ أن المعدد Result ومتغايراته -كما هو الحال مع المعدد Option- أُضيف إلى النطاق في بداية الشيفرة البرمجية، لذا ليس علينا تحديد Result::‎ قبل المتغايرين Ok و Err في أذرع match. تُعيد الشيفرة البرمجية قيمة file الداخلية من المتغاير Ok عندما تكون النتيجة Ok، ومن ثم نُسند قيمة مقبض الملف إلى المتغير greeting_file، ومن ثم يمكننا استخدام مقبض الملف للقراءة منه أو الكتابة إليه بعد التعبير match. تتعامل الذراع الأخرى من match مع الحالات التي نحصل فيها على قيمة Err من File::open. استدعينا في هذا المثال الماكرو panic!‎، إذ سنحصل على الخرج التالي من الماكرو إذا لم يكن هناك أي ملف باسم "hello.txt" في المسار الحالي عند تشغيل الشيفرة البرمجية: $ cargo run Compiling error-handling v0.1.0 (file:///projects/error-handling) Finished dev [unoptimized + debuginfo] target(s) in 0.73s Running `target/debug/error-handling` thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace يخبرنا الخرج عن الخطأ بالتحديد كما اعتدنا. مطابقة عدة أخطاء ستهلع الشيفرة 4 (أي ستتسبب باستدعاء الماكرو panic!‎) عند فشل File::open لأي سبب من الأسباب، إلا أنه من الممكن أن نتخذ إجراءات مختلفة لكل سبب من الأسباب: على سبيل المثال نريد إنشاء ملف وإعادة مقبضه إذا فشلت File::open بسبب عدم وجود الملف؛ وإلا فنريد الشيفرة أن تهلع باستخدام panic!‎ إذا كان السبب مختلفًا -مثل عدم امتلاكنا للأذونات المناسبة- بالطريقة ذاتها في الشيفرة 4، ولتحقيق ذلك نُضيف تعبير match داخلي كما هو موضح في الشيفرة 5. اسم الملف: src/main.rs use std::fs::File; use std::io::ErrorKind; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => match error.kind() { ErrorKind::NotFound => match File::create("hello.txt") { Ok(fc) => fc, Err(e) => panic!("Problem creating the file: {:?}", e), }, other_error => { panic!("Problem opening the file: {:?}", other_error); } }, }; } [الشيفرة 5: التعامل بصورةٍ مختلفة مع أخطاء مختلفة] نوع القيمة التي تعيدها File::open داخل متغاير Err هو io::Error وهو هيكل struct موجود في المكتبة القياسية، ويحتوي هذا الهيكل على التابع kind الذي يمكننا استدعاءه للحصول على القيمة io::ErrorKind. يحتوي المعدّد io::ErrorKind الموجود في المكتبة القياسية على متغايرات تمثل الأنواع المختلفة من الأخطاء التي قد تنتج من عملية io، والمتغاير الذي نريد استخدامه هنا هو ErrorKind::NotFound الذي يشير إلى الملف الذي نحاول فتحه إلا أنه غير موجود بعد، لذا نُطابقه مع greeting_file_result إلا أنه يوجد تعبير match داخلي خاص بالتابع error.kind()‎. الشرط الذي نريد أن نتحقق منه في تعبير match الداخلي هو فيما إذا كانت القيمة المُعادة من error.kind()‎ هي المتغاير NotFound من المعدد ErrorKind، فإذا كان هذا الحال فعلًا فسنحاول إنشاء ملف باستخدام File::create، وإذا فشل File::create أيضًا، فنحن بحاجة ذراع آخر في تعبير match الداخلي، وعندما لا يكون من الممكن إنشاء الملف تُطبع رسالة خطأ مختلفة. تبقى ذراع match الخارجية الثانية كما هي حتى يهلع البرنامج عند حدوث أي خطأ ما عدا خطأ عدم العثور على الملف. بدائل لاستخدام match مع Result‎ استخدمنا كثيرًا من تعابير match، فهي مفيدةٌ جدًا إلا أنها بدائية، وسنتحدث لاحقًا عن المغلفات closures التي تُستخدم مع الكثير من التوابع المعرّفة في Result<T, E>‎، وقد تكون هذه التوابع أكثر اختصارًا من match عند التعامل مع قيم Result<T, E>‎ في شيفرتك البرمجية. على سبيل المثال، إليك طريقة أخرى لكتابة المنطق ذاته الموضح في الشيفرة 5، ولكن سنستخدم في هذه المرة المغلفات وتابع unwrap_or_else: use std::fs::File; use std::io::ErrorKind; fn main() { let greeting_file = File::open("hello.txt").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|error| { panic!("Problem creating the file: {:?}", error); }) } else { panic!("Problem opening the file: {:?}", error); } }); } على الرغم من أن هذه الشيفرة البرمجية تبدي السلوك ذاته الخاص بالشيفرة 5، إلا أنها لا تحتوي على أي تعبير match وهي أوضح للقراءة. ألقِ نظرةً على التابع unwrap_or_else وكيفية عمله في توثيق المكتبة القياسية إذا أردت وعُد مرةً ثانية لهذا المثال. تغنينا العديد من التوابع الأخرى عن الحاجة لاستخدام تعابير match متداخلة عند التعامل مع الأخطاء. اختصارات للهلع عند حصول الأخطاء باستخدام unwrap و expect يفي استخدام match بالغرض، إلا أن استخدامه يتطلب كتابة مطوّلة ولا يدلّ على الغرض منه بوضوح. بدلًا من ذلك يحتوي النوع Result<T, E>‎ العديد من التوابع المساعدة المعرفة بداخله لإنجاز مهام متعددة ومحددة، إذ يمثّل التابع unwrap مثلًا تابعًا مختصرًا يؤدي مهمّة التعبير match الذي كتبناه في الشيفرة 4، فإذا كانت قيمة Result هي المتغاير Ok، فسيعيد القيمة الموجودة في Ok وإلا إذا احتوى على المتغاير Err، فسيستدعي الماكرو panic!‎. إليك مثالًا عمليًا على unwrap: اسم الملف: src/main.rs use std::fs::File; fn main() { let greeting_file = File::open("hello.txt").unwrap(); } إذا نفذت الشيفرة البرمجية السابقة دون وجود الملف hello.txt، سنحصل على رسالة خطأ من استدعاء panic!‎ بسبب التابع unwrap: thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:49 يسمح لنا التابع expect باختيار رسالة خطأ panic!‎ بصورةٍ مشابهة، كما يمكن أن ينقل استخدام expect بدلًا من unwrap وتقديم رسالة خطأ معبّرة قصدك جيدًا، مما يساعدك في تعقب مصدر الهلع بصورةٍ أفضل. يمكننا استخدام expect على الشكل التالي: اسم الملف: src/main.rs use std::fs::File; fn main() { let greeting_file = File::open("hello.txt") .expect("hello.txt should be included in this project"); } نستخدم expect كما نستخدم unwrap، إما لإعادة مقبض الملف أو لاستدعاء الماكرو panic!‎. تمثل رسالة الخطأ التي سترسل باستخدام expect لاستدعاء panic!‎ معاملًا يُمرّر إلى expect بدلًا من رسالة panic!‎ الافتراضية التي يستخدمها التابع unwrap، إليك ما ستبدو عليه الرسالة: thread 'main' panicked at 'hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:10 يختار معظم مبرمجي لغة رست عند كتابة شيفرة برمجية مُخرجة جيدًا التابع expect بدلًا من unwrap لمنح السياق المناسب عن سبب نجاح العملية دومًا، ويمكنك بهذه الطريقة الحصول على معلومات أكثر لتستخدمها في تنقيح الأخطاء في حال كانت افتراضاتك خاطئة. نشر الأخطاء يُمكنك إعادة الخطأ الناتج عن استدعاء دالةٍ ما شيئًا قد يفشل إلى الشيفرة البرمجية المُستدعية له للتعامل مع الخطأ بدلًا من التعامل مع الخطأ داخل الدالة نفسها، وهذا يُعرف بنشر propagating الخطأ ويُعطي تحكمًا أكبر بالشيفرة البرمجية التي استدعت هذا الخطأ، إذ يمكننا توفير المزيد من المعلومات أو المنطق الذي يتعامل مع الخطأ بصورةٍ أفضل عما هو موجود في سياق شيفرتك البرمجية. على سبيل المثال، ألقِ نظرةً على الشيفرة 6 التي تقرأ اسم مستخدم من ملف، وتُعيد الدالة خطأ عدم وجود الملف أو عدم القدرة على قرائته إلى الشيفرة البرمجية التي استدعت الدالة. اسم الملف: src/main.rs #![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let username_file_result = File::open("hello.txt"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e), }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e), } } } [الشيفرة 6: دالة تعيد الأخطاء إلى الشيفرة البرمجية التي استدعتها باستخدام تعبير match] يُمكن كتابة هذه الدالة بطريقة أقصر إلا أننا سنبدأ بكتابة معظمها يدويًا حتى نفهم التعامل مع الأخطاء أكثر، ثم سننظر إلى الطريقة الأقصر. دعنا ننظر إلى النوع المُعاد من الدالة أولًا ألا وهو Result<String, io::Error>‎ وهذا يعني أن الدالة تُعيد قيمةً من النوع Result< T, E>‎، إذ يُملأ النوع المعمّم T بالنوع String، بينما يُملأ النوع المعمم E بالنوع io::Error. تحصل الشيفرة البرمجية التي استدعت الدالة في حال عمل الدالة دون أي مشاكل على القيمة Ok التي تخزِّن داخلها قيمةً من النوع String ألا وهو اسم المستخدم الذي قرأته الدالة من الملف، وإذا واجهت الدالة خلال عملها أي خطأ، تحصل الشيفرة البرمجية التي استدعت الدالة على القيمة Err التي تخزن داخلها نسخةً من io::Error تحتوي على مزيدٍ من المعلومات حول المشاكل التي جرت. اخترنا io::Error نوعًا للقيمة المُعادة لأنه يوافق نوع قيمة الخطأ المُعاد من كلا العمليتين التي نستدعي فيهما الدالة اللتان قد تفشلان ألا وهما الدالة File::open والتابع read_to_string. يبدأ متن الدالة باستدعاءٍ للدالة File::open، ثمّ نتعامل مع القيمة Result في match بطريقة مماثلة للتعبير match في الشيفرة 4؛ فإذا نجح عمل الدالة File::open يصبح مقبض الملف في متغير النمط file بقيمة المتغير القابل للتغيير username_file ويستمر تنفيذ الدالة، إلا أننا نستخدم الكلمة المفتاحية return في حالة Err عوضًا عن استدعاء panic! للخروج من الدالة وتمرير قيمة الخطأ الناتجة عن File::open في متغير النمط e إلى الشيفرة البرمجية التي استدعت الدالة. إذًا، تُنشئ الدالة قيمة String جديدة في المتغير username إذا كان لدينا مقبض ملف في username_file، ثم تستدعي التابع read_to_string باستخدام مقبض الملف في المتغير username_file لقراءة محتويات الملف إلى المتغير username. يعيد التابع read_to_string قيمة Result أيضًا لأنها من الممكن أن تفشل على الرغم من نجاح File::open، لذا فنحن بحاجة تعبير match آخر للتعامل مع قيمة Result على النحو التالي: تنجح دالتنا إذا نجح التابع read_to_string ونُعيد اسم المستخدم من الملف الموجود في username والمغلّف بقيمة Ok، وإلا إذا فشل read_to_string نُعيد قيمة الخطأ بطريقة إعادة الخطأ ذاتها في match التي تعاملت مع القيمة المُعادة من File::open، إلا أننا لسنا بحاجة كتابة الكلمة المفتاحية return هنا لأن هذا هو آخر تعبير في الدالة. ستتعامل الشيفرة البرمجية التي تستدعي هذه الشيفرة البرمجية مع حالة الحصول على قيمة Ok تحتوي على اسم مستخدم، أو قيمة Err تحتوي على قيمة من النوع io::Error، ويعود اختيار الإجراء المُتّخذ إلى الشيفرة البرمجية التي استدعت الدالة، فيمكن للشيفرة البرمجية أن تستدعي الماكرو panic!‎ وأن توقف البرنامج فورًا في حال الحصول على قيمة Err، أو استخدام اسم مستخدم افتراضي، أو البحث على اسم المستخدم في مكان آخر عوضًا عن الملف. لا نمتلك ما يكفي من المعلومات حول الشيء الذي ستفعله الشيفرة البرمجية التي استدعت الدالة، لذا فنحن ننشر معلومات الخطأ أو النجاح للشيفرة البرمجية للتعامل معها بصورةٍ مناسبة. يُعد نمط نشر الأخطاء هذا شائع جدًا في رست، وتقدم لنا رست عامل إشارة الاستفهام ? لاستخدام هذا النمط بسهولة. اختصار لنشر الأخطاء: عامل ? توضح الشيفرة 7 تطبيقًا للدالة read_username_from_file بوظيفة مماثلة للشيفرة 6، إلا أننا نستخدم هنا العامل ?. اسم الملف: src/main.rs #![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut username_file = File::open("hello.txt")?; let mut username = String::new(); username_file.read_to_string(&mut username)?; Ok(username) } } [الشيفرة 7: دالة تعيد أخطاء للشيفرة البرمجية المُستدعية باستخدام العامل ?] العامل ? الموجود بعد القيمة Result مُعرَّف بحيث يعمل بطريقة مماثلة لعمل تعابير match التي عرفناها سابقًا بهدف التعامل مع قيم Result المختلفة في الشيفرة 6، فإذا كانت القيمة Result هي Ok تُعاد القيمة داخل Ok من التعبير هذا ويستمر تنفيذ البرنامج، بينما إذا كانت القيمة Err فتُعاد القيمة الموجودة داخل Err من الدالة ككُل وكأننا استخدمنا الكلمة المفتاحية return وبالتالي تُنشر قيمة الخطأ إلى الشيفرة البرمجية التي استدعت الدالة. هناك فرقٌ ما بين ما يفعله التعبير match في الشيفرة 6 وبين ما يفعله العامل ?؛ إذ أن الأخطاء التي تُستدعى عن طريقة العامل ? تمرّ بدالة from، المعرّفة في السمة From في المكتبة القياسية التي تُستخدم لتحويل القيم من نوع إلى نوع آخر؛ فعندما يستدعي العامل ? الدالة from يُحوَّل الخطأ المُتلقى إلى نوع الخطأ المعرف في نوع القيمة المُعادة ضمن الدالة الحالية، وهذا الأمر مفيد عندما تُعيد الدالة نوعًا واحدًا من الخطأ لتمثيل جميع حالات فشل الدالة، حتى لو كانت الأجزاء التي قد تفشل ضمن الدالة تفشل لأسباب مختلفة. على سبيل المثال، يمكننا التعديل على الدالة read_username_from_file في الشيفرة 7 لتُعيد نوع خطأ مخصص نعرّفه اسمه OurError. إذا عرفنا أيضًا impl From<io::Error> for OurError لإنشاء نسخة من OurError من io::Error فهذا يعني أن العامل ? المُستدعى في متن الدالة read_username_from_file سيستدعي from ويحوّل أنواع الأخطاء دون الحاجة لكتابة شيفرة برمجية إضافية لهذا الغرض. في سياق الشيفرة 7: سيُعيد العامل ? في نهاية استدعاء File::open القيمة الموجودة داخل Ok إلى المتغير username_file، وإذا حدث خطأ ما فسيعيد العامل ? قيمةً من Err إلى الشيفرة التي استدعت الدالة وتوقف تنفيذ الدالة مبكرًا، وينطبق الأمر ذاته على العامل ? في نهاية استدعاء read_to_string. يُغنينا العامل ? عن كتابة أي شيفرات برمجية متكررة ويجعل من كتابة الدالة عمليةً أسهل وأسرع، إلا أنه يمكننا جعل اللشيفرة البرمجية هذه أقصر أكثر عن طريق كتابة استدعاءات التوابع الواحدة تلو الأخرى مباشرةً بعد العامل ? كما هو موضح في الشيفرة 8. اسم الملف: src/main.rs use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut username = String::new(); File::open("hello.txt")?.read_to_string(&mut username)?; Ok(username) } [الشيفرة 8: كتابة استدعاءات التوابع بصورة متسلسلة بعد العامل ?] نقلنا عملية إنشاء String الجديد في username إلى بداية الدالة، ولم نغيّر ذلك الجزء، وبدلًا من إنشاء متغير username_file، كتبنا استدعاء read_to_string قبل نتيجة File::open("hello.txt:)?‎ مباشرةً، إلا أن العامل ? ما زال موجودًا في نهاية استدعاء read_to_string وما زلنا نُعيد قيمة Ok تحتوي على username عندما تنجح كل من File::open و read_to_string بدلًا من إعادة الأخطاء. وظيفة الشيفرة البرمجية مماثلة لكل من الشيفرة 6 والشيفرة 7 إلا أن الفارق هنا أن الشيفرة هذه أكثر سهولة للكتابة. توضح الشيفرة 9 طريقةً أكثر اختصارًا باستخدام fs::read_to_string. اسم الملف: src/main.rs use std::fs; use std::io; fn read_username_from_file() -> Result<String, io::Error> { fs::read_to_string("hello.txt") } [الشيفرة 9: استخدام fs::read_to_string بدلًا من فتح وقراءة الملف بخطوتين منفصلتين] تُعد قراءة محتويات ملف ما وإسنادها إلى سلسلة نصية عمليةً شائعةً جدًا، لذا تُقدّم المكتبة القياسية دالةً لتحقيق هذه العملية ألا وهي fs::read_to_string، إذ تفتح الملف ومن ثم تُنشئ سلسلةً نصيةً String جديدة وتقرأ محتويات الملف وتُسندها إلى السلسلة النصية وتُعيد السلسلة النصية String أخيرًا، إلا أن استخدام fs::read_to_string لا يُتيح لنا إمكانية شرح جميع حالات التعامل مع الأخطاء، وهذا السبب في تقديمنا للطريقة الأطول أولًا. أماكن استخدام العامل ? يُمكن استخدام العامل ? فقط في الدوال التي تُعيد نوعًا متوافقًا مع العامل ?، وذلك لأن العامل ? معرّف ليُجري عملية إعادة لقيمة بصورةٍ مبكرة خارج الدالة بطريقة تعبير match ذاتها الذي عرفناه في الشيفرة 6. نلاحظ في الشيفرة 6 أن match استخدم القيمة Result وأعاد الذراع القيمة Err(e)‎، ينبغي على النوع المُعاد من الدالة أن يكون Result لكي يكون متوافقًا مع التعليمة return هذه. دعنا ننظر في الشيفرة 10 إلى الخطأ الذي سنحصل عليه في حال استخدمنا العامل ? في الدالة main بنوع مُعاد غير متوافق مع الأنواع الخاصة بالعامل ?: اسم الملف: src/main.rs use std::fs::File; fn main() { let greeting_file = File::open("hello.txt")?; } [الشيفرة 10: محاولة استخدام العامل ? في الدالة main التي تُعيد () وهي قيمة غير متوافقة] تفتح الشيفرة البرمجية السابقة ملفًا، وقد تفشل عملية فتحه. يتبع العامل ? القيمة Result المُعادة من File::open إلا أن الدالة main تحتوي على النوع المُعاد () وليس Result، وعندما نصرّف الشيفرة البرمجية السابقة نحصل على رسالة الخطأ التالية: $ cargo run Compiling error-handling v0.1.0 (file:///projects/error-handling) error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`) --> src/main.rs:4:48 | 3 | / fn main() { 4 | | let greeting_file = File::open("hello.txt")?; | | ^ cannot use the `?` operator in a function that returns `()` 5 | | } | |_- this function should return `Result` or `Option` to accept `?` | = help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()` For more information about this error, try `rustc --explain E0277`. error: could not compile `error-handling` due to previous error يشير هذا الخطأ إلى أنه من غير المسموح استخدام العامل ? إلا في دالة تُعيد Result، أو Option، أو أي نوع آخر يطبّق FromResidual. يوجد خياران لإصلاح الخطأ السابق: الأول هو تغيير نوع القيمة المُعادة من الدالة لتصبح متوافقةً مع القيمة التي تستخدم العامل ? عليها وهذا خيار جيّد طالما لا يوجد أي قيود أخرى تمنعك من ذلك، أما الخيار الثاني فهو باستخدام match أو إحدى توابع Result<T, E>‎ للتعامل مع Result<T, E>‎ بالطريقة المناسبة. ذكرت رسالة الخطأ أيضًا أنه يمكننا استخدام العامل ? مع قيم Option<T>‎ أيضًا بالطريقة ذاتها التي نستخدم فيها العامل مع Result، إلا أنه يمكنك استخدام العامل على Option فقط في حال كانت الدالة تُعيد Option. يشبه سلوك العامل ? عند استدعائه على Option<T, E>‎ سلوكه عند استدعائه على Result<T, E>‎، إذ تُعاد القيمة None كما هي مبكرًا من الدالة، وإذا كانت القيمة Some فالقيمة التي بداخل Some هي القيمة الناتجة عن ذلك التعبير، وتستمر الدالة عندها بالتنفيذ. تحتوي الشيفرة 11 على مثال لدالة تعثر على المحرف الأخير من السطر الأول في سلسلة نصية. fn last_char_of_first_line(text: &str) -> Option<char> { text.lines().next()?.chars().last() } [الشيفرة 11: استخدام العامل ? على قيمة Option<T>‎] تُعيد الدالة Option<char>‎ لأنه من الممكن أن يكون هناك محرف في النتيجة أو لا. تأخذ الشيفرة البرمجية السابقة شريحة السلسلة النصية string slice‏ text وسيطًا، وتستدعي التابع lines عليها مما يُعيد مُكرّرًا iterator عبر السطور في السلسلة النصية. ولأن هذه الدالة تهدف لفحص السطر الأول فهي تستدعي next على المكرر للحصول على القيمة الأولى منه، وإذا كان text سلسلة نصية فارغة فسيُعيد استدعاء next القيمة None وفي هذه الحالة نستخدم العامل ? لإيقاف التنفيذ وإعادة القيمة None من الدالة last_char_of_first_line. إذا لم يكن text سلسلةً نصيةً فارغة فسيُعيد استدعاء next قيمة Some تحتوي على شريحة سلسلة نصية تحتوي على السطر الأول من text. يستخلص العامل ? شريحة السلسلة النصية ويمكننا استدعاء chars على شريحة السلسلة النصية للحصول على مكرّر يحتوي على محارفه. ما نبحث عنه هنا هو المحرف الأخير من السطر الأول، لذلك نستدعي last للحصول على آخر عنصر موجود في المكرر وهي قيمة Option لأنه من الممكن أن يكون السطر الأول سلسلة نصية فارغة، إذا من الممكن مثلًا أن يبدأ text بسطر فارغ وأن يحتوي على محارف في السطور الأخرى مثل "‎\nhi"، فإذا كان هناك فعلًا محرف في نهاية السطر فإننا نحصل عليه داخل متغاير Some. يُعطينا العامل ? الموجود في المنتصف طريقة موجزة للتعبير عن هذا المنطق مما يسمح لنا بتطبيق محتوى الدالة بسطر واحد، وإذا لم نستطع تطبيق العامل ? على Option، سيتوجب علينا كتابة المنطق ذاته باستخدام عدد أكبر من استدعاءات للدوال أو باستخدام التعبير match. لاحظ أنه يمكنك استخدام العامل ? على Result داخل دالة تُعيد Result، ويمكنك استخدام العامل ? على Option داخل دالة تُعيد Option إلا أنه لا يمكنك الخلط ما بين الاثنين، إذ لن يحول العامل ? النوع Result إلى Option أو بالعكس تلقائيًا، ويمكنك في هذه الحالات استخدام توابع، مثل التابع ok على النوع Result، أو التابع ok_or على النوع Option لإجراء التحويل صراحةً. استخدمت جميع دوال main حتى هذه اللحظة القيمة المُعادة (). تُعد الدالة mainمميزةً لأنها نقطة بداية ونهاية البرامج التنفيذية وبالتالي هناك بعض القيود على الأنواع المُعادة لكي تتصرف البرامج على النحو الصحيح كما هو متوقع. لحسن الحظ، تُعيد الدالة main النوع Result<(), E>‎. تحتوي الشيفرة 12 على الشيفرة البرمجية الموجودة في الشيفرة 10، إلا أننا عدلنا النوع المُعاد من الدالة main ليصبح Result<(), Box<dyn Error>>‎ وأضفنا قيمة مُعادة Ok(())‎ في النهاية، وستُصرَّف الشيفرة البرمجية نتيجةً لهذه التعديلات بنجاح: use std::error::Error; use std::fs::File; fn main() -> Result<(), Box<dyn Error>> { let greeting_file = File::open("hello.txt")?; Ok(()) } [الشيفرة 12: تعديل الدالة main لتُعيد Result<(), E>‎ لتسمح لنا باستخدام العامل ? على قيم Result] النوع Box<dyn Error>‎ هو كائن سمة trait object وهو ما سنغطيه لاحقًا، ويكفي الآن معرفتك أن Box<dyn Error>‎ يعني "أي نوع من الأخطاء". استخدام العامل ? على قيمة Result في دالة main باستخدام قيمة الخطأ Box<dyn Error>‎ هو أمر مسموح، وذلك لأنه يسمح لأي نوع Err أن يُعاد مبكرًا، وعلى الرغم من أن محتوى الدالة main سيعيد الأخطاء من النوع std::io::Error فقط إلا أن بصمة الدالة ستبقى صالحة بتحديد Box<dyn Error>‎ إذا أُضيفت شيفرة برمجية تُعيد أخطاء أخرى داخل الدالة main. يتوقف الملف التنفيذي عندما تُعيد الدالة main القيمة Result<(), E>‎ وذلك بإعادة القيمة "0" إذا أعادت main القيمة Ok(())‎ وقيمة غير صفرية إذا أعادت الدالة قيمة Err. تُعيد الملفات التنفيذية المكتوبة بلغة سي أعدادًا صحيحة عند مغادرة البرنامج؛ فالبرنامج الذي يتوقف بنجاح يُعيد العدد الصحيح "0"؛ بينما يُعيد البرنامج الذي يتوقف بسبب خطأ قيمة عدد صحيح لا تساوي "0". تُعيد رست أيضًا أعدادًا صحيحة من الملفات التنفيذية بصورةٍ مماثلة لهذا الاصطلاح. قد تُعيد الدالة main أي نوع يطبّق السمة std::process::Termination التي تحتوي بدورها على دالة تدعى report تُعيد قيمة ExitCode، انظر إلى توثيق المكتبة القياسية للمزيد من المعلومات حول استخدام سمة Termination ضمن أنواعك. الآن، بعد مناقشتنا التفاصيل الخاصة بالماكرو panic!‎ وإعادة النوع Result، سنعود تاليًا إلى موضوع كيفية تحديد الاستخدام المناسب لكل حالة. ترجمة -وبتصرف- لقسم من الفصل Error Handling من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: كيفية استخدام النوع HashMap لتخزين البيانات في رست Rust أنواع البيانات Data Types في لغة رست Rust الحزم packages والوحدات المصرفة crates في لغة رست Rust الأخطاء السبع القاتلة لأيّ مشروع برمجيات
  14. نستعرض في هذا المقال آخر التجميعات الشائعة في رست التي تحدثنا عنها سابقًا خلال رحلتنا في سلسلة البرمجة بلغة رست ألا وهي الخريطة المعمّاة hash map. الخريطة المعماة HashMap يخزّن النوع HashMap<K, V>‎ ربطًا ما بين القيم من النوع K التي تمثل المفاتيح والقيم من النوع V التي تمثل القيم باستخدام دالة التعمية hashing function، التي تحدد أين يجب وضع هذه المفاتيح والقيم في الذاكرة. تدعم كثيرًا من لغات البرمجة هذا النوع من هيكل البيانات إلا أنها غالبًا ما تستخدم اسمًا مختلفًا مثل النوع hash، أو خريطة map، أو كائن object، أو جدول hash، أو قاموس dictionary، أو مصفوفة مترابطة associative array والقائمة تطول. الخرائط المعمّاة مفيدة عندما تريد البحث عن بيانات دون استخدام دليل لها كما هو الحال في الأشعة vectors وإنما باستخدام مفتاح key قد يكون من أي نوع بيانات. على سبيل المثال، يمكنك تتبع نتيجة كل فريق في لعبة ما باستخدام الخريطة المعمّاة باستخدام اسم الفريق مفتاحًا للقيمة ونتيجة كل فريق مثل قيم، ويمكنك بالتالي الحصول على نتيجة الفريق باستخدام اسمه. سنستعرض مبادئ الواجهة البرمجية الخاصة بالخرائط المعمّاة في هذا المقال، إلا أن هناك المزيد من الأشياء المثيرة للاهتمام الموجودة ضمن الدوال المُعرّفة في HashMap<K, V>‎ ضمن المكتبة القياسية، ولكن عليك التحقق من توثيق المكتبة القياسية إذا أردت المزيد من التفاصيل. إنشاء خريطة معماة HashMap جديدة إحدى الطرق لإنشاء خريطة معمّاة فارغة هي باستخدام new وإضافة العناصر باستخدام insert. نتابع في الشيفرة 20 نتيجة فريقّين اسمهما أزرق Blue وأصفر Yellow، إذ يبدأ الفريق الأزرق بعشر نقاط والفريق الأصفر بخمسين نقطة. use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); [الشيفرة 20: إنشاء خريطة معماة جديدة وإضافة بعض المفاتيح والقيم إليها] لاحظ أننا بحاجة كتابة use لتضمين HashMap من الجزء الذي يحتوي على التجميعات من المكتبة القياسية، وهذه هي التجميعة الأقل استخدامًا من التجميعات الثلاث الشائعة التي تكلمنا عنها، لذا فهي غير مضمّنة تلقائيًا في مقدمة البرنامج، إضافةً لما سبق، تملك الخرائط المعمّاة دعمًا أقل في المكتبة القياسية من التجميعات الأخرى وليس هناك ماكرو موجود مسبقًا لإنشاء خريطة معمّاة على سبيل المثال. تخزّن الخرائط المعماة البيانات في الكومة heap كما هو الحال في الأشعة، إذ تحتوي الخريطة المُعمّاة السابقة على مفاتيح من النوع String وقيم من النوع i32، وكما هو الحال في الأشعة أيضًا فإن الخرائط المعماة متجانسة، بمعنى أن جميع المفاتيح يجب أن تكون من نفس النوع وكذلك الحال بالنسبة للقيم. الوصول إلى القيم في الخريطة المعماة يمكننا الحصول على قيمة في الخريطة المعماة باستخدام مفتاحها وتابع get كما هو موضح في الشيفرة 21. fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); let team_name = String::from("Blue"); let score = scores.get(&team_name).copied().unwrap_or(0); } [الشيفرة 21: الوصول إلى نتيجة الفريق الأزرق المخزنة في الخريطة المعمّاة] سيأخذ score القيمة المرتبطة مع الفريق الأزرق وستكون النتيجة 10. يُعيد التابع get قيمةً من النوع Option<&V>‎ وبالتالي إذا لم يكن هناك أي قيمة للمفتاح المحدد في الخريطة المعمّاة، فسيعيد التابع get القيمة None. يتعامل هذا البرنامج مع Option باستدعاء unwrap_or لضبط score إلى صفر إذا كان score لا يحتوي على قيمة للمفتاح المحدّد. يمكننا المرور على كل زوج مفتاح/قيمة في الخريطة المعمّاة بطريقة مشابهة لما نفعل في الأشعة عن طريق حلقة for: use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); for (key, value) in &scores { println!("{}: {}", key, value); } ستطبع الشيفرة البرمجية السابقة كل زوج بترتيب عشوائي: Yellow: 50 Blue: 10 الخرائط المعماة والملكية تُنسخ القيم التي تطبّق السمة Copy، مثل i32 إلى الخريطة المعماة؛ بينما تُنقل القيم للأنواع المملوكة، مثل String وتصبح الخريطة المعماة مالكةً لهذه القيم كما هو موضح في الشيفرة 22. use std::collections::HashMap; let field_name = String::from("Favorite color"); let field_value = String::from("Blue"); let mut map = HashMap::new(); map.insert(field_name, field_value); // ‫يصبح كل من field_name و field_value غير صالح بحلول هذه النقطة // حاول استخدامهما وانظر إلى خطأ المصرف الذي ستحصل عليه [الشيفرة 22: توضيح أن المفاتيح والقيم تصبح مملوكة من قبل الخريطة المعماة فور إدخالها إليها] لا يمكننا استخدام القيمتين field_name و field_value بعد نقلهما إلى الخريطة المعماة باستدعاء التابع insert. لن تُنقل القيم إلى الخريطة المعماة في حال أضفنا مراجعًا القيم، إلا أن القيم التي تُشير إليها هذه المراجع يجب أن تكون صالحة طالما الخريطة المعماة صالحة على أقل تقدير، وسنتحدث مفصلًا عن هذه المشاكل لاحقًا. تحديث قيم خريطة معماة على الرغم من كون أزواج القيمة والمفتاح قابلة للزيادة إلا أنه يجب أن يقابل كل مفتاح فريد قيمة واحدة فقط (العكس ليس صحيحًا، على سبيل المثال قد تكون نتيجة كل من الفريق الأزرق والأصفر 10 في الخريطة المعماة scores في الوقت ذاته). عليك أن تحدد التصرف الذي ستفعله عند تعديل القيم الموجودة في الخريطة المعمّاة والوصول إلى مفتاح له قيمة مسبقة، إذ يمكنك في هذه الحالة استبدال القيمة القديمة بالقيمة الجديدة وإهمال القيمة القديمة كليًا، أو تستطيع جمع القيمة القديمة مع القيمة الجديدة. دعنا ننظر إلى كيفية تحقيق كلا الطريقتين. الكتابة على القيمة إذا أدخلنا مفتاحًا وقيمةً إلى خريطة معمّاة، ثمّ أدخلنا المفتاح ذاته مع قيمة مختلفة، ستُستبدل القيم المرتبطة بذلك المفتاح. على الرغم من أن الشيفرة 23 تستدعي التابع insert مرتين، إلا أن الخريطة المعماة الناتجة ستحتوي على زوج مفتاح/قيمة واحد لأننا أدخلنا قيمةً لمفتاح الفريق الأزرق في المرتين. use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Blue"), 25); println!("{:?}", scores); [الشيفرة 23: استبدال قيمة مخزّنة في خريطة معمّاة باستخدام مفتاحها] ستطبع الشيفرة البرمجية السابقة ‎{"Blue": 25}، إذ كُتِبَ على القيمة 10 السابقة القيمة 25. إضافة مفتاح وقيمة فقط في حال عدم وجود المفتاح من الشائع أن نكون بحاجة للتحقق من مفتاح فيما إذا كان موجود مسبقًا أم لا في خريطة المعمّاة مع قيمة ما، ومن ثم إجراء التالي: نبقي القيمة الموجود كما هي إذا كان المفتاح موجودًا في الخريطة المعماة، وإلا فنضيف المفتاح مع القيمة إذا لم نجد المفتاح. لدى الخرائط المعماة واجهة برمجية خاصة بتلك العملية وهي التابع entry الذي يأخذ المفتاح التي تريد أن تتحقق منه مثل معامل، ويُعيد معددًا اسمه Entry يمثّل القيمة الموجودة أو غير الموجودة. دعنا نقول بأننا نريد أن نتحقق إذا كان مفتاح الفريق الأصفر يحتوي على قيمة؛ وإذا لم تكن هناك قيمة موجودة، نريد عندها إضافة القيمة 50 وينطبق الأمر ذاته على الفريق الأزرق. تبدو الشيفرة البرمجية باستخدام الواجهة البرمجية الخاصة بالتابع entry كما هو موضح في الشيفرة 24. use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.entry(String::from("Yellow")).or_insert(50); scores.entry(String::from("Blue")).or_insert(50); println!("{:?}", scores); [الشيفرة 24: استخدام التابع entry لإدخال قيمة فقط في حال لا يحتوي المفتاح على قيمة مرتبطة به] التابع or_insert في Entry معرّف ليُعيد مرجعًا قابلًا للتعديل يشير إلى قيمة المفتاح Entry إذا كان المفتاح المحدد موجودًا، وإلا سيدخل قيمة المعامل على أنها قيمة جديدة للمفتاح وسيُعيد مرجعًا قابلًا للتعديل إلى القيمة الجديدة، وهذه الطريقة أفضل من كتابة المنطق ذلك بأنفسنا، كم أنها تعمل بصورةٍ أفضل مع مدقق الاستعارة borrow checker. بتشغيل الشيفرة 24، نحصل على الخرج {Yellow": 50, "Blue": 10"}، إذ سيتسبب الاستدعاء الأول للتابع entry بإضافة مفتاح الفريق الأصفر بالقيمة 50 لأن الفريق الأصفر لا يحتوي على أي قيمة بعد، بينما لن يُغيّر الاستدعاء الثاني للتابع entry الخريطة المعمّاة لأن مفتاح الفريق الأزرق موجود مسبقًا بقيمة 10. تحديث قيمة بحسب قيمتها السابقة حالة أخرى لاستخدام الخرائط المعماة هي بالبحث عن قيمة المفتاح ومن ثم التعديل عليها بناءً على قيمتها القديمة، على سبيل المثال توضح الشيفرة 25 برنامجًا يعدّ عدد مرات ظهور كل كلمة في النص، إذ نستخدم هنا خريطة معماة تحتوي على الكلمات مثل مفاتيح بحيث نزيد قيمة كل كلمة حتى نراقب عدد ظهور كلمة ما، وإذا رأينا الكلمة للمرة الأولى فإننا نضيفها إلى الخريطة بالقيمة 0. use std::collections::HashMap; let text = "hello world wonderful world"; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(0); *count += 1; } println!("{:?}", map); [الشيفرة 25: عدّ عدد مرات ظهور الكلمات باستخدام خريطة معماة تخزّن الكلمة وعدد مرات ظهورها] ستطبع الشيفرة البرمجية {world": 2, "hello": 1, "wonderful": 1"}. قد تجد أزواج قيمة/مفتاح متماثلة بترتيب مختلف عن ذلك، لذلك نذكر هنا أننا تكلمنا في الفقرات السابقة وقلنا أن المرور على قيم الخريطة المعماة يكون بترتيب عشوائي. يُعيد التابع split_whitespace مؤشرًا يشير إلى الشرائح الجزئية ضمن text والمفصول ما بينها بالمسافة، بينما يعيد التابع or_insert مرجعًا قابلًا للتعديل (‎&mut V) إلى قيمة المفتاح المُحدّد، ونخزّن هنا المرجع القابل للتعديل في المتغير count، لذا ومن أجل الإسناد إلى تلك القيمة علينا أولًا أن نُحصّل المتغير count باستخدام رمز النجمة *. تخرج المراجع القابلة للتعديل من النطاق في نهاية الحلقة for، لذا جميع هذه التغييرات آمنة ومسموحة استنادًا لقوانين الاستعارة. دوال التعمية hashing function تستخدم الخريطة المعماة HashMap افتراضيًا دالة تعمية hashing function تدعى SipHash، وهي دالة توفر حمايةً لخرائط التعمية ضد هجمات الحرمان من الخدمة Denial of Service -أو اختصارًا DoS- إلا أنها ليست أسرع خوارزميات التعمية المتاحة ولكنها تقدم حمايةً أفضل، والتراجع في السرعة مقابل ذلك يستحقّ المساومة. إذا راجعت شيفرتك البرمجية ورأيت أن دالة التعمية الاعتيادية بطيئة جدًا مقارنةً بحاجتك، فيمكنك استبدالها بدالة أخرى بتحديد مُعَمّي hasher؛ والمعمّي هو النوع الذي يطبّق السمة BuildHasher وسنناقش السمات بالتفصيل لاحقًا. ليس من الضروري كتابة المعمّي الخاص بك بنفسك، إذ يحتوي crates.io على مكتبات شاركها مستخدمو رست آخرون لتقديم تطبيق معمّي معيّن باستخدام خوارزميات التعمية الشهيرة. ترجمة -وبتصرف- لقسم من الفصل Common Collections من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: تخزين النصوص بترميز UTF-8 داخل السلاسل النصية في لغة رست Rust التحكم بسير تنفيذ برامج راست Rust تخزين لائحة من القيم باستخدام الأشعة Vectors في لغة رست Rust المسارات paths وشجرة الوحدة module tree في رست Rust
  15. أتينا على ذكر السلاسل النصية سابقًا إلا أننا لم ننظر إليها بالتفصيل بعد، إذ يجد متعلمو لغة رست فهم السلاسل النصية صعبًا لثلاثة أسباب رئيسية: ميل رست لاستباق الخطأ قبل حدوثه وعرض رسالة خطأ تدلّ عليه، كما يبدو هيكل بيانات السلاسل النصية أكثر صعوبةً وتعقيدًا مما تبدو عليه، وأخيرًا ترميز UTF-8. قد تجتمع جميع الأسباب الثلاث السابقة وتجعل من الصعب عليك فهم السلاسل النصية إن قدمت من لغات برمجة مختلفة. نناقش السلاسل النصية هنا في سياق التجميعات collections، وذلك لأن السلاسل النصية هي تطبيق لتجميعة من البايتات إضافةً إلى بعض التوابع المفيدة الأخرى، والتي تبرز أهميتها عندما تمثّل هذه البايتات نصًا ما. سنتحدث في هذا المقال عن العمليات على السلاسل النصية String الموجودة في كل نوع تجميعة، مثل إنشاء سلسلة نصية والتعديل عليها وقراءة محتوياتها، كما سنناقش أيضًا الطرق التي تختلف فيها السلاسل النصية عن التجميعات الأخرى وبالأخص استخدام دليل السلسلة النصية للوصول إلى محتوياتها، فهو معقد نظرًا لاختلافه مع ما يفسّره البشر لبيانات سلسلة نصية وما تفسره الحواسيب. ما هي السلسلة النصية؟ دعنا نبدأ أولًا بتعريف ما الذي نقصده عندما نقول سلسلة نصية؛ إذ تمتلك رست نوع سلسلة نصية واحد في أساس اللغة وهو شريحة السلسلة النصية string slice‏ str، الذي نراه عادةً بشكله المختصر ‎&str. تحدثنا سابقًا عن شرائح السلاسل النصية؛ وهي مراجع إلى بعض بيانات السلاسل النصية المرمزة بترميز UTF-8 والمخزنة في مكان آخر. على سبيل المثال، تُخزَّن السلاسل النصية المجرّدة string literals في ملف البرنامج الثنائي وبالتالي فهي شرائح سلسلة نصية. النوع String الموجود في مكتبة رست القياسية بدلًا من وجوده في أساس اللغة، هو نوع سلسلة نصية قابل للتعديل mutable وللنمو growable والامتلاك owned ومُرمَّز بترميز UTF-8. عندما يقول مبرمجو لغة رست "سلسلة نصية" في رست فهم يقصدون إما النوع String أو أنواع شريحة السلسلة النصية ‎&str ولا يقتصر الأمر على واحد من النوعَين. على الرغم من أن هذا المقال يتكلم خاصةً على النوع String إلا أن كلا النوعين مُستخدمان جدًا في مكتبة رست القياسية، وكلٌ من النوع String وشريحة السلسلة النصية مُرمّزَان بترميز UTF-8. إنشاء سلسلة نصية جديدة الكثير من العمليات المتاحة في النوع Vec<T>‎ هي عمليات متاحة في النوع String أيضًا، وذلك لأن النوع String هو تطبيق لمُغلَّف wrapper حول شعاع من البايتات، إضافةً إلى بعض الضمانات والقيود والإمكانات الأخرى. الدالة new هي مثال على دالة تعمل بنفس الطريقة على Vec<T>‎ وعلى String سويًا لإنشاء نسخة instance كما هو موضح في الشيفرة 11. let mut s = String::new(); الشيفرة 11: إنشاء نسخة جديدة وفارغة من النوع String يُنشئ السطر السابق سلسلةً نصيةً جديدةً وفارغة بالاسم s، وبالتالي يمكننا الآن إضافة البيانات إليها، وسنحتاج غالبًا في البداية إلى بيانات تهيئة لتخزينها في السلسلة النصية، ونستخدم لهذا الغرض التابع to_string؛ وهو تابع متاح في أي نوع يطبّق السمة‏ Display وهو ما تفعله السلسلة النصية المجردة، توضح الشيفرة 12 مثالين على ذلك. let data = "initial contents"; let s = data.to_string(); //تعمل هذه الدالة على السلاسل النصية المجردة مباشرةً let s = "initial contents".to_string(); الشيفرة 12: استخدام التابع to_string لإنشاء النوع String من سلسلة نصية مجردة تُنشئ الشيفرة البرمجية السابقة سلسلة نصية تحتوي على بيانات تهيئة initial contents. يمكننا أيضًا استخدام الدالة String::from لإنشاء النوع String من سلسلة نصية مجردة، والشيفرة 13 مكافئة للشيفرة 12 التي تستخدم التابع to_string. let s = String::from("initial contents"); الشيفرة 13: استخدام الدالة String::from لإنشاء النوع String من سلسلة نصية مجردة بما أن السلاسل النصية تُستخدم للعديد من الأشياء، يمكننا استخدام عدة واجهات برمجية عامة لها، إذ تزودنا هذه الواجهات بكثيرٍ من الخيارات، وقد يبدو بعضها مكرّرًا إلا أن جميعها تؤدي غرضًا ما. في هذه الحالة، يؤدي كلًا من String::from و to_string الغرض ذاته، واستخدام أي منهما يعود إلى أسلوبك في كتابة الشيفرات البرمجية. تذكر أن السلاسل النصية تستخدم ترميز UTF-8 وهذا يعني أنه يمكننا تضمين أي بيانات بهذا الترميز، ألقِ نظؤةً على الشيفرة 14. let hello = String::from("السلام عليكم"); let hello = String::from("Dobrý den"); let hello = String::from("Hello"); let hello = String::from("שָׁלוֹם"); let hello = String::from("नमस्ते"); let hello = String::from("こんにちは"); let hello = String::from("안녕하세요"); let hello = String::from("你好"); let hello = String::from("Olá"); let hello = String::from("Здравствуйте"); let hello = String::from("Hola"); الشيفرة 14: تخزين رسالة تحية بلغات مختلفة في سلاسل نصية تحتوي جميع السلاسل النصية السابقة قيمًا صالحة من النوع String. تحديث سلسلة نصية يمكن أن يكبر حجم النوع String وأن تتغير محتوياته بصورةٍ مشابهة للنوع Vec<T>‎ عند إضافة مزيدٍ من البيانات إليه، ويمكنك إضافةً إلى ذلك استخدام العامل + أو الماكرو format!‎ لضمّ concatenate عدّة قيم من النوع String. إضافة قيم إلى السلسلة النصية باستخدام push_str و push يمكننا توسعة حجم النوع String باستخدام التابع push_str لإضافة شريحة سلسلة نصية كما هو موضح في الشيفرة 15. let mut s = String::from("foo"); s.push_str("bar"); الشيفرة 15: إضافة شريحة سلسلة نصية إلى النوع String باستخدام التابع push_str ستحتوي السلسلة النصية s -بعد السطرين البرمجيين السابقين- على foobar، إذ يأخذ التابع push_str شريحة سلسلة نصية لأننا لسنا بحاجة لأخذ ملكية المعامل، على سبيل المثال نريد في الشيفرة 16 أن نكون قادرين على استخدام s2 بعد إضافة محتوياته إلى s1. let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(s2); println!("s2 is {}", s2); الشيفرة 16: استخدام شريحة سلسلة نصية بعد إضافة محتوياتها إلى النوع String إذا أخذ التابع push_str ملكية s2، فلن نكون قادرين على طباعة قيمتها في السطر الأخير، إلا أن الشيفرة البرمجية هنا تعمل كما هو متوقع منها. يأخذ التابع push محرفًا وحيدًا مثل معامل ويُضيفه إلى النوع String، ونُضيف في الشيفرة 17 الحرف "I" إلى النوع String باستخدام التابع push. let mut s = String::from("lo"); s.push('l'); الشيفرة 17: إضافة محرف واحد إلى قيمة من النوع String باستخدام push ستحتوي السلسلة النصية s نتيجةً لما سبق على lol. ضم السلاسل النصية باستخدام العامل + أو الماكرو format!‎ ستحتاج غالبًا إلى ضم سلسلتين نصيتين، وإحدى الطرق لتحقيق ذلك هو باستخدام العامل + كما هو موضح في الشيفرة 18. fn main() { let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // ‎‫لاحظ أن s1 نُقلَت إلى هنا ولا يمكن استخدامها بعد الآن } الشيفرة 18: استخدام العامل + لضم قيمتين من النوع String إلى قيمة أخرى جديدة من النوع String ستحتوي السلسلة النصية s3 بعد تنفيذ الشيفرة السابقة على "Hello, world!‎"، والسبب في عدم كون s1 صالحة بعد عملية الجمع إلى استخدامنا مرجع إلى s2 يعود إلى بصمة التابع method signature الذي استدعيناه عندما استخدمنا العامل +، إذ يستخدم هذا العامل التابع add وتبدو بصمته كما يلي: fn add(self, s: &str) -> String { ستجد التابع add معرفًا في المكتبة القياسية باستخدام الأنواع المعمّاة generics والأنواع المترابطة associated types، إلا أننا استبدلنا هذه الأنواع بأنواع فعلية وهو ما يحدث عند استدعاء هذا التابع بقيمٍ من النوع String (سنناقش الأنواع المعمّاة لاحقًا)، تعطينا بصمة التابع أدلة يجب علينا فهمها لفهم العامل +. أولًا، لدى s2 الرمز & أي أننا نُضيف مرجعًا للسلسلة النصية الثانية إلى السلسلة النصية الأولى، وهذا بسبب المعامل s في الدالة، إذ يمكننا جمع ‎&str إلى النوع String فقط وليس بإمكاننا إضافة قيمتين من النوع String سويًا، ولكن تمهّل، نوع ‎&s2 هو ‎&String وليس ‎&str كما هو موضح في المعامل الثاني للتابع add. إذًا، لمَ تُصرَّف الشيفرة 18 بنجاح؟ السبب في كوننا قادرين على استخدام ‎&s2 في استدعاء add هو أن المصرف هنا قادرٌ على تحويل الوسيط ‎&String قسريًا إلى ‎&str، وعند استدعاء للتابع add، تستخدم رست التحصيل القسري deref corecion الذي يحوّل ‎&s2 إلى ‎&s2[..]‎ (سنناقش التحصيل القسري بمزيدٍ من التفاصيل لاحقًا)، ولأن add لا يأخذ ملكية المعامل s السلسلة s2، ستظل قيمةً صالحةً من النوع String بعد هذه العملية. ثانيًا، يمكننا في بصمة التابع رؤية أن add يأخذ ملكية self، لأن self لا تحتوي على الرمز &، وهذا يعني أن السلسلة s1 في الشيفرة 18 ستُنقل إلى استدعاء add ولن تصبح قيمةً صالحةً بعد ذلك، لذلك يبدو السطر البرمجي let s3 = s1 + &s2;‎ بأنه ينسخ كلا السلسلتين النصيتين ويُنشئ سلسلةً جديدةً، إلا أنه في الحقيقة يأخذ ملكية s1 ويُسند نسخةً من محتويات s2 إلى نهايتها ومن ثم يُعيد ملكية النتيجة؛ أي بكلمات أخرى يبدو أن السطر البرمجي يُنشئ كثيرًا من النُسخ إلا أنه في حقيقة الأمر لا يفعل ذلك، وهذا التطبيق فعال أكثر من النسخ. يصبح سلوك العامل + غير عملي في حال أردنا ضمّ عدة سلاسل نصية في نفس الوقت: let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = s1 + "-" + &s2 + "-" + &s3; بحلول هذه النقطة، ستكون قيمة السلسلة s هي "tic-tac-toe"، إلا أن الأمر صعب المعرفة فورًا باستخدام محارف + و ". يمكننا استخدام الماكرو format!‎ لعمليات ضم السلاسل النصية الأكثر تعقيدًا: let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = format!("{}-{}-{}", s1, s2, s3); نحصل على القيمة "tic-tac-toe" في السلسلة s أيضًا بتنفيذ الشيفرة السابقة، إذ يعمل الماكرو format!‎ على نحوٍ مماثل لعمل الماكرو println!‎ إلا أنه يُعيد قيمةً من النوع String بدلاً من طباعة الخرج على الشاشة. نلاحظ أن الشيفرة البرمجية التي تستخدم الماكرو format!‎ أسهل قراءةً من سابقتها. تستخدم الشيفرة المولّدة عن طريق استخدام الماكرو format!‎ المراجع، لذلك لن يتسبب استدعائها بأخذ ملكية أي من معاملاتها. الحصول على محتويات السلسلة النصية باستخدام الدليل يمكننا الوصول إلى محارف السلسلة النصية في العديد من لغات البرمجة باستخدام دليلها index، وهي عمليةٌ صالحةٌ وشائعة في هذه اللغات، إلا أنك ستحصل على خطأ في رست إذا حاولت الوصول إلى أجزاء من النوع String باستخدام نفس الأسلوب. ألقِ نظرةً على الشيفرة البرمجية غير الصالحة في الشيفرة 19. let s1 = String::from("hello"); let h = s1[0]; الشيفرة 19: محاولة الوصول إلى أجزاء من السلسلة النصية باستخدام دليلها ستتسبب الشيفرة البرمجية السابقة بظهور الخطأ التالي: $ cargo run Compiling collections v0.1.0 (file:///projects/collections) error[E0277]: the type `String` cannot be indexed by `{integer}` --> src/main.rs:3:13 | 3 | let h = s1[0]; | ^^^^^ `String` cannot be indexed by `{integer}` | = help: the trait `Index<{integer}>` is not implemented for `String` = help: the following other types implement trait `Index<Idx>`: <String as Index<RangeFrom<usize>>> <String as Index<RangeFull>> <String as Index<RangeInclusive<usize>>> <String as Index<RangeTo<usize>>> <String as Index<RangeToInclusive<usize>>> <String as Index<std::ops::Range<usize>>> <str as Index<I>> For more information about this error, try `rustc --explain E0277`. error: could not compile `collections` due to previous error تخبرنا رسالة الخطأ بالآتي: لا تدعم السلاسل النصية في رست الفهرسة indexing، ولكن لماذا؟ للإجابة على هذا السؤال دعنا نناقش كيف تخزّن رست السلاسل النصية في الذاكرة. التمثيل الداخلي النوع String في الحقيقة هو مغلّف حول النوع Vec<u8>‎، دعنا نلقي نظرةً على السلسلة النصية التالية المرمزة بترميز UTF-8 من الشيفرة 14: let hello = String::from("Hola"); طول السلسلة النصية len في هذه الحالة هو 4، ما يعني أن الشعاع الذي يخزن السلسلة النصية "Hola" هو بطول 4 بايتات، إذ يأخذ كل حرف من هذه الأحرف 1 بايت عند ترميزه بترميز UTF-8، إلا أن السطر التالي قد يفاجئك (لاحظ أن السلسلة النصية التالية تبدأ بالحرف السيريلي زي Cyrillic letter Ze وليس العدد العربي 3). let hello = String::from("Здравствуйте"); إذا سألناك عن طول السلسلة النصية ستجيب غالبًا 12، إلا أن رست ستجيبك بالقيمة 24 وهو رقم البايتات المطلوبة لترميز السلسلة النصية "Здравствуйте" بترميز UTF-8 وذلك لأن كل محرف يونيكود unicode يأخذ 2 بايت للتخزين، ولذلك استخدام دليل إلى بايت السلسلة النصية لن يعمل دومًا، ولتوضيح ذلك ألقِ نظرةً على شيفرة رست التالية غير الصالحة: let hello = "Здравствуйте"; let answer = &hello[0]; أنت تعلم مسبقًا أن قيمة answer لن تكون الحرف الأول З، إذ تكون قيمة البايت الأول من الحرف 3 عند ترميز السلسلة النصية بترميز UTF-8 هي 208 والثاني قيمته 151 وبالتالي ستكون قيمة answer هي 208 إلا أن القيمة 208 ليست بقيمة صالحة لمحرف، وإعادة القيمة 208 ليست التصرف الذي يترقبه المستخدم غالبًا عند سؤاله عن المحرف الأول من السلسلة النصية، إلا أنها القيمة الوحيدة الموجودة في الدليل 0. لا يريد المستخدمون عادةً الحصول على قيمة البايت حتى لو احتوت السلسلة النصية على أحرف لاتينية، فإذا كانت ‎&"hello"[0]‎ شيفرة برمجية صالحة، فسيعيد ذلك قيمة البايت الممثلة بالقيمة 104 وليس h. يكمن الحل هنا بتفادي إعادة قيمة غير متوقعة تتسبب بالأخطاء وقد لا نستطيع اكتشافها مباشرةً، ولذلك لا تسمح لنا رست بتصريف هذه الشيفرة البرمجية بهدف منع سوء التفاهم المستقبلي الذي قد يحصل خلال عملية التطوير. البايتات والقيم العددية ومجموعات حروف اللغة نقطة أخرى يجب ذكرها عن ترميز UTF-8، ألا وهو وجود ثلاث طرق مختلفة للنظر إلى السلاسل النصية من منظور لغة رست: مثل بايتات أو قيم عددية scalar values أو مجموعات قيم عددية grapheme clusters (التمثيل الأقرب لما نسميه نحن البشر بالأحرف). إذا نظرنا إلى الكلمة الهندية "नमस्ते" المكتوبة بالطريقة الديفاناغارية Devanagari، فهي مُخزّنة على أنها شعاع من نوع u8 تبدو قيمه على الشكل التالي: [224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135] الشعاع حجمه 18 بايت وهو ما يستخدمه الحاسوب لتخزين هذه البيانات، وإذا أردنا النظر إلى القيم على أنها قيم عددية، التي تمثّل نوع char في رست فسنحصل على البايتات كما يلي: ['न', 'म', 'स', '्', 'त', 'े'] هناك ست قيم من النوع char هنا إلا أن المحرف الرابع والسادس لا يمثلان أحرفًا وإنما علامات تشكيل لا تعني أي شيء بمفردهما. أخيرًا، إذا أردنا النظر إلى السلسلة النصية بكونها مجموعات حروف لغة، فسنحصل على تمثيل لما قد ندعوه بأحرف باللغة الهندية: ["न", "म", "स्", "ते"] تقدم لغة رست طرقًا مختلفة لتمثيل بيانات السلسلة النصية وتخزينها بالحاسوب، بحيث يختار كل برنامج التمثيل الذي يناسبه بغض النظر عن اللغة التي كُتب فيها النص. السبب الأخير لكون رست لا تسمح بالوصول إلى النوع String باستخدام الدليل للحصول على محرف معين هو أن عمليات الفهرسة تأخذ تعقيدًا زمنيًا time complexity ثابتًا -مقداره O(1)‎- إلا أنه ليس من الممكن ضمان ذلك الأداء مع النوع String لأن رست ستكون بحاجة للنظر إلى محتويات السلسلة النصية من البداية إلى الدليل المُحدّد لتحديد عدد المحارف الصالحة الموجودة. شرائح السلاسل النصية لا يُعد استخدام الأدلة مع السلاسل النصية فكرةً جيدةً كما أوضحنا سابقًا، إذ أن القيمة المُعادة من عملية الفهرسة ليست واضحة، فهل ستكون قيمة بايت أم محرف أم مجموعة حروف لغة أم شريحة سلسلة نصية. ستسألك رست أن تكون دقيقًا إذا أردت استخدام الأدلة للحصول على شريحة سلسلة نصية. بدلًا من استخدام الأقواس [] مع رقم وحيد، يمكنك استخدام الأقواس [] لتحديد نطاق معين يحتوي على الشريحة النصية بعدد محدد من البايتات: let hello = "Здравствуйте"; let s = &hello[0..4]; ستكون s هنا هي str& تحتوي على أول 4 بايتات من السلسلة. ذكرنا سابقًا أن كل حرف من هذه الأحرف هو بحجم 2 بايت، وهذا يعني أن s ستكون "Зд". إذا حاولنا تقسيم جزء واحد من بايتات المحرف مثل [hello[0..1&، ستصاب رست بالهلع أثناء التشغيل بنفس الطريقة التي تحدث عند الوصول إلى دليل غير صالح في شعاعٍ ما، كما هو موضح في الخطأ التالي: $ cargo run Compiling collections v0.1.0 (file:///projects/collections) Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/collections` thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', library/core/src/str/mod.rs:127:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ينبغي استخدام المجالات لإنشاء شرائح سلسلة نصية بحذر، لأن ذلك سيؤدي إلى تعطُّل برنامجك. توابع التكرار على السلاسل النصية أفضل طريقة للعمل على قطع pieces من السلاسل النصية هي أن تكون واضحًا فيما إذا كنت تريد أحرفًا أم بايتات، فمن أجل قيم عددية مفردة مُرمزة بالترميز الموحد يونيكود Unicode، استخدم التابع chars، إذ أن استدعاء هذا التابع على السلسلة "Зд" سيفصل المحرفين ويعيد قيمتان من النوع char، ويمكنك تكرار النتيجة للوصول إلى كل عنصر لوحده: #![allow(unused)] fn main() { for c in "Зд".chars() { println!("{}", c); } } ستطبع الشيفرة البرمجية ما يلي: З д يمكننا استخدام التابع bytes بديلًا عمّا سبق وهو تابع يُعيد كل بايتًا كاملًا، إلا أنه قد يكون غير مناسبًا لاستخدامك: for b in "Зд".bytes() { println!("{}", b); } تطبع الشيفرة البرمجية السابقة الخرج التالي: 208 151 208 180 تأكد من تذكرك أن القيمة العددية الصالحة ليونيكود قد تتألف من أكثر من بايت واحد. الحصول على مجموعة حروف لغة من السلاسل النصية كما حدث في مثال الأحرف الديفاناغارية غير موجود في المكتبة القياسية لأنه معقّد، إلا أن هناك العديد من الصناديق crates crates.io التي تساعدك للحصول على النتيجة المرجوة. السلاسل النصية ليست بسيطة لنلخّص ما سبق، السلاسل النصية معقدة، وتتخذ لغات البرمجة المختلفة خيارات مختلفة لتقديم وتبسيط هذا التعقيد للمبرمج، واختارت رست هنا التقيد بالسلوك الموجود للتعامل مع بيانات من النوع String سلوكًا افتراضيًا لكل برامجها، ما يعني أنه على المبرمج التفكير مليًا عند التعامل مع بيانات بترميز UTF-8، وسلبيات هذا السلوك هو كشف تعقيد السلاسل النصية بوضوح أكثر من لغات البرمجة الأخرى، إلا أن هذا السلوك يُعفيك من الحاجة إلى التعامل مع الأخطاء المتعلقة بالمحارف التي لا تنتمي إلى آسكي ASCII لاحقًا ضمن دورة حياة التطوير. الخبر الجيد هنا هو أن المكتبة القياسية تقدم العديد من المزايا المبنية على كل من النوعين String و ‎&str لمساعدتنا في التعامل مع الحالات المعقدة بصورةٍ صحيحة. ألقِ نطرةً على التوثيق في حال أردت استخدام توابع مفيدة مثل contains للبحث في سلسلة نصية و replace لاستبدال أجزاء من السلسلة النصية بسلسلة نصية أخرى. دعنا ننتقل إلى شيء أقل تعقيدًا، ألا وهو الخرائط المُعمّاة Hash maps. ترجمة -وبتصرف- لقسم من الفصل Common Collections من كتاب The Rust Programming Language اقرأ أيضًا المقال السابق: تخزين لائحة من القيم باستخدام الأشعة Vectors في لغة رست Rust التحكم بسير تنفيذ برامج راست Rust كيفية كتابة الدوال Functions والتعليقات Comments في لغة راست Rust المتغيرات والتعديل عليها في لغة رست
  16. سننظر أولًا إلى نوع التجميعة Vec<T>‎، المعروف أيضًا باسم الشعاع vector، إذ تسمح لك الأشعة بتخزين أكثر من قيمة واحدة في هيكل بيانات واحد يضع القيم على نحوٍ متتالي في الذاكرة، ويمكن أن تخزن الأشعة قيمًا من النوع ذاته، وهي مفيدةٌ عندما يكون لديك لائحةٌ من العناصر، مثل سطور نصية ضمن ملف، أو أسعار منتجات في سلة تسوّق. إنشاء شعاع جديد لإنشاء شعاع جديد فارغ نستدعي الدالة Vec::new كما هو موضح في الشيفرة 1. let v: Vec<i32> = Vec::new(); الشيفرة 1: إنشاء شعاع جديد فارغ لتخزين قيم من النوع i32 لاحظ أننا كتبنا ما يشير إلى نوع البيانات، لأننا لا نستطيع إدخال أي نوع نريده في الشعاع، ويجب على رست أن تعلم أي نوع من البيانات نريد أن ندخله إلى الشعاع، وهذه النقطة مهمة جدًا. بُنيَت الأشعة باستخدام الأنواع المعمّاة generics وسنغطّي استخدام الأنواع المعمّاة مع أنواعك الخاصة لاحقًا، ويكفي حاليًا أن تعرف أن النوع Vec<T>‎ الموجود في المكتبة القياسية يستطيع تخزين أي نوع داخله. يمكننا تحديد النوع الذي يحمله الشعاع عند إنشائه دون استخدام الأقواس المثلثة angle brackets. أخبرنا راست في الشيفرة 1 أن Vec<T>‎ في v يحمل عناصر من النوع i32. ستُنشئ معظم الأحيان شعاع Vec<T>‎ يحتوي على قيم ابتدائية ويستنتج رست نوع البيانات التي تريد أن تخزنها داخل الشعاع من القيم الابتدائية، لذا يُعد استخدام الطريقة السابقة نادرًا. توفر لنا رست الماكرو vec!‎ الذي يُنشئ شعاعًا جديدًا يحتوي على القيم التي تمررها له، وتوضح الشيفرة 2 ذلك بإنشاء شعاع من النوع Vec<i32>‎ يحتوي على القيم 1 و2 و3، والنوع هو i32 لأنه النوع الافتراضي للأعداد الصحيحة كما ناقشنا سابقًا في مقال أنواع البيانات. let v = vec![1, 2, 3]; الشيفرة 2: إنشاء شعاع جديد يحتوي على قيم تستطيع رست استنتاج أن نوع الشعاع v هو Vec<i32>‎، لأننا أعطينا قيمًا ابتدائية من النوع i32، وكتابة النوع مباشرةً ليس ضروريًا هنا. الآن دعنا ننظر إلى كيفية التعديل على شعاع. تحديث شعاع يمكننا استخدام التابع push لإضافة عناصر إلى شعاع بعد إنشائه كما هو موضح في الشيفرة 3. let mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8); الشيفرة 3: استخدام التابع push لإضافة قيم إلى شعاع إذا أردنا تغيير القيم، علينا أن نجعل الشعاع قابلًا للتعديل -كما هو الحال مع أي متغير اعتيادي- باستخدام الكلمة المفتاحية mut كما ناقشنا سابقًا. جميع الأرقام التي وضعناها في الشعاع هي من النوع i32، وتستنتج رست ذلك من البيانات، لذلك ليس من الضروري تحديد النوع بكتابة Vec<i32>‎. قراءة العناصر من الأشعة هناك طريقتان للإشارة إلى قيمة موجودة في الشعاع: إما باستخدام الدليل index أو باستخدام التابع get، نوضح في الأمثلة التالية أنواع البيانات التي تُعيدها الدوال بهدف جعلها واضحةً قدر الإمكان. توضح الشيفرة 4 كلا الطريقتين في الوصول إلى قيمة ضمن شعاع، وذلك باستخدام دليل العنصر أو التابع get. let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; println!("The third element is {}", third); let third: Option<&i32> = v.get(2); match third { Some(third) => println!("The third element is {}", third), None => println!("There is no third element."), } الشيفرة 4: استخدام دليل العنصر أو التابع get للحصول على عنصر ضمن الشعاع لاحظ بعض التفاصيل المهمة هنا؛ إذ استخدمنا قيمة الدليل "2" للحصول على العنصر الثالث وذلك لأن العناصر في الشعاع تحمل دليل عددي يبدأ من الصفر، كما يعطي استخدام & و[] مرجعًا إلى العنصر الموجود في الدليل الذي حددناه. عندما نستخدم التابع get مع تمرير الدليل مثل وسيط argument نحصل على Option<&T>‎ الذي يمكننا استخدامه مع البنية match. توفّر رست طريقتين مختلفتين ليتسنى لك اختيار تصرف برنامجك عندما تحاول استخدام قيمة دليل خارج نطاق العناصر الموجودة في الشعاع، على سبيل المثال دعنا ننظر إلى ما يحدث عندما يكون لدينا شعاع من خمسة عناصر ونحاول الوصول إلى العنصر ذو الدليل 100 باستخدام كلا الطريقتين كما هو موضح في الشيفرة 5. let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100]; let does_not_exist = v.get(100); الشيفرة 5: محاولة الوصول إلى الدليل 100 في شعاع يحتوي على خمسة عناصر فقط ستتسبب الطريقة الأولى [] بهلع panic البرنامج عند تشغيل الشيفرة البرمجية السابقة، لأنها تحاول لإشارة إلى عنصر غير موجود، وهذه الطريقة هي الأفضل في حال أردت لبرنامجك أن يتوقف عن العمل في حال محاولتك الوصول إلى عنصر يقع بعد نهاية الشعاع. عندما نمرر إلى التابع get دليلًا يقع خارج المصفوفة فهو يُعيد القيمة None دون الهلع، ويجب أن تستخدم هذه الطريقة إذا كانت محاولة الوصول إلى عنصر يقع خارج نطاق الشعاع ممكنة الحدوث تحت الظروف الاعتيادية، ويجب على برنامجك عندها أن يتعامل مع Some(&element)‎ أو None كما ناقشنا سابقًا. على سبيل المثال، قد يكون الدليل مُدخلًا من قِبل المستخدم وبالتالي إذا أدخل المستخدم رقمًا أكبر من حجم الشعاع عن طريق الخطأ نحصل على القيمة None ويمكنك إخبار المستخدم عندها أن الرقم الذي أدخله كبير ويمكن إعلامه أيضًا بحجم الشعاع الحالي وإعادة طلب إدخال القيمة منه، وهذه وسيلة عملية أكثر من جعل البرنامج يتوقف بالكامل بسبب خطأ كتابي بسيط عندما نحصل على مرجع صالح، يتأكد مدقق الاستعارة borrow checker من أن قوانين الملكية ownership والاستعارة borrowing محققة (ناقشنا هذه القوانين سابقًا) للتأكد من أن المرجع وأي مراجع أخرى لمحتوى الشعاع هي مراجع صالحة. تذكر القاعدة التي تنص على أنه لا يمكنك الحصول على مرجع قابل للتعديل ومرجع آخر غير قابل للتعديل في النطاق ذاته، وتنطبق هذه القاعدة على الشيفرة 6 عندما نخزن مرجعًا غير قابلٍ للتعديل للعنصر الأول في الشعاع ومن ثم نجرّب إضافة عنصر إلى نهاية الشعاع، ولن يعمل هذا البرنامج إذا أردنا الإشارة إلى ذلك العنصر لاحقًا ضمن الدالة ذاتها: let mut v = vec![1, 2, 3, 4, 5]; let first = &v[0]; v.push(6); println!("The first element is: {}", first); الشيفرة 6: محاولة إضافة عنصر إلى شعاع مع تخزين مرجع إلى عنصر داخل الشعاع في الوقت ذاته سيتسبب تصريف الشيفرة البرمجية السابقة بالخطأ التالي: $ cargo run Compiling collections v0.1.0 (file:///projects/collections) error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable --> src/main.rs:6:5 | 4 | let first = &v[0]; | - immutable borrow occurs here 5 | 6 | v.push(6); | ^^^^^^^^^ mutable borrow occurs here 7 | 8 | println!("The first element is: {}", first); | ----- immutable borrow later used here For more information about this error, try `rustc --explain E0502`. error: could not compile `collections` due to previous error قد تبدو الشيفرة 6 صالحة ويجب أن تعمل. لماذا يتعلق مرجع العنصر الأول بالتغييرات الحاصلة بنهاية الشعاع؟ لنفهم الخطأ يجب أن نفهم كيف تعمل الأشعة، إذ تضع الأشعة القيم بصورةٍ متتالية في الذاكرة وبالتالي يتطلب إضافة عنصر جديد إلى نهاية الشعاع حجز ذاكرة جديدة ونسخ العناصر القديمة في الشعاع إلى مساحة جديدة إذا لم يكن هناك مساحةٌ كافية لوضع جميع العناصر في الشعاع على نحوٍ متتالي، وسيشير مرجع العنصر الأول في تلك الحالة إلى ذاكرة مُحرَّرة deallocated، وتمنع قوانين الاستعارة البرامج من التسبب بهذا النوع من الأخطاء قبل حدوثها. ملاحظة: للمزيد من التفاصيل حول النوع Vec<T>‎ ألقِ نظرةً على إلى مثال عن تنفيذ Vec Rustonomicon. الوصول إلى قيم شعاع متعاقبة للوصول إلى عناصر الشعاع بصورةٍ متعاقبة، نمرّ بجميع العناصر الموجودة دون الحاجة لاستخدام دليل كل عنصر في كل مرة، وتوضح الشيفرة 7 كيفية استخدام الحلقة for للحصول على مراجع غير قابلة للتعديل لعناصر الشعاع من النوع i32 وطباعتها. let v = vec![100, 32, 57]; for i in &v { println!("{}", i); } الشيفرة 7: طباعة كل عنصر في شعاع عن طريق المرور على العناصر باستخدام حلقة for يمكننا أيضًا المرور على مراجع قابلة للتعديل mutable لكل عنصر في شعاع قابل للتعديل، وذلك بهدف إجراء تغييرات على جميع العناصر. تجمع الحلقة for في الشيفرة 8 القيمة 50 إلى كل عنصر في الشعاع. let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } الشيفرة 8: المرور على مراجع قابلة للتعديل لعناصر في شعاع لتغيير قيمة العنصر الذي يشير المرجع القابل للتعديل إليه نستخدم عامل التحصيل dereference * للحصول على القيمة الموجودة في i قبل استخدام العامل =+، وسنناقش عامل التحصيل بتوسع أكبر لاحقًا. المرور على عناصر الشعاع عملية آمنة، سواءً كان ذلك الشعاع قابلًا للتعديل أو غير قابل للتعديل وذلك بسبب قوانين مدقق الاستعارة، فإذا حاولنا إدخال أو إزالة العناصر ضمن الحلقة for في الشيفرة 7 أو الشيفرة 8، فسنحصل على خطأ تصريفي مشابه للخطأ الذي حصلنا عليه عند تصريفنا للشيفرة 6، إذ يمنع المرجع الذي يشير إلى الشعاع ضمن الحلقة for أي تعديلات متزامنة لكامل الشعاع. استخدام تعداد لتخزين عدة أنواع يمكن للأشعة أن تخزن قيمًا من النوع ذاته فقط، وقد يشكّل هذا عقبةً صغيرةً، إذ أن هناك الكثير من الاستخدامات التي نريد فيها تخزين لائحة من عناصر من أنواع مختلفة، ولحسن الحظ المتغايرات variants الخاصة بالتعداد enum معرفة تحت نوع التعداد ذاته وبالتالي يمكننا استخدام نوع واحد لتمثيل عناصر من أنواع مختلفة بتعريف واستخدام تعداد. على سبيل المثال، لنفرض أننا بحاجة للحصول على قيم من صف ضمن جدول، ويحتوي هذا الصف قيمًا من نوع الأعداد الصحيحة وقيم أعداد عشرية floating point وقيم سلاسل نصية strings، حينها يمكننا تعريف تعداد يحتوي على متغايرات يمكنها تخزين أنواع القيم المختلفة وسيُنظر إلى جميع متغايرات التعداد إلى أنها من النوع ذاته الخاص بالتعادد، يمكننا بعد ذلك إنشاء شعاع يخزّن ذلك التعداد، وبالتالي سيخزّن أنواعًا مختلفة، وقد وضحنا هذه العملية في الشيفرة 9. enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from("blue")), SpreadsheetCell::Float(10.12), ]; الشيفرة 9: تعريف تعداد enum لتخزين قيم من أنواع مختلفة في شعاع واحد تحتاج رست إلى معرفة الأنواع التي ستُخزَّن في الشعاع وقت التصريف حتى تعلم بالضبط المساحة التي ستحتاجها لتخزين كل عنصر في الكومة، ويجب علينا أن نكون واضحين بخصوص الأنواع التي ستُخزَّن في الشعاع. في الحقيقة إذا سمحت رست للشعاع بأن يحمل أي نوع فهناك احتمال أن يتسبب نوع أو أكثر بالأخطاء عند إجراء العمليات على عناصر الشعاع، واستخدام التعداد مع التعبير match يعني أن رست ستعالج كل حالة بوقت التصريف كما ناقشنا سابقًا. لن تعمل طريقة التعداد هذه إذا لم تُعرّف جميع أنواع البيانات التي سيحتاجها البرنامج عند وقت التشغيل، ويمكنك استخدام كائن السمة trait object عندها، والذي سنتكلم عنه لاحقًا. الآن، وبعد أن ناقشنا أكثر الطرق شيوعًا في استخدام الأشعة، ألقِ نظرةً على توثيق الواجهة البرمجية للمزيد من التوابع المفيدة المعرفة في النوع <Vec<T في المكتبة القياسية، على سبيل المثال، بالإضافة إلى تابع push، هناك تابع pop لحذف وإعادة العنصر الأخير ضمن الشعاع. التخلص من الشعاع يعني التخلص من عناصره يُحرَّر الشعاع من الذاكرة مثل أيّ هيكل struct اعتيادي عند خروجه من النطاق كما هو موضح في الشيفرة 10. { let v = vec![1, 2, 3, 4]; // استخدام‫ v } // تخرج‫ ‎‎ v من النطاق وتُحرَّر من الذاكرة بحلول هذه النقطة الشيفرة 10: توضيح النقطة التي يُحرَّر فيها الشعاع ونفقد عناصره عندما تُحرَّر الذاكرة الخاصة بالشعاع، هذا يعني أننا نفقد عناصر أيضًا، أي أننا لن نستطيع الوصول إلى الأعداد الصحيحة في الشيفرة السابقة بعد خروج الشعاع من النطاق. يتأكد مدقق الاستعارة من أن جميع المراجع التي تشير إلى محتوى الشعاع -إن وُجدت- تُستخدَم فقط عندما يكون الشعاع بنفسه صالحًا. دعنا ننتقل إلى نوع التجميعة التالي: ألا وهو السلسلة النصية String. ترجمة -وبتصرف- لقسم من الفصل Common Collections من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: المسارات paths والنطاق الخاص بها في لغة رست Rust بنية match للتحكم بسير برامج لغة رست Rust التعدادات enums في لغة رست Rust التحكم بسير تنفيذ برامج راست Rust
  17. قد تكون عملية كتابة مسارات استدعاء الدوال في بعض الأحيان عملية غير مريحة ورتيبة، كان علينا في الشيفرة 7 (سابقًا) تحديد front_of_house و hosting في حال اخترنا المسار النسبي relative path أو المسار المطلق absolute path إلى الدالة add_to_waitlist وفي كل مرة أردنا استدعاء الدالة add_to_waitlist أيضًا. لحسن الحظ هناك طريقة لتبسيط العملية السابقة، إذ يمكننا إنشاء اختصار للمسار باستخدام الكلمة المفتاحية use مرةً واحدةً فقط ومن ثمّ استخدام المسار الأقصر المُختصر في كل مكان ضمن النطاق. نُضيف الوحدة crate::front_of_house::hosting إلى نطاق دالة eat_at_restaurant في الشيفرة 11، وذلك حتى نكتفي بكتابة hosting::add_to_waitlist لاستدعاء الدالة add_to_waitlist في eat_at_restaurant. اسم الملف: src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); } الشيفرة 11: إضافة الوحدة إلى النطاق باستخدام use تُعد عملية إضافة use متبوعةً بالمسار في النطاق عمليةً مشابهةً لإنشاء رابط رمزي symbolic link في نظام الملفات، إذ يصبح hosting اسمًا صالحًا بإضافة use crate::front_of_house::hosting في جذر الوحدة المصرّفة وضمن ذلك النطاق وكأن الوحدة hosting معرفةٌ داخل جذر الوحدة المصرّفة. تتبع المسارات الموجودة ضمن النطاق باستخدام use قوانين الخصوصية مثلها مثل أي مسار آخر. لاحظ أن use يُنشئ اختصارًا للنطاق الذي يحتوي على use فقط. ننقل في الشيفرة 12 الدالة eat_at_restaurant إلى وحدة ابن جديدة تدعى customer وهي ذات نطاق مختلف عن تعليمة use، لذا لن نستطيع تصريف محتوى الدالة: اسم الملف: src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use crate::front_of_house::hosting; mod customer { pub fn eat_at_restaurant() { hosting::add_to_waitlist(); } } الشيفرة 12: تعليمة use تُطبّق فقط في النطاق الواردة فيها يوضح خطأ التصريف التالي أن الاختصار غير موجود ضمن الوحدة customer: $ cargo build Compiling restaurant v0.1.0 (file:///projects/restaurant) error[E0433]: failed to resolve: use of undeclared crate or module `hosting` --> src/lib.rs:11:9 | 11 | hosting::add_to_waitlist(); | ^^^^^^^ use of undeclared crate or module `hosting` warning: unused import: `crate::front_of_house::hosting` --> src/lib.rs:7:5 | 7 | use crate::front_of_house::hosting; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default For more information about this error, try `rustc --explain E0433`. warning: `restaurant` (lib) generated 1 warning error: could not compile `restaurant` due to previous error; 1 warning emitted لاحظ وجود تحذير بخصوص عدم وجود use في النطاق، ولتصحيح هذه المشكلة ننقل التعليمة use إلى الوحدة customer أيضًا، أو نُشير إلى الاختصار في الوحدة الأب باستخدام super::hosting ضمن الوحدة الابن customer. إنشاء مسارات اصطلاحية Idiomatic باستخدام use لعلّك تساءلت عند قراءتك للشيفرة 11 عن سبب كتابتنا use crate::front_of_house::hosting ومن ثم استدعائنا hosting::add_to_waitlist في eat_at_restaurant بدلًا من تحديد مسار use الموجود في الدالة add_to_waitlist لتحقيق النتيجة ذاتها كما هو الأمر في الشيفرة 13. اسم الملف: src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use crate::front_of_house::hosting::add_to_waitlist; pub fn eat_at_restaurant() { add_to_waitlist(); } الشيفرة 13: إضافة الدالة add_to_waitlist إلى النطاق باستخدام use وهو استخدام غير اصطلاحي unidiomatic على الرغم من أن الشيفرة 11 والشيفرة 13 يحققان الغرض ذاته، إلا أن الشيفرة 11 هي الطريقة الاصطلاحية لإضافة دالة إلى النطاق باستخدام use. تعني إضافة وحدة الدالة الأب إلى النطاق باستخدام use أننا بحاجة تحديد الوحدة الأب عند استدعاء الدالة، وتحديد الوحدة الأب عند استدعاء الدالة يوضّح أن الدالة ليست معرّفة محليًا مع المحافظة على تقليل تكرار كامل المسار. لا توضح الشيفرة البرمجية الموجودة في الشيفرة 13 مكان تعريف add_to_waitlist. على الجانب الآخر، عند إضافة الهياكل والتعدادات والعناصر الأخرى إلى النطاق باستخدام use فإن الاستخدام الاصطلاحي هو تحديد كامل المسار، وتوضح الشيفرة 14 الطريقة الاصطلاحية في إضافة الهيكل HashMap الموجود في المكتبة القياسية إلى نطاق الوحدة الثنائية المُصرّفة. اسم الملف: src/main.rs use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); } الشيفرة 14: إضافة HashMap إلى النطاق بطريقة اصطلاحية لا يوجد هناك أي مبرر قوي بخصوص هذا الاصطلاح سوى أنه الاصطلاح المتعارف عليه واعتاد معظم المبرمجين عليه، وبالتالي فإن استخدامه يجعل قراءة شيفرة راست البرمجية والتعديل عليها أسهل. الاستثناء لهذا الاصطلاح هو حالة إضافة عنصرين إلى النطاق يحملان الاسم ذاته باستخدام تعليمة use، إذ لا تسمح رست بذلك. توضح الشيفرة 15 كيفية إضافة نوعَي Result إلى النطاق يحملان الاسم ذاته ولكنهما ينتميان إلى وحدات أب مختلفة، إضافةً إلى كيفية الإشارة إليهما. اسم الملف: src/lib.rs use std::fmt; use std::io; fn function1() -> fmt::Result { // --snip-- Ok(()) } fn function2() -> io::Result<()> { // --snip-- Ok(()) } الشيفرة 15: إضافة نوعين من الاسم ذاته إلى النطاق ذاته يتطلب استخدام الوحدات الأب كما تلاحظ، يميز استخدام الوحدات الأب ما بين النوعين Result، وإذا استخدمنا use std::fmt::Result و use std::io::Result بدلًا من ذلك فسيكون لدينا قيمتين Result في نفس النطاق، ولن تعرف رست عندها أي الأنواع التي نقصدها عندما نستخدم Result. إضافة أسماء جديدة باستخدام الكلمة المفتاحية as هناك حل آخر لمشكلة إضافة نوعين من الاسم ذاته إلى النطاق باستخدام use، إذ يمكننا كتابة الكلمة المفتاحية as بعد المسار وكتابة اسم محلي بعدها أو اسم مستعار alias للنوع. توضح الشيفرة 16 طريقةً أخرى لكتابة الشيفرة 15 بإعادة تسمية واحد من النوعَين Result باستخدام as. اسم الملف: src/lib.rs use std::fmt::Result; use std::io::Result as IoResult; fn function1() -> Result { // --snip-- Ok(()) } fn function2() -> IoResult<()> { // --snip-- Ok(()) } الشيفرة 16: إعادة تسمية نوع بعد إضافته إلى النطاق باستخدام الكلمة المفتاحية as اخترنا في تعليمة use الثانية الاسم الجديد IoResult للنوع std::io::Result وهو اسم لا يتعارض مع الاسم Result في std::fmt الذي أضفناه إلى النطاق أيضًا. تعدّ كلًا من الشيفرة 15 والشيفرة 16 طريقةً اصطلاحيةً في التغلب على مشكلة تعارض الأسماء، فاختر ما يحلو لك. إعادة تصدير الأسماء باستخدام pub use عندما نُضيف اسمًا إلى النطاق باستخدام الكلمة المفتاحية use، سيكون هذا الاسم المتاح في النطاق الجديد خاصًا. لتمكين الشيفرة البرمجية التي تستدعي الشيفرة البرمجية لتُشير إلى ذلك الاسم وكأنه معرّف في نطاق الشيفرة البرمجية تلك، علينا استخدام pub مع use، وتُعرف هذه الطريقة باسم إعادة التصدير re-exporting لأننا نُضيف العنصر إلى النطاق، لكننا نجعله ممكن الإضافة لنطاقات أخرى. تمثل الشيفرة 17 نسخةً معدلةً عن الشيفرة 11 بتغيير استخدام use في الوحدة الجذر إلى pub use. اسم الملف: src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } pub use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); } الشيفرة 17: جعل الاسم متاحًا لأي شيفرة برمجية لاستخدامه من نطاق جديد باستخدام pub use كان على الشيفرة البرمجية الخارجية -قبل هذا التغيير- استدعاء الدالة add_to_waitlist باستخدام المسار restaurant:: front_of_house::hosting::add_to_waitlist()‎، والآن وبعد إعادة تصدير وحدة hosting باستخدام pub use من الوحدة الجذر يُمكن للشيفرة الخارجية الآن أن تستخدم المسار restaurant::hosting::add_to_waitlist()‎. إعادة التصدير مفيد عندما يكون الهيكل الداخلي لشيفرتك البرمجية مختلفًا عن الطريقة التي قد يفكّر بها المبرمجون الآخرون باستدعاء شيفرتك البرمجية بحسب استخدامهم لها. على سبيل المثال وبالنظر إلى تشبيه المطعم، يفكر الأشخاص الذين يعملون بالمطعم بخصوص كل من الأشياء التي تحدث في واجهة المطعم وفي خلفية المطعم، إلا أن الزبائن الذي يزورون المطعم لن يفكرون غالبًا بخصوص الأشياء التي تحدث في خلفية المطعم ضمن هذا السياق. يمكننا كتابة شيفرتنا البرمجية بالاستعانة بالكلمتين pub use ضمن هيكل واحد مع كشف expose هيكل مختلف، وفعل ذلك يجعل من مكتبتنا منظّمة للمبرمجين الذي يعملون على المكتبة وللمبرمجين الذين يستدعون المكتبة على حد سواء، وسننظر لمثال آخر يحتوي على pub use وكيف يؤثر على توثيق الوحدة المصرّفة لاحقًا. استخدام الحزم الخارجية برمجنا سابقًا لعبة تخمين الأرقام واستخدمنا فيها حزمةً خارجيةً تدعى rand للحصول على أرقام عشوائية، ولاستخدام rand في مشروعنا أضفنا السطر التالي إلى ملف Cargo.toml: اسم الملف: Cargo.toml rand = "0.8.3" تخبر إضافة rand مثل اعتمادية dependency في ملف Cargo.toml كارجو بأنه يجب عليه تنزيل الحزمة rand وأي اعتمادية من crates.io وجعل الحزمة rand متاحة في مشروعنا. أضفنا السطر use مع اسم الوحدة المُصرّفة لجلب التعاريف الموجودة في الحزمة rand إلى نطاق حزمتنا، وأضفنا العناصر التي أردنا إضافتها إلى النطاق. تذكر أننا أضفنا السمة Rng إلى نطاقنا واستدعينا الدالة rand::thread_rng: use rand::Rng; fn main() { let secret_number = rand::thread_rng().gen_range(1..=100); } كتب الكثير من أعضاء مجتمع رست حزمًا وجعلها متاحة على crates.io، ويجب أن تتبع هذه الخطوات إذا أردت استخدام أي منها: إضافة الحزم في الملف Cargo.toml مثل قائمة، واستخدام use لإضافة العناصر من وحداتها المُصرّفة إلى النطاق. لاحظ أن المكتبة القياسية std هي في الحقيقة وحدة مُصرَّفة خارجية لحزمتنا، وليس علينا تغيير الملف Cargo.toml لتضمين std لأن المكتبة القياسية تُضاف افتراضيًا إلى مشروعنا بلغة رست، إلا أننا بحاجة إضافة عناصرها إلى نطاق حزمتنا باستخدام use. على سبيل المثال، يجب علينا كتابة السطر البرمجي التالي لاستخدام HashMap: use std::collections::HashMap; وهذا يمثّل مسار مطلق يبدأ بالكلمة std وهو اسم وحدة مكتبة مُصرَّفة قياسي. استخدام المسارات المتداخلة لتنظيم تعليمات use الطويلة إذا كنا نستخدم عدة عناصر معرفة في الوحدة المصرّفة ذاتها أو الوحدة ذاتها، فكتابة كل واحدة منها على حدى في سطر خاص سيأخذ الكثير من المساحة ضمن ملفنا. على سبيل المثال انظر إلى تعليمات use التالية التي استخدمناها في برمجة لعبة التخمين التي تُضيف بعض العناصر من std إلى النطاق: اسم الملف: src/main.rs use std::cmp::Ordering; use std::io; عوضًا عن ذلك يمكننا استخدام المسارات المتداخلة nested paths لإضافة العناصر ذاتها إلى النطاق ضمن سطر واحد، ونفعل ذلك عن طريق تحديد الجزء المشترك من المسار متبوعًا بنقطتين مزدوجتين ومن ثم أقواس معقوصة حول لائحة من الأجزاء التي تختلف عن الجزء المشترك من المسار كما هو موضح في الشيفرة 18. اسم الملف: src/main.rs use std::{cmp::Ordering, io}; الشيفرة 18: تحديد مسار متداخل لإضافة عدة عناصر إلى النطاق باستخدام مسار مشترك يُقلّل استخدام المسارات المشتركة عدد السطور اللازمة لإضافة بعض العناصر باستخدام تعليمة use في البرامج الكبيرة بصورةٍ ملحوظة. يمكننا استخدام المسار المتداخل ضمن أي مستوى في المسار، وهو أمر مفيد عند دمج تعليمتَي use تتشاركان بمسار جزئي. توضح الشيفرة 19 مثلًا تعليمتَي use: أحدها تُضيف std::io إلى النطاق والأخرى تجلب std::io::Write إلى النطاق. اسم الملف: src/lib.rs use std::io; use std::io::Write; الشيفرة 19: تعليمتَي use، تشكًل واحدة منهما مسارًا جزئيًا للآخر الجزء المشترك من المسارَين هو std::io وهذا هو المسار الكامل الأول، ولدمج المسارَين إلى تعليمة use واحدة يمكننا استخدام self في المسار المتداخل كما هو موضح في الشيفرة 20. اسم الملف: src/lib.rs use std::io::{self, Write}; الشيفرة 20: دمج المسارَين في الشيفرة 19 إلى تعليمة use واحدة يُضيف السطر البرمجي السابق كلًا من std::io و std::io::Write إلى النطاق. عامل تحديد الكل glob يمكننا استخدام العامل * إذا أردنا إضافة جميع العناصر العلنية الموجودة في مسار ما إلى النطاق: use std::collections::*; تُضيف تعليمة use السابقة جميع العناصر العلنية المعرفة في المسار std::collections إلى النطاق الحالي. كُن حريصًا عن استخدامك لهذا العامل، لأنه قد يجعل من الصعب معرفة أي الأسماء الموجودة في النطاق وأين الأسماء المستخدمة في البرنامج والمعرفة داخله. يُستخدم عامل تحديد الكل غالبًا عند تجربة إضافة كل شيء ضمن عملية تجربة إلى الوحدة tests، وسنتكلم لاحقًا عن كيفية كتابة هذا النوع من الوحدات. يُستخدم عامل تحديد الكل أيضًا في بعض الأحيان جزءًا من النمط التمهيدي prelude pattern، ألقِ نظرةً على توثيق المكتبة القياسية لمزيد من التفاصيل حول هذا النمط. فصل الوحدات إلى ملفات مختلفة لحدّ اللحظة، عرّفت الشيفرات البرمجية التي استعرضناها في الأمثلة عدة وحدات في ملف واحد، إلا أنك قد تكون بحاجة نقل تعاريف الوحدات إلى ملف منفصل في حال كان عدد الوحدات كبيرًا وذلك بهدف جعل شيفرتك البرمجية سهلة القراءة. على سبيل المثال، دعنا نبدأ من الشيفرة البرمجية الموجودة في الشيفرة 17 التي تحتوي على عدّة وحدات للمطاعم، وسنستخرج هذه الوحدات إلى ملف مخصص بدلًا من تعريف كل من الوحدات في ملف جذر الوحدة المُصرّفة، وفي هذه الحالة ملف جذر الوحدة المُصرّفة هو src/lib.rs إلا أن هذه العملية تعمل أيضًا مع الوحدات الثنائية المُصرّفة التي يكون ملف ملف جذر الوحدة المُصرّفة الخاص بها هو src/main.rs. دعنا نستخرج أولًا الوحدة front_of_house إلى ملفها الخاص، ونحذف الشيفرة البرمجية داخل الأقواس المعقوصة الخاصة بالوحدة front_of_house ونترك فقط تصريح ;mod front_of_house، بحيث يحتوي src/lib.rs على الشيفرة البرمجية الموضحة في الشيفرة 21، ولاحظ أن الشيفرة لن تُصرَّف حتى نُنشئ ملف src/front_of_house.rs، وهو ما سنفعله في الشيفرة 22. اسم الملف: src/lib.rs mod front_of_house; pub use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); } الشيفرة 21: تصريح الوحدة front_of_house التي سيتواجد محتواها في الملف src/front_of_house.rs ننقل الشيفرة البرمجية التي كانت موجودة في الأقواس المعقوصة إلى ملف جديد يسمى src/front_of_house.rs كما هو موضح في الشيفرة 22. سيعلم المصرّف مكان هذا الملف لأنه سيصادف التصريح عن الوحدة front_of_house في جذر الوحدة المُصرّفة. اسم الملف: src/front_of_house.rs pub mod hosting { pub fn add_to_waitlist() {} } الشيفرة 22: التعريف داخل الوحدة front_house ضمن الملف src/front_of_house.rs لاحظ أنك تستطيع تحميل ملف ما باستخدام تصريح mod مرةً واحدةً فقط ضمن شجرة الوحدة، وحالما يعلم المصرف أن هذا الملف ينتمي إلى المشروع (بالإضافة إلى معرفته لمكان الملف ضمن شجرة الوحدة بفضل المكان الذي كتبت فيه تعليمة mod)، ويجب أن تشير الملفات الأخرى إلى الشيفرة البرمجية الخاصة بالملف المُحمّل باستخدام مسار يشير إلى مكان التصريح عنه كما ناقشنا في الفقرات السابقة. بكلمات أخرى، لا تشبه mod عملية تضمين "include"، الموجودة في بعض لغات البرمجة الأخرى. الآن، نستخرج الوحدة hosting إلى ملفها الخاص، إلا أن العملية مختلفة هنا بعض الشيء لأن hosting هي وحدة ابن من الوحدة front_of_house وليس وحدة ابن من الوحدة الجذر، ولذلك سنضع ملف الوحدة hosting في مسار جديد يُسمّى بحسب آباء الوحدة في شجرة الوحدة، وفي هذه الحالة سيكون المسار هو src/front_of_house/‎. لنقل الوحدة hosting نعدّل الملف src/front_of_house.rs بحيث يحتوي على تصريح الوحدة hosting فقط: اسم الملف: src/front_of_house.rs pub mod hosting; نُنشئ بعد ذلك مسار src/front_of_house وملف hosting.rs ليحتوي على التعريف الخاص بالوحدة hosting: اسم الملف: src/front_of_house/hosting.rs pub fn add_to_waitlist() {} إذا وضعنا hosting.rs في المجلد src بدلًا من المجلد الحالي فإن المصرّف سيتوقع رؤية شيفرة hosting.rs في الوحدة hosting المصرح عنها في جذر الوحدة المُصرّفة وليس المصرح عنها وحدة ابن للوحدة front_of_house. تُملي قواعد المصرف بخصوص مسارات الملفات والوحدات أهمية تنظيمها بالنسبة لشجرة الوحدة الخاصة بالمشروع. نقلنا الشيفرة البرمجية الخاصة بكل وحدة إلى ملف منفصل وبقيت شجرة الوحدة على حالها، وستعمل استدعاءات الدالة في eat_at_restaurant دون أي تعديلات على الرغم من أن التعريفات موجودة في ملفات مختلفة، وتسمح لنا هذه الطريقة بتحريك الوحدات إلى ملفات جديدة بسهولة مع نموّ حجم مشروعك. لاحظ أن تعليمة pub use crate::front_of_house::hosting الموجودة في src/lib.rs لم تتغير، إذ لا تؤثّر use على الملفات التي ستُصرَّف مثل جزء من الوحدة المُصرّفة. تعرّف الكلمة المفتاحية mod الوحدات، إلا أن رست تنظر إلى الملف الذي يحمل الاسم ذاته الخاص بالوحدة للبحث عن الشيفرة البرمجية الخاصة بتلك الوحدة. مسارات ملفات مختلفة غطّينا بحلول هذه النقطة مسارات الملفات الشائعة والاصطلاحية في مصرّف رست، إلا أن رست تدعم أيضًا بعض التنسيقات القديمة لمسارات الملفات، فبالنسبة لوحدة تدعى front_of_house مصرح عنها في جذر الوحدة المُصرّفة، ينظر المصرف للشيفرة البرمجية الخاصة بالوحدة في: المسار src/front_of_house.rs (وهو مسار تكلمنا عنه سابقًا). المسار src/front_of_house/mod.rs (مسار قديم إلا أنه مدعوم). سينظر المصرف بالنسبة لوحدة جزئية من front_of_house تدعى hosting إلى اللشيفرة البرمجية الخاصة بالوحدة في: المسار src/front_of_house/hosting.rs (مسار ناقشناه سابقًا). المسار src/front_of_house/hosting/mod.rs (مسار قديم إلا أنه مدعوم). إذا استخدمت كلا الأسلوبين للوحدة ذاتها، ستحصل على خطأ من المصرّف، إلا أن استخدام كلا النوعين في ذات المشروع لوحدات مختلفة مسموح، ولكنه قد يتسبب بالخلط لبعض الناس الذين يقرؤون الشيفرة البرمجية. النقطة السلبية الأساسية للأسلوب الذي يستخدم ملفات بالاسم mod.rs هو انتهاء مشروعك غالبًا باحتوائه على كثير من الملفات تحمل الاسم mod.rs، وهذا قد يثير الإرباك عندما تحاول تعديل الشيفرة البرمجية ضمن محرر ما. ترجمة -وبتصرف- لقسم من الفصل Managing Growing Projects with Packages, Crates, and Modules من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: المسارات paths وشجرة الوحدة module tree في رست Rust أنواع البيانات Data Types في لغة رست Rust الحزم packages والوحدات المصرفة crates في لغة رست Rust
  18. يُستخدم المسار path بنفس الطريقة المُستخدمة عند التنقل ضمن نظام الملفات في الحاسوب حتى ندلّ رست على مكان وجود عنصر ما ضمن شجرة الوحدة module tree، وبالتالي علينا معرفة مسار الدالة أولًا إذا أردنا استدعائها. قد يكون المسار واحدًا من النوعين التاليين: المسار المُطلق absolute path: وهو المسار الكامل بدءًا من جذر الوحدة المصرّفة؛ إذ أن المسار المُطلق لشيفرة برمجية موجودة في وحدة مصرّفة خارجية تبدأ باسم الوحدة المصرّفة بينما يبدأ مسار شيفرة برمجية داخل الوحدة المصرّفة الحالية المُستخدمة بالكلمة crate. المسار النسبي relative path: ويبدأ هذا المسار من الوحدة المصرّفة الحالية المُستخدمة ويستخدم الكلمة المفتاحية self، أو super، أو معرّف ينتمي إلى الوحدة نفسها. يُتبع كلا نوعَي المسارات السابقَين بمعرّف identifier واحد، أو أكثر ويفصل بينهما نقطتان مزدوجتان ::. بالعودة إلى الشيفرة 1 في المقالة السابقة، لنفترض أننا نريد استدعاء الدالة add_to_waitlist، وهذا يقتضي أن نسأل أنفسنا: ما هو مسار الدالة add_to_waitlist؟ تحتوي الشيفرة 3 على الشيفرة 1 مع إزالة بعض الوحدات والدوال. سنستعرض طريقتين لاستدعاء الدالة add_to_waitlist من دالة جديدة معرّفة في جذر الوحدة المصرّفة وهي eat_at_restaurant في هذه الحالة، والمسارات الموجودة في كل من الطريقتين صالحة إلا أن هناك مشكلة أخرى ستمنع مثالنا من أن يُصرَّف كما هو، وسنفسّر ذلك قريبًا. تشكل الدالة eat_at_restaurant جزءًا من الواجهة البرمجية العامة public API الخاصة بوحدة المكتبة المصرّفة library crate، لذا نُضيف الكلمة المفتاحية pub إليها، وسنناقش المزيد من التفاصيل بخصوص pub في الفقرة التالية. اسم الملف: src/lib.rs mod front_of_house { mod hosting { fn add_to_waitlist() {} } } pub fn eat_at_restaurant() { // مسار مطلق crate::front_of_house::hosting::add_to_waitlist(); // مسار نسبي front_of_house::hosting::add_to_waitlist(); } [الشيفرة 3: استدعاء الدالة addtowaitlist باستخدام المسار المُطلق والمسار النسبي] نستخدم مسارًا مُطلقًا عندما نريد استدعاء الدالة add_to_waitlist داخل eat_at_restaurant للمرة الأولى، ويمكننا استخدام المسار المطلق بدءًا بالكلمة المفتاحية crate بالنظر إلى أن الدالة add_to_waitlist معرفة في نفس الوحدة المصرّفة الخاصة بالدالة eat_at_restaurant، ومن ثمّ نضمّن كل من الوحدات الأخرى الموجودة في شجرة الوحدة module tree حتى الوصول إلى الدالة add_to_waitlist. يمكنك تخيّل هذا الأمر بصورةٍ مشابهة لنظام الملفات على حاسوبك، إذ نكتب المسار ‎/front_of_house/hosting/add_to_waitlist لتشغيل البرنامج add_to_waitlist، إلا أننا نستخدم الكلمة المفتاحية crate للدلالة على جذر الوحدة المصرّفة بدلًا من استخدام / في بداية المسار، وهو أمرٌ مشابه لكتابة / في سطر الأوامر حتى تصل إلى جذر نظام الملفات على حاسوبك. نستخدم المسار النسبي في المرة الثانية التي نستدعي add_to_waitlist في eat_at_restaurant، ويبدأ المسار هنا بالاسم front_of_house، وهو اسم الوحدة المعرفة ضمن نفس مستوى eat_at_restaurant في شجرة الوحدة، وسيستخدم نظام الملفات المكافئ المسار front_of_house/hosting/add_to_waitlist بدءًا باسم الوحدة مما يعني أن المسار هو مسار نسبي. يجب الاختيار بين المسار المطلق والنسبي بناءً على مشروعك ويعتمد على إذا ما كنت ستميل غالبًا إلى نقل شيفرة تعريف العنصر البرمجية وتفريقها عن الشيفرة البرمجية التي تستخدم ذلك العنصر، أو إذا كنت ستبقيهما سويًا. على سبيل المثال، إذا أردنا نقل الوحدة front_of_house والدالة eat_at_restaurant إلى وحدة باسم customer_experience، سنضطر إلى تحديث المسار المطلق الخاص بالدالة add_to_waitlist، إلا أن المسار سيبقى صالحًا في حال استخدمنا المسار النسبي، لكن إذا نقلنا الدالة eat_at_restaurant بصورةٍ منفصلة إلى وحدة جديدة تدعى dining فلن يكون المسار النسبي لاستدعاء الدالة add_to_waitlist صالحًا بعد الآن ويجب تحديثه، إلا أن المسار المطلق سيبقى صالحًا. نفضّل هنا المسارات المطلقة، لأننا على الأغلب سنحرّك تعريف الشيفرة البرمجية واستدعاء العناصر على نحوٍ متفرق عن بعضهما بعضًا. دعنا نجرّب تصريف الشيفرة 3 ونقرأ الخطأ ونحاول معرفة السبب في عدم قابلية تصريفها. نحصل على الخطأ الموضح في الشيفرة 4. $ cargo build Compiling restaurant v0.1.0 (file:///projects/restaurant) error[E0603]: module `hosting` is private --> src/lib.rs:9:28 | 9 | crate::front_of_house::hosting::add_to_waitlist(); | ^^^^^^^ private module | note: the module `hosting` is defined here --> src/lib.rs:2:5 | 2 | mod hosting { | ^^^^^^^^^^^ error[E0603]: module `hosting` is private --> src/lib.rs:12:21 | 12 | front_of_house::hosting::add_to_waitlist(); | ^^^^^^^ private module | note: the module `hosting` is defined here --> src/lib.rs:2:5 | 2 | mod hosting { | ^^^^^^^^^^^ For more information about this error, try `rustc --explain E0603`. error: could not compile `restaurant` due to 2 previous errors [الشيفرة 4: أخطاء المصرف الناجمة عن تصريف الشيفرة 3] تدلنا رسالة الخطأ على أن الوحدة hosting هي وحدة خاصة، بمعنى أنه على الرغم من استخدامنا للمسار الصحيح الخاص بوحدة hosting ودالة add_to_waitlist إلا أن رست لن تسمح لنا باستخدامهما لأنه لا يوجد لدينا سماحية الوصول إلى هذه الأجزاء الخاصة. جميع العناصر في رست (دوال وتوابع وهياكل وتعدادات ووحدات وثوابت) هي خاصة بالوحدة الأب (الأصل) فقط افتراضيًا، وإذا أردت جعل عنصر ما مثل دالة أو هيكل خاصًا، فعليك وضعه داخل الوحدة. لا يمكن لعناصر في وحدة أب استخدام العناصر الخاصة داخل الوحدات التابعة لها، إلا أن وحدات الابن يمكن أن تستخدم العناصر الموجودة في الوحدات الأب، وهذا لأن الوحدات الابن تغلّف وتُخفي تفاصيل تطبيقها وتستطيع الوحدات الابن رؤية السياق الخاص بتعريفها، وحتى نستمرّ في تشبيهنا السابق للمطعم، تخيل أن قوانين الخصوصية مشابهة للمكاتب الإدارية للمطعم، فالذي يحصل في هذه المكاتب هو معزول عن زبائن المطعم، لكن يستطيع مدراء المطعم رؤية أي شيء في المطعم. تختار رست بأن يعمل نظام الوحدة على هذا النحو، بحيث يكون إخفاء تفاصيل التطبيق الداخلي هو الحالة الافتراضية، وبالتالي تستطيع بهذه الطريقة معرفة أي الأجزاء من الشيفرة الداخلية التي يمكنك تغييرها دون التسبب بأخطاء في الشيفرة الخارجية. مع ذلك، تعطيك رست الخيار لكشف الأجزاء الداخلية من الوحدات الابن للوحدات الخارجية باستخدام الكلمة المفتاحية pub لجعل العنصر عامًا. كشف المسارات باستخدام الكلمة المفتاحية pub بالعودة إلى الخطأ الموجود في الشيفرة 4 الذي كان مفاده أن الوحدة hosting هي وحدة خاصة، نريد أن تمتلك الدالة eat_at_restaurant الموجودة في الوحدة الأب وصولًا إلى الدالة add_to_waitlist الموجودة في الوحدة الابن، ولتحقيق ذلك نُضيف الكلمة المفتاحية pub إلى الوحدة hosting كما هو موضح في الشيفرة 5. اسم الملف: src/lib.rs mod front_of_house { pub mod hosting { fn add_to_waitlist() {} } } pub fn eat_at_restaurant() { // مسار مطلق crate::front_of_house::hosting::add_to_waitlist(); // مسار نسبي front_of_house::hosting::add_to_waitlist(); } [الشيفرة 5: التصريح عن الوحدة hosting باستخدام الكلمة المفتاحية pub لاستخدامها داخل eatatrestaurant] لسوء الحظ، تتسبب الشيفرة 5 بخطأ موضح في الشيفرة 6. $ cargo build Compiling restaurant v0.1.0 (file:///projects/restaurant) error[E0603]: function `add_to_waitlist` is private --> src/lib.rs:9:37 | 9 | crate::front_of_house::hosting::add_to_waitlist(); | ^^^^^^^^^^^^^^^ private function | note: the function `add_to_waitlist` is defined here --> src/lib.rs:3:9 | 3 | fn add_to_waitlist() {} | ^^^^^^^^^^^^^^^^^^^^ error[E0603]: function `add_to_waitlist` is private --> src/lib.rs:12:30 | 12 | front_of_house::hosting::add_to_waitlist(); | ^^^^^^^^^^^^^^^ private function | note: the function `add_to_waitlist` is defined here --> src/lib.rs:3:9 | 3 | fn add_to_waitlist() {} | ^^^^^^^^^^^^^^^^^^^^ For more information about this error, try `rustc --explain E0603`. error: could not compile `restaurant` due to 2 previous errors [شيفرة 6: أخطاء المصرّف الناتجة عن بناء الشيفرة 5] ما الذي حصل؟ تجعل إضافة الكلمة المفتاحية pub أمام mod hosting من الوحدة وحدةً عامةً، وبهذا التغيير إن أمكننا الوصول إلى front_of_house، فهذا يعني أنه يمكننا الوصول إلى hosting، ولكن محتوى hosting ما زال خاصًّا، إذ أن تحويل الوحدة إلى وحدة عامة لا يجعل من محتواها عامًا أيضًا، إذ أن استخدام الكلمة المفتاحية pub على وحدة ما يسمح الوحدات الأب بالإشارة إلى الشيفرة البرمجية فقط وليس الوصول إلى الشيفرة البرمجية الداخلية، ولأن الوحدات هي بمثابة حاويات فليس هناك الكثير لفعله بجعل الوحدة فقط عامة، وسنحتاج إلى جعل عنصر واحد أو أكثر من عناصرها عامًا أيضًا. تدلنا الأخطاء الموجودة في الشيفرة 6 إلى أن الدالة add_to_waitlist هي دالة خاصة، وتُطبَّق قوانين الخصوصية على الهياكل والتعدادات والتوابع والدوال كما هو الحال مع الوحدات. دعنا نجعل من الدالة add_to_waitlist دالة عامة بإضافة الكلمة المفتاحية pub قبل تعريفها كما هو موضح في الشيفرة 7. اسم الملف: src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } pub fn eat_at_restaurant() { // مسار مطلق crate::front_of_house::hosting::add_to_waitlist(); // مسار نسبي front_of_house::hosting::add_to_waitlist(); } [الشيفرة 7: إضافة الكلمة المفتاحية pub إلى mod hosting و fn addtowaitlist مما يسمح لنا باستدعاء الدالة من eatatrestaurant] يمكننا الآن تصريف الشيفرة البرمجية بنجاح. دعنا ننظر إلى المسارات المطلقة والمسارات النسبية حتى نفهم لماذا تسمح لنا إضافة pub باستخدام هذه المسارات في add_to_waitlist مع مراعاة قوانين الخصوصية. نبدأ بكتابة crate في المسار المطلق وهو جذر شجرة الوحدة الخاصة بالوحدة المصرّفة. تُعرّف الوحدة front_of_house في جذر الوحدة المصرّفة، إلا أنها ليست عامة، وذلك لأن الدالة eat_at_restaurant معرفة في الوحدة ذاتها الخاصة بالوحدة front_of_house (أي أن eat_at_restaurant و front_of_house أشقاء)، ويُمكننا الإشارة إلى front_of_house من eat_at_restaurant. تاليًا تبدو الوحدة hosting معلّمة marked بفضل الكلمة المفتاحية pub، إذ يمكننا الوصول إلى الوحدة الأب الخاصة بالوحدة hosting، وبالتالي يمكننا الوصول إلى hosting، وأخيرًا، الدالة add_to_waitlist مُعلّمة بالكلمة المفتاحية pub، وبالتالي يعمل بقية المسار ويُعد استدعاء الدالة هذا صالحًا. نعتمد في المسار النسبي نفس المنطق في المسار المطلق باستثناء الخطوة الأولى، إذ بدلًا من البدء بجذر الوحدة المصرّفة، يبدأ المسار من الوحدة front_of_house، التي تُعرّف في الوحدة ذاتها الخاصة بالوحدة eat_at_restaurant، وبالتالي يبدأ المسار النسبي من الوحدة التي يعمل عندها تعريف الوحدة eat_at_restaurant. تبدو الوحدة hosting و add_to_waitlist معلّمة بفضل الكلمة المفتاحية pub، ولذلك يعمل بقية المسار ويُعد استدعاء الدالة هذا صالحًا. إذا أردت مشاركة وحدة المكتبة المصرّفة الخاصة بك بحيث تستفيد مشاريع أخرى من الشيفرة البرمجية الخاصة بالوحدة المصرّفة، ستكون واجهتك البرمجية API العامة هي نقطة الوصل بين مستخدمي الوحدة المصرّفة ومحتوياتها وهي التي تحدد كيفية تفاعل المستخدمين مع شيفرتك البرمجية، وهناك الكثير من الأشياء التي يجب وضعها في الحسبان لإدارة التغييرات التي تجريها على الواجهة البرمجية العامة حتى تجعل عملية الاعتماد على وحدتك المصرّفة أسهل بالنسبة للمستخدمين، إلا أن هذه الأشياء خارج نطاق موضوعنا هنا، وإذا كنت مهتمًا بهذا الخصوص عليك رؤية دليل رست الخاصة بالواجهات البرمجية. الممارسات المثلى للحزم التي تحتوي على وحدات ثنائية مصرفة ووحدات مكتبة مصرفة ذكرنا أنه يمكن أن تحتوي الحزمة على كلٍّ من جذر وحدة ثنائية مصرّفة src/main.rs إضافةً إلى جذر وحدة مكتبة مصرّفة src/lib.rs وأن يحمل كلتا الوحدتين المصرّفتين اسم الحزمة افتراضيًا، ويحتوي هذا النوع من الحزم عادةً على شيفرة برمجية كافية داخل الوحدة الثنائية المصرّفة، بحيث يمكن إنشاء ملف تنفيذي باستخدامها يستدعي الشيفرة البرمجية الموجودة في وحدة المكتبة المصرّفة، مما يسمح للمشاريع الأخرى بالاستفادة القصوى من المزايا التي تقدمها هذه الحزمة وذلك لأنه من الممكن مشاركة الشيفرة البرمجية الموجودة داخل وحدة المكتبة المصرّفة. يجب أن تُعرَّف شجرة الوحدة في الملف src/lib.rs، ومن ثمّ يمكننا استخدام أي عنصر عام في الوحدة الثنائية المُصرّفة ببدء المسار باسم الحزمة، وبذلك تصبح الوحدة الثنائية المصرّفة مستخدمًا user لوحدة المكتبة المصرّفة كما تستخدم أية وحدة مصرّفة خارجية وحدة مكتبة مصرّفة عادةً، وذلك باستخدام الواجهة البرمجية العامة فقط. يساعدك ذلك في تصميم واجهة برمجية جيّدة؛ إذ أنك لست كاتب المكتبة فقط في هذه الحالة، بل أنك مستخدمها أيضًا. سنستعرض الممارسات الشائعة في تنظيم برامج سطر الأوامر التي تحتوي على كل من الوحدة الثنائية المصرّفة ووحدة المكتبة المصرّفة لاحقًا. كتابة المسارات النسبية باستخدام super يُمكننا إنشاء مسار نسبي يبدأ في الوحدة الأب بدلًا من الوحدة الحالية أو جذر وحدة مصرّفة باستخدام الكلمة المفتاحية super في بداية المسار، وهذا يُشابه بدء مسار الملفات بالنقطتين ..، إذ يسمح لنا استخدام super بالإشارة إلى عنصر نعلم أنه موجود في الوحدة الأب، مما يجعل إعادة ترتيب شجرة الوحدة أسهل عندما تكون الوحدة مرتبطة بصورةٍ وثيقة مع الوحدة الأب مع احتمال نقل الوحدة الأب إلى مكان آخر ضمن شجرة الوحدة في يوم ما. ألقِ نظرةً إلى الشيفرة 8 التي تصف موقفًا معينًا، ألا وهو تصحيح الطاهي لطلب غير صحيح وإحضاره بنفسه شخصيًا إلى الزبون. تستدعي الدالة fix_incorrect_order المُعرفة في الوحدة back_of_house الدالة deliver_order المعرّفة في الوحدة الأب بتحديد المسار إلى الدالة deliver_order باستخدام الكلمة المفتاحية super في البداية: اسم الملف: src/lib.rs fn deliver_order() {} mod back_of_house { fn fix_incorrect_order() { cook_order(); super::deliver_order(); } fn cook_order() {} } [الشيفرة 8: استدعاء دالة باستخدام المسار النسبي بدءًا بالكلمة super] تتواجد الدالة fix_incorrect_order في الوحدة back_of_house، لذا يمكننا استخدام super للإشارة إلى الوحدة الأب الخاصة بالوحدة back_of_house التي هي في هذه الحالة الوحدة crate الجذر، ومن هناك نبحث عن deliver_order ونجده بنجاح. نعتقد هنا أن الوحدة back_of_house والدالة deliver_order سيبقيان سويًا نظرًا للعلاقة فيما بينهما مهما تغيّر تنظيم شجرة الوحدة، وبالتالي استخدمنا super بحيث نختصر على أنفسنا أماكن تحديث الشيفرة البرمجية في المستقبل إذا قرّرنا نقل الشيفرة البرمجية إلى وحدة مختلفة. جعل الهياكل والتعدادات عامة يمكننا أيضًا استخدام الكلمة المفتاحية pub لتعيين الهياكل structs والتعدادات enums على أنها عامة، إلا أن هناك بعض التفاصيل الإضافية لاستخدام pub مع الهياكل والتعدادات؛ فإذا استخدمنا pub قبل تعريف الهيكل، فسيتسبب هذا بإنشاء هيكل عام إلا أن حقول هذا الهيكل ستظلّ خاصة، ويمكن جعل كل حقل عامًا أو لا اعتمادًا على أسس وشروط كل حالة بحد ذاتها. نُعرّف في الشيفرة 9 هيكلًا عامًا باسم back_of_house::Breakfast بحقل عام toast وحقل خاص seasonal_fruit، يُحاكي هذا الأمر عملية اختيار الزبون لنوع الخبز الذي يأتي مع الوجبة واختيار الطاهي لنوع الفاكهة الذي يأتي مصحوبًا مع الوجبة بناءً على المتاح في مخزون المطعم؛ ولأن الفاكهة المُتاحة تتغير سريعًا حسب المواسم فالزبون لا يستطيع اختيار الفاكهة أو حتى رؤية النوع الذي سيحصل عليه. اسم الملف: src/lib.rs mod back_of_house { pub struct Breakfast { pub toast: String, seasonal_fruit: String, } impl Breakfast { pub fn summer(toast: &str) -> Breakfast { Breakfast { toast: String::from(toast), seasonal_fruit: String::from("peaches"), } } } } pub fn eat_at_restaurant() { // Rye طلب فطور في الصيف مع خبز محمص من النوع راي let mut meal = back_of_house::Breakfast::summer("Rye"); // غيّرنا رأينا بخصوص الخبز الذي نريده meal.toast = String::from("Wheat"); println!("I'd like {} toast please", meal.toast); // لن يُصرَّف السطر التالي إذا أزلنا تعليقه لأنه من غير المسموح رؤية أو تعدي الفاكهة الموسمية التي تأتي مع الوجبة // meal.seasonal_fruit = String::from("blueberries"); } [الشيفرة 9: هيكل يحتوي على بعض الحقول العامة والخاصة] بما أن الحقل toast في الهيكل back_of_house::Breakfast هو حقل عام، فهذا يعني أننا نستطيع الكتابة إلى والقراءة من الحقل toast في eat_at_restaurant باستخدام النقطة. لاحظ أنه لا يمكننا استخدام الحقل seasonal_fruit في eat_at_restaurant وذلك لأن الحقل seasonal_fruit هو حقل خاص. حاول إزالة تعليق السطر البرمجي الذي يُعدّل من قيمة الحقل seasonal_fruit وستحصل على خطأ. لاحظ أن الهيكل back_of_house::Breakfast بحاجة لتوفير دالة عامة مرتبطة به تُنشئ نسخةً من Breakast (سميناها summer في هذا المثال)، لأنه يحتوي على حقل خاص، ولن يصبح بالإمكان إنشاء نسخة من الهيكل Breakfast في eat_at_restaurant إذا لم يحتوي على دالة مشابهة، وذلك لأنه لن يكون بإمكاننا ضبط قيمة للحقل الخاص seasonal_fruit في eat_at_restaurant. وفي المقابل، إذا أنشأنا تعدادًا عامًا، ستكون جميع متغايراته variants عامة، ونحن بحاجة لاستخدام الكلمة المفتاحية pub مرةً واحدةً فقط قبل الكلمة enum كما هو موضح في الشيفرة 10. اسم الملف: src/lib.rs mod back_of_house { pub enum Appetizer { Soup, Salad, } } pub fn eat_at_restaurant() { let order1 = back_of_house::Appetizer::Soup; let order2 = back_of_house::Appetizer::Salad; } [الشيفرة 10: تحديد المعدد على أنه عام يجعل جميع متغايراته عامة] يمكننا استخدام المتغايرات Soup و Salad في eat_at_restaurant وذلك لأننا أنشأنا التعداد Appetier على أنه تعداد عام. ليست التعدادات مفيدة إلا إذا كانت جميع متغايراتها عامة، وسيكون من المزعج تحديد كل متغاير من متغايرات المعدد بكونه متغاير علني باستخدام pub، لذا فإن حالة متغايرات التعداد الافتراضية هي أن تكون عامة. على النقيض، يمكن أن تكون الهياكل مفيدة دون أن يكون أحد حقولها عامًا وبالتالي تتبع الهياكل القاعدة العامة التي تنص على أن جميع العناصر خاصة إلا إذا أُشير إلى العنصر على أنه عام باستخدام pub. هناك بعض الحالات الأخرى بالتي تتضمن pub التي لم نناقشها بعد، ألا وهي آخر مزايا نظام الوحدات: الكلمة المفتاحية use، والتي سنغطّيها تاليًا، ومن ثمّ سنناقش كيفية دمج pub و use معًا. ترجمة -وبتصرف- لقسم من الفصل Managing Growing Projects with Packages, Crates, and Modules من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: الحزم packages والوحدات المصرفة crates في لغة رست Rust بنية match للتحكم بسير برامج لغة رست Rust الملكية Ownership في لغة رست
  19. سنُغطّي أولى أجزاء نظام الوحدة ألا وهو الحزم packages والوحدات المصرّفة crates. الوحدة مُصرَّفة هي الجزء الأصغر من الشيفرة البرمجية التي يستطيع المصرّف التعرف عليها في المرة الواحدة. حتى إذا شغّلنا rustc بدلًا من cargo ومررنا ملفًا مصدريًا للشيفرة، سيتعامل المصرّف مع هذا الملف على أنه وحدة مُصرَّفة. يمكن أن تحتوي الوحدات المُصرَّفة وحدات modules يمكن أن تكون معرّفة في ملفات أخرى مُصرّفة مع تلك الوحدة المُصرَّفة، كما سنرى في الأقسام التالية. يُمكن أن تكون الوحدة المُصرَّفة ثنائية binary أو أن وحدة مكتبة مُصرَّفة library، وتُعد الوحدات المُصرَّفة الثنائية برامجًا يُمكنك تصريفها إلى ملف تنفيذي ومن ثم تشغيلها مثل برامج سطر الأوامر command line programs أو الخوادم servers، ويجب أن تحتوي الوحدات المُصرَّفة الثنائية على دالة تدعى main تُعرِّف ما الذي يحدث عند تشغيل الملف التنفيذي، وجميع الوحدات المُصرَّفة التي أنشأناها حتى اللحظة هي وحدات مصرّفة ثنائية. لا تحتوي الوحدات المكتبية المصرّفة على دالة main ولا تُصرَّف إلى ملف تنفيذي، وإنما تعرِّف وظائف بُنيَت بهدف الاستفادة منها من خلال مشاركتها من قبل عدّة مشاريع، ونذكر منها على سبيل المثال وحدة المكتبة المصرفة rand المُستخدمة في مقال لُعبة تخمين الأرقام السابق، والتي تقدّم خاصية توليد الأرقام العشوائية. عندما يذكر مستخدمو رست مصطلح وحدة مصرّفة، فهم يقصدون وحدة المكتبة المصرّفة، ويعتمدون مفهوم الوحدة المصرّفة على نحو تبادلي لمصطلح "المكتبة" السائد في مفهوم البرمجة العام. وحدة الجذر المصرّفة crate root هي الملف المصدري الذي يبدأ منه مصرّف رست ويشكّل وحدة الجذر للوحدة المصرّفة crate، وسنشرح الوحدات بتفصيلٍ أكبر في الفقرات اللاحقة. الحزمة هي مجمع لوحدة مصرّفة واحدة أو أكثر، وتقدّم مجموعةً من الوظائف، وتحتوي على ملف "Cargo.toml" يصِف كيفية بناء الوحدات المصرّفة داخله. كارجو Cargo هي حزمة تحتوي على وحدة ثنائية مُصرّفة مخصصة لأداة سطر الأوامر المُستخدمة لبناء شيفرتك البرمجية، وتحتوي أيضًا على وحدة مكتبة مصرّفة تعتمد عليها الوحدة الثنائية المُصرّفة. يمكن أن تعتمد المشاريع الأخرى على وحدة مكتبة كارجو المُصرّفة لاستخدام نفس المنطق المُستخدم في أداة سطر أوامر كارجو. هناك عدّة قواعد تُملي ما على الحزمة أن تحتويه، إذ يمكن أن تحتوي الحزمة على وحدة مكتبة مُصرّفة واحدة فقط، ويمكن أن تحتوي على عدد من الوحدات الثنائية المُصرّفة بحسب حاجتك إلا أنها يجب أن تحتوي على الأقل على وحدةٍ مصرّفة واحدةٍ على الأقل بغضّ النظر عن نوعها سواءٌ كانت وحدة ثنائية أو وحدة مكتبة. دعنا ننظر ما الذي يحدث عندما نُنشئ حزمة. نُدخل أوّلًا الأمر cargo new: $ cargo new my-project Created binary (application) `my-project` package $ ls my-project Cargo.toml src $ ls my-project/src main.rs بعد تنفيذ الأمر السابق، نستخدم الأمر ls لنرى ماذا أنشأ كارجو، إذ سنجد ضمن مجلد المشروع ملفًا اسمه Cargo.toml، والذي يمنحنا حزمة، ونجد أيضًا مجلد "src" يحتوي على الملف "main.rs". وإذا نظرنا إلى Cargo.toml فلن نجد هناك أي ذكر للملف src/main.rs، وذلك لأن كارجو يتبع اصطلاحًا معيّنًا بحيث يكون src/main.rs الوحدة الجذر للوحدة الثنائية المصرّفة باستخدام نفس اسم الحزمة. يعلم كارجو أيضًا أنه إذا احتوى مجلد الحزمة على src/lib.rs فهذا يعني أن الحزمة تحتوي على وحدة مكتبة مصرّفة باسم الحزمة ذاته و src/lib.rs هي الوحدة الجذر في هذه الحالة. يُمرّر كارجو ملفات الوحدة الجذر المصرّفة إلى rustc لبناء وحدة مكتبة مصرّفة أو وحدة ثنائية مصرّفة. لدينا هنا في هذه الحالة حزمة تحتوي src/main.rs فقط، وهذا يعني أنها تحتوي وحدة ثنائية مُصرّفة تُدعى my-project، وفي حالة احتواء المشروع على src/main.rs و src/lib.rs في ذات الوقت فهذا يعني أنه يحتوي على وحدتين مصرفتين، وحدة مكتبة مصرّفة ووحدة ثنائية مُصرّفة ويحتوي كلاهما على الاسم ذاته الخاص بالحزمة، ويمكن أن تحتوي الحزمة عدّة وحدات ثنائية مُصرّفة من خلال وضع الملفات في المجلد src/bin، بحيث يُمثّل كل ملف داخل هذا المجلد وحدة ثنائية مُصرّفة منفصلة. . تعريف الوحدات للتحكم بالنطاق والخصوصية سننتقل في هذا القسم للتكلم عن الوحدات modules والأجزاء الأخرى من نظام الوحدة، والتي تُسمى المسارات paths، وهي تسمح لك بتسمية العناصر؛ وسنتطرق أيضًا إلى الكلمة المفتاحية use التي تُضيف مسارًا إلى النطاق؛ والكلمة المفتاحية pub التي تجعل من العناصر عامة public؛ كما سنناقش الكلمة المفتاحية as والحزم الخارجية وعامل glob. لكن أولًا دعنا نبدأ بمجموعة من القوانين التي تُساعدك بتنظيم شيفرتك البرمجية مستقبلًا، ومن ثمّ سنشرح كل قاعدة من القواعد بالتفصيل. ورقة مرجعية للوحدات إليك كيف تعمل كل من المسارات والوحدات وكلمتَي use و pub المفتاحيتَين في المصرّف وكيف يُنظّم المطوّرون شيفرتهم البرمجية. سنستعرض مثالًا عن كلٍ من القواعد الموجودة أدناه وقد ترى هذه الفقرة مفيدةً ويمكن استعمالها مثل مرجع سريع في المستقبل لتذكّرك بكيفية عمل الوحدات. ابدأ من وحدة الجذر المصرّفة root crate: عند تصريف وحدة مصرّفة ما، ينظر المصرّف أولًا إلى ملف وحدة الجذر المُصرّفة (عادةً src/lib.rs لوحدة المكتبة المصرّفة و src/main.rs للوحدة الثنائية المصرّفة). التصريح عن الوحدات: يُمكنك التصريح في ملف وحدة الجذر المصرّفة عن وحدة جديدة باسم معيّن وليكن "garden" بكتابة السطر البرمجي mod garden;‎، وسيبحث عندها المصرّف عن الشيفرة البرمجية داخل الوحدة في هذه الأماكن: ضمن السطر ذاته inline: أي ضمن السطر الخاص بالتعليمة mod garden ضمن الأقواس المعقوصة عوضًا عن الفاصلة المنقوطة. في الملف src/garden.rs. في الملف src/garden/mod.rs. التصريح عن الوحدات الفرعية submodules: يُمكنك التصريح عن وحدات فرعية لأي ملف غير وحدة الجذر المصرّفة، إذ يمكنك مثلًا التصريح عن mod vegtables في الملف src/garden.rs، وسيبحث المصرّف عن شيفرة الوحدة الفرعية في المجلد المُسمى للوحدة الأصل في هذه الأماكن: ضمن السطر ذاته inline: أي ضمن السطر الخاص بالتعليمة mod vegetables ضمن الأقواس المعقوصة عوضًا عن الفاصلة المنقوطة. في الملف src/garden/vegetables.rs. في الملف src/garden/vegetables/mod.rs. المسارات التي ستُشفّر في الوحدات: بمجرد أن تصبح الوحدة جزءًا من الوحدة المصرّفة، يمكنك الرجوع إلى الشيفرة البرمجية الموجودة في تلك الوحدة من أي مكانٍ آخر في نفس الوحدة المصرّفة، طالما تسمح قواعد الخصوصية بذلك، وذلك باستخدام المسار الواصل إلى هذه الشيفرة. يمكنك مثلًا العثور على نوع Asparagus في وحدة الخضار عند المسار crate::garden::vegetables::Asparagus. الخاص private والعام public: الشيفرة البرمجية الموجودة داخل الوحدة هي خاصة بالنسبة للوحدة الأصل افتراضيًا، ولجعل الوحدة عامة يجب أن نصرح عنها باستخدام pub mod بدلًا من mod، ولجعل العناصر الموجودة داخل الوحدة العامة عامة أيضًا نستخدم pub قبل التصريح عنها. الكلمة use المفتاحية: تُنشئ الكلمة المفتاحية use داخل النطاق اختصارًا للعناصر لتجنب استخدام المسارات الطويلة، إذ يمكنك في أي نطاق اختصار المسار crate::garden::vegetables::Asparagus باستخدام use عن طريق كتابة use crate::garden::vegetables::Asparagus;‎ ومن ثمّ يمكنك كتابة Asparagus مباشرةً ضمن النطاق دون الحاجة لكتابة كامل المسار. إليك وحدة ثنائية مصرّفة اسمها backyard توضح القوانين السابقة. نُسمّي مسار الوحدة المصرّفة أيضًا backyard ويحتوي ذلك المسار على هذه الملفات والمسارات: backyard ├── Cargo.lock ├── Cargo.toml └── src ├── garden │ └── vegetables.rs ├── garden.rs └── main.rs في هذه الحالة، يحتوي ملف وحدة الجذر المصرّفة src/main.rs على التالي: اسم الملف: src/main.rs use crate::garden::vegetables::Asparagus; pub mod garden; fn main() { let plant = Asparagus {}; println!("I'm growing {:?}!", plant); } يعني السطر البرمجي pub mod garden;‎ بأن المصرّف سيضمّ الشيفرة البرمجية التي سيجدها في src/garden.rs، وهي: اسم الملف: src/garden.rs pub mod vegetables; ويعني السطر pub mod vegetables;‎ بأنّ الشيفرة البرمجية الموجودة في الملف src/garden/vegetables.rs ستُتضمّن أيضًا: #[derive(Debug)] pub struct Asparagus {} لننظر إلى ما سبق عمليًا مع بتطبيق القوانين التي ذكرناها سابقًا. ‎تجميع الشيفرات البرمجية المرتبطة ببعضها في الوحدات تسمح لنا الوحدات بتنظيم الشيفرة البرمجية ضمن وحدات مصرّفة على شكل مجموعات لزيادة سهولة القراءة والاستخدام، وتتحكم الوحدات أيضًا بخصوصية privacy العناصر داخلها، لأن الشيفرات داخل الوحدات خاصة افتراضيًا، والعناصر الخاصة هي تفاصيل داخلية تطبيقية خاصة وغير مُتاحة للاستخدام الخارجي. يمكننا اختيار إنشاء وحدات وعناصر وجعلها عامة، وهذا يسمح باستخدام الشيفرات الخارجية والاعتماد عليها. دعنا نكتب وحدة مكتبة مصرّفة تزوّدنا بخصائص مطعم مثلًا، وسنعرّف داخلها بصمات signatures الدوال، لكن سنترك محتوى كل منها فارغًا لنركّز على جانب تنظيم الشيفرة البرمجية بدلًا من التطبيق الفعلي لمطعم. يُشار في مجال المطاعم إلى بعض الأجزاء بكونها أجزاء على الواجهة الأمامية front of house بينما يُشار إلى أجزاء أخرى بأجزاء في الخلفية back of house؛ وأجزاء الواجهة الأمامية هي حيث يتواجد الزبائن، أي جانب حجز الزبائن للمقاعد وأخذ الطلبات والمدفوعات منهم وتحضير النادل للمشروبات؛ بينما أجزاء الواجهة هي حيث يتواجد الطهاة وتُطهى الأطباق وتُنظّف الصحون والأعمال الإدارية التي يجريها المدير. من أجل هيكلة الوحدة المصرّفة بطريقة مماثلة للمطعم، نلجأ لتنظيم الدوال في وحدات متداخلة. لننشئ مكتبةً جديدة ونسميها restaurant بتنفيذ الأمر cargo new restaurant --lib، ثمّ نكتب الشيفرة 1 داخل الملف src/lib.rs لتعريف بعض الوحدات وبصمات الدوال. اسم الملف: src/lib.rs mod front_of_house { mod hosting { fn add_to_waitlist() {} fn seat_at_table() {} } mod serving { fn take_order() {} fn serve_order() {} fn take_payment() {} } } [الشيفرة 1: وحدة frontofhouse تحتوي على وحدات أخرى، وتحتوي هذه الوحدات الأخرى بدورها على دوال] نعرّف الوحدة عن طريق البدء بكتابة الكلمة المفتاحية mod ومن ثم تحديد اسم الوحدة (في حالتنا هذه الاسم هو front_of_house) ونضيف بعد ذلك أقواص معقوصة حول متن الوحدة. يمكننا إنشاء وحدات أخرى داخل وحدة ما وهذه هي الحالة مع الوحدات hosting و serving، يمكن للوحدات أيضًا أن تحتوي داخلها على تعريفات لعناصر أخرى، مثل الهياكل structs والتعدادات enums والثوابت constants والسمات traits أو الدوال كما هو موجود في الشيفرة 1. يمكننا تجميع التعريفات المرتبطة ببعضها بعضًا باستخدام الوحدات وتسمية العامل الذي يربطهم، وبالتالي سيكون من الأسهل للمبرمجين الذين يستخدمون الشيفرة البرمجية هذه أن يعثروا على التعريفات التي يريدوها لأنه من الممكن تصفح الشيفرة البرمجية بناءً على المجموعات بدلًا من قراءة كل التعريفات، كما أنه سيكون من السهل إضافة مزايا جديدة إلى الشيفرة البرمجية لأن المبرمج سيعرف أين يجب إضافة الشيفرة البرمجية بحيث يحافظ على تنظيمها. ذكرنا سابقًا أن src/main.rs و src/lib.rs هي وحدات جذر مُصرَّفة، والسبب في تسميتهما بذلك هو أن محتويات أيّ منهما يشكّل وحدةً تدعى crate في جذر هيكل الوحدة الخاص بالوحدة المصرّفة وكما تُعرف أيضًا بشجرة الوحدة module tree. توضح الشيفرة 2 شجرة الوحدة الخاصة بهيكل الشيفرة 1. crate └── front_of_house ├── hosting │ ├── add_to_waitlist │ └── seat_at_table └── serving ├── take_order ├── serve_order └── take_payment [الشيفرة 2: شجرة الوحدة للشيفرة 1] توضح الشجرة بأن بعض الوحدات متداخلة مع وحدات أخرى (على سبيل المثال الوحدة hosting متداخلة مع front_of_house)، كما توضح الشجرة أيضًا أن بعض الوحدات أشقاء siblings لبعضها، بمعنى أنها معرفة داخل الوحدة ذاتها (hosting و serving معرفتان داخل front_of_house). لإبقاء تشبيه شجرة العائلة، إذا كانت الوحدة (أ) محتواة داخل الوحدة (ب) نقول بأن الوحدة (أ) هي ابن child للوحدة (ب) وأن الوحدة (ب) هي الأب parent للوحدة (أ). لاحظ أن كامل الشجرة محتواة داخل الوحدة الضمنية المسماة crate. قد تذكرك شجرة الوحدة بشجرة مسارات الملفات على حاسبك، وهذه مقارنة ملائمة، إذ أننا نستخدم الوحدات لتنظيم الشيفرة البرمجية بنفس الطريقة التي نستخدم فيها المجلدات لتنظيم الملفات، وكما هو الحال في الملفات داخل المجلدات نحتاج إلى طريق لإيجاد الوحدات. ترجمة -وبتصرف- لقسم من الفصل Managing Growing Projects with Packages, Crates, and Modules من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: بنية match للتحكم بسير برامج لغة رست Rust استخدام التوابع methods ضمن الهياكل structs في لغة رست Rust استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست
  20. لدى رست بنية construct فعالة جدًا للتحكم بسير البرنامج وتدعى ببنية match، إذ تسمح لك هذه البنية بمقارنة قيمة مع مجموعة من الأنماط، ثم تنفيذ شيفرة برمجية بناءً على النمط الموافق لهذه القيمة، ويمكن أن يكون النمط مشكلًا من قيمًا مجرّدة literal values، أو أسماء متغيرات، أو محرف بدل wildcard، أو أي شيء آخر، وسنتكلم لاحقًا عن جميع الأنواع المختلفة من الأنماط وعمل كل منها. تأتي قوة البنية match من قابلية التعبير الواضح عن الأنماط ومن أن المتصرف يتحقق من أن جميع الحالات الممكنة قد جرى التعامل معها. انظر إلى تعبير البنية match بكونه آلةً لترتيب القطع النقدية المعدنية، إذ تدخل القطعة النقدية إلى مسار يحتوي على ثقوب متفاوتة الأحجام بحيث تسقط كل قطعة نقدية داخل الثقب الذي تتسع داخله، تدخل المتغيرات بالطريقة ذاتها للقطع النقدية ويجري فحصها بالنسبة لكل نمط موجود في البنية match و"يدخل" المتغير إلى أول كتلة برمجية يوافق نمطها مع المتغير حتى يُستخدم المتغير داخل تلك الكتلة البرمجية، وبما أننا اتخذنا القطع النقدية مثالًا للشرح دعنا نطّبق ذلك في برنامجنا باستخدام البنية match، إذ يُمكننا كتابة دالة تأخذ عددًا غير معين من القطع النقدية الأمريكية بطريقة مماثلة لآلة عد النقود لتحديد قيمة كل قطعة نقدية ومن ثم تُعيد قيمتها بالسنت cent، وتوضح الشيفرة 3 هذا البرنامج. enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } [الشيفرة 3: تعداد enum وتعبير match يحتوي على جميع متغايرات التعداد مثل أنماط] دعنا نشرح التعبير match بالتفصيل في الدالة value_in_cents؛ إذ يبدأ أولًا بكتابة الكلمة المفتاحية match متبوعة بتعبير وهو قيمة القطعة النقدية coin في هذه الحالة، ويبدو هذا الأمر مشابهًا لاستخدام تعابير if إلا أن هناك فارقًا كبيرًا؛ إذ يحتاج تعبير if أن يُعيد قيمة بوليانية boolean إلا أن التعبير هنا يمكن أن يُعيد قيمةً من أي نوع، فعلى سبيل المثال نحصل هنا على القيمة coin من نوع التعداد Coin الذي عرفناه في السطر الأول. ننتقل الآن إلى أذرع arms البنية match، إذ يكون للذراع جزءان: نمط وشيفرة برمجية. يحتوي الذراع الأول هنا على نمط من النوع Coin::Penny ومن ثم العامل <= الذي يفصل ما بين النمط والشيفرة البرمجية الواجب تنفيذها، والشيفرة البرمجية في هذه الحالة هي فقط القيمة "1"، ومن ثم يُفصل كل ذراع من الذراع الذي يليه باستخدام الفاصلة. تُقارن نتيجة التعبير match عند تنفيذه مع نمط كل ذراع بالترتيب؛ فإذا كان النمط يوافق القيمة تُنفّذ الشيفرة البرمجية المرتبطة بذلك النمط؛ وإذا لم يوافق القيمة يستمر تنفيذ البرنامج ليفحص الذراع التالية كما هو الحال في آلة عد القطع النقدية. يمكننا إضافة أذرع أخرى بقدر ما نريد، إذ تحتوي match في الشيفرة 3 على أربعة أذرع. الشيفرة البرمجية المرتبطة بكل ذراع هي تعبير، ونتيجة ذلك التعبير في الذراع الموافقة للقيمة هي القيمة التي تُعاد من تعبير match الكامل. لا تُستخدم عادةً الأقواس المعقوصة curly brackets، إذا كانت الشيفرة البرمجية في الذراع قصيرة كما هو الحال في الشيفرة 3، إذ تُعيد الشيفرة قيمة واحدة فقط مباشرةً، إلا أنه يجب علينا استخدام الأقواس المعقوصة إذا أردنا تنفيذ عدة أسطر برمجية داخل ذراع البنية match، ويكون استخدام الفاصلة بعد الذراع عندئذٍ اختياريًا. على سبيل المثال، تطبع الشيفرة البرمجية في المثال التالي "Lucky penny!‎" كل مرة يُستدعى فيها التابع باستخدام القيمة Coin::Penny إلا أنها ما زالت تُعيد أيضًا القيمة "1" في نهاية الكتلة البرمجية: fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } الأنماط المرتبطة مع القيم لذراع البنية match ميزةٌ مفيدةٌ أخرى، وهي قدرة ربط أجزاء من القيمة لتوافق النمط، وهذه هي الطريقة المتبعة عندما نريد الحصول على قيم من متغايرات التعدادات. دعنا نعدل مثلًا متغايرات التعداد السابق بحيث نستطيع تخزين بعض البيانات داخله. سكّت الولايات المتحدة من العام 1999 إلى 2008 قطعًا نقدية بقيمة ربع دولار بتصاميم مختلفة لكلٍ من الولايات الخمسين على أحد الجوانب، ولا يوجد أي قطع نقدية أخرى تحتوي على تصاميم خاصة بالولايات. إذًا، نحتاج وضع القيمة الإضافية فقط في الأرباع، ويمكننا إضافة هذه المعلومة داخل التعداد enum بتغيير المتغاير Quarter بحيث يحتوي على القيمة UsState مخزنةً بداخله وتلك العملية موضحة في الشيفرة 4. #[derive(Debug)] // حتى نستطيع معاينة الولاية لاحقًا enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } [الشيفرة 4: تعداد Coin يحتوي فيه المتغاير Quarter على القيمة UsState] دعنا نتخيل أن صديقًا من أصدقائك يحاول جمع الأرباع الخمسين جميعها، ولمساعدته نستدعي اسم الولاية المرتبطة بكل ربع عند ترتيب القطع النقدية، بحيث يضيف صديقك ربعًا جديدًا إلى مجموعته إذا صادفنا ربعًا يعود لولاية ما لا يمتلكها. نُضيف في تعبير match ضمن هذه الشيفرة البرمجية متغيرًا يدعى state إلى النمط الذي يطابق قيمة المتغاير Coin::Quarter وعندما تتطابق القيمة مع النوع Coin::Quarter يُربَط المتغير state مع قيمة ولاية الربع، ثم يصبح بإمكاننا استخدام state في الشيفرة البرمجية ضمن الذراع الخاصة بها كما يلي: fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {:?}!", state); 25 } } } سيأخذ المتغير coin القيمة Coin::Quarter(UsState::Alaska)‎ إذا استدعينا value_in_cents(Coin::Quarter(UsState::Alaska))‎ وعندما نقارن تلك القيمة مع كل من أذرع البنية match لن يُطابق أي منهم القيمة وسنصل إلى Coin::Quarter(state)‎ وبحلول تلك النقطة سيكون ربط القيمة state مع النوع UsState::Alaska ومن ثم يمكننا استخدام ذلك الربط في تعبير println!‎، مما سيسمح لنا بالحصول على قيمة الولاية الداخلية من متغاير Quarter ضمن التعداد Coin. المطابقة مع Option‎ أردنا سابقًا الحصول على القيمة T الداخلية من الحالة Some عند استخدام Option<T>‎، إلا أنه يمكننا أيضًا التعامل مع Option<T>‎ باستخدام match كما فعلنا في معدد Coin؛ إذ سنقارن متغايرات Option<T>‎ عوضًا عن مقارنة العملات، إلا أن طريقة عمل تعبير match ستبقى كما هي. دعنا نقول أننا نريد كتابة دالة تأخذ Option<i32>‎ مثل وسيط وتُضيف إلى قيمته "1" إذا كان هناك قيمةٌ داخله، وإذا لم يحتوي الوسيط على قيمة يجب أن تُعيد الدالة القيمة None وألّا تُجري أي عمليات أخرى. الدالة سهلة الكتابة جدًا وذلك بفضل match وستبدو كما هو موضح في الشيفرة 5. fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), } } let five = Some(5); let six = plus_one(five); let none = plus_one(None); [الشيفرة 5: دالة تستخدم تعبير match على المتغير Option‎] دعنا نفحص التنفيذ الأول للدالة plus_one بالتفصيل، إذ يأخذ المتغير x الموجود داخل دالة plus_one القيمة Some(5)‎ عند الاستدعاء plus_one(five)‎ ومن ثم نقارن تلك القيمة مع كل ذراع ضمن match. None => None, لا تُطابق القيمة Some(5)‎ النمط الأول None التالي، لذا نستمر بمحاولة النمط الذي يليه. Some(i) => Some(i + 1), هل يُطابق Some(5)‎ النمط Some(i)‎؟ نعم. لدينا المتغاير ذاته وترتبط i بالقيمة التي تحتويها Some وبالتالي يأخذ i القيمة 5. تُنفّذ الشيفرة البرمجية الموجودة في ذراع match الموافقة، وبالتالي نُضيف "1" إلى قيمة i ونُنشأ قيمة Some جديدة باستخدام الناتج "6" الإجمالي داخله. دعنا ننظر إلى الاستدعاء الثاني للدالة plus_one في الشيفرة 5، إذ يأخذ المتغير x القيمة None. ندخل البنية match ونُقارن مع الذراع الأول. None => None, حصلنا على مطابقة. ليس هناك أي قيمة لنضيفها لذا يتوقف البرنامج ويُعيد القيمة None على يمين المعامل <= ولا تحدث أي مقارنة أخرى لأننا حصلنا على مطابقة مع الذراع الأولى. استخدام البنية match مع التعدادات مفيد في العديد من الحالات، وستجد نمط استخدام البنية match مع التعداد شائعًا في لغة رست، إذ يُربط متغير إلى قيمة داخل match، ثم تُنفّذ الشيفرة البرمجية الموافقة، وقد يكون هذا الأسلوب معقدًا بعض الشيء إلا أنك ستتمنى لو أنه موجود في كل لغات البرمجة حالما تعتاد عليه إذ أنه الخيار المفضل لكثير من مبرمجي لغة رست. يجب أن تكون بنى match شاملة هناك جانب آخر من البنية match لم نناقشه بعد. ألقِ نظرةً على الإصدار التالي من دالة plus_one الذي يحتوي على خطأ برمجي ولن يُصرّف: fn plus_one(x: Option<i32>) -> Option<i32> { match x { Some(i) => Some(i + 1), } } لم نشمل حالة None في بنية match السابقة وبالتالي ستتسبب شيفرتنا السابقة بخطأ برمجي، إلا أن رست تنبهنا على هذا الخطأ لحسن الحظ، إذ أنك ستحصل على رسالة الخطأ التالية إذا حاولت تصريف الشيفرة البرمجية: $ cargo run Compiling enums v0.1.0 (file:///projects/enums) error[E0004]: non-exhaustive patterns: `None` not covered --> src/main.rs:3:15 | 3 | match x { | ^ pattern `None` not covered | note: `Option<i32>` defined here = note: the matched value is of type `Option<i32>` help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | 4 ~ Some(i) => Some(i + 1), 5 ~ None => todo!(), | For more information about this error, try `rustc --explain E0004`. error: could not compile `enums` due to previous error تعلم رست أننا لم نغطي جميع الحالات الممكنة وحتى أنها تعرف الأنماط التي نسيناها؛ إذ أن الأنماط في لغة رست شاملة بحيث أنها يجب أن تشمل أي حالة ممكنة حتى تكون الشيفرة البرمجية صالحة، وبالأخص في حالة Option<T>‎، إذ تذكرنا رست بالتعامل مع حالة None صراحةً، وتحمينا من افتراض أننا حصلنا على قيمة بينما نحن نملك قيمة فارغة في الحقيقة مما يجعل الخطأ ذو مليارات الدولارات الذي ناقشناه لاحقًا مستحيل الحدوث. التعامل مع جميع الأنماط والمحرف _ المؤقت يمكننا أن نحدد تنفيذ بعض الأمور المميزة لبعض القيم في التعدادات بينما تُنفَّذ شيفرةٌ برمجيةٌ محددة لجميع القيم الممكنة الأخرى. تخيل أننا نبرمج لعبة نرمي فيها النرد وعندما نحصل على القيمة 3 لا يتحرك اللاعب بل يحصل على قبعة جميلة جديدة، بينما إذا حصلت على 7 فإن اللاعب يفقد تلك القبعة الجميلة، ويتحرك اللاعب بباقي الحالات وفقًا للقيمة التي حصلنا عليها من النرد على الرقعة. نحاول تطبيق منطق اللعبة السابقة باستخدام البنية match بحيث يكون نتيجة النرد مكتوبة صراحةً عوضًا عن الحصول عليها عشوائيًا، ونمثل جميع الدوال الأخرى دون متن لها، وذلك لأن التطبيق الفعلي لكل منها خارج نطاق نقاشنا هنا: let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), other => move_player(other), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn move_player(num_spaces: u8) {} نستخدم قيمًا مجرّدة مثل نمط لكل من الذراعين الأوليين، بينما نستخدم نمطًا للذراع الأخيرة يغطّي كل النتائج الأخرى المحتملة؛ والنمط هو اسم المتغير الذي اخترناه ألا وهو other، إذ ستستخدم الشيفرة البرمجية المتغيّر other وتمرره إلى الدالة move_player. تُصرَّف الشيفرة البرمجية السابقة بنجاح على الرغم من أننا لم نُضيف جميع القيم الممكنة التي يمكن تمثيلها باستخدام النوع u8 وذلك لأن النمط الأخير سيطابق أي قيمة لم تُدرج صراحةً قبله، وهذا النمط يحقق شرط البنية match في كونها شاملة على جميع القيم المحتملة. لاحظ أنه يجب علينا إضافة ذراعًا آخرًا لمعالجة جميع الحالات catch-all، لأن الأنماط تُقيَّم بالترتيب، وستحذرنا رست إذا أضفنا أي ذراع آخر بعد ذراع معالجة جميع الحالات، وذلك لأن هذا الذراع الإضافي لن يُطابق إطلاقًا. لدى رست نمطٌ يمكننا استخدامه لاستخدام القيمة التي نحصل عليها من نمط الحصول على جميع الحالات، ألا وهو النمط _ المميز الذي يطابق أي قيمة ولا يُربط مع القيمة. تعلم رست عند استخدامه أننا لن نستخدم القيمة، لذا لن تحذرنا بخصوص المتغير غير المُستعمل. دعنا نغيّر من قوانين اللعبة بحيث يجب عليك إعادة رمي النرد مجددًا إذا حصلت على نتيجة مغايرة عن 3 أو 7، ولا نحتاج هنا لاستخدام القيم صراحةً وكل ما علينا هو وضع المحرف المميز _ بدلًا من اسم المتغيّر other: let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => reroll(), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn reroll() {} تحقق الشيفرة البرمجية السابقة شرط بنية match، الذي ينص على وجوب شموليتها، إذ أننا نتجاهل جميع القيم الأخرى في الذراع الأخير مما يعني أننا لم ننسى أي احتمال. يمكننا استخدام قيمة الواحدة unit value (نوع الصف الفارغ empty tuple type الذي ذكرناه سابقًا) مع ذراع المحرف المميز _، إذا أردنا تغيير قوانين اللعبة مجددًا بحيث لا يحصل أي شيء عندما تحصل على قيمة مغايرة عن القيمة 3 أو 7 : let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => (), } fn add_fancy_hat() {} fn remove_fancy_hat() {} نخبر رست هنا صراحةً أننا لن نستخدم أي قيمة أخرى لا تتوافق مع النمطين السابقين للذراع الأخيرة، وأننا لا نريد تنفيذ أي شيفرة برمجية في هذه الحالة أيضًا. هناك المزيد من الأنماط التي سنغطيها لاحقًا، إلا أننا سنتحدث الآن عن if let التي قد تكون مفيدةً في حالات يكون فيها استخدام التعبير match يتطلب صياغةً طويلة. التحكم بسير البرنامج باستخدام if let تسمح لك if let بجمع كل من if و let بطريقة أكثر اختصارًا مقارنةً باستخدام الأنماط و match مع تجاهل القيم الأخرى التي لا تهمنا. ألقِ نظرةً على البرنامج الموجود في الشيفرة 6 الذي يُطابق قيمة <Option<u8 في المتغير config_max بحثًا عن قيمة واحدة ألا وهي متغاير Some. let config_max = Some(3u8); match config_max { Some(max) => println!("The maximum is configured to be {}", max), _ => (), } [الشيفرة 6: بنية match تنفذ شيفرة برمجية فقط في حالة الحصول على القيمة Some] إذا كانت القيمة هي Some، نطبع القيمة داخل المتغاير Some من خلال ربطها مع المتغير max في النمط، إلا أننا لا نريد فعل أي شيء إذا حصلنا على القيمة None ولتحقيق شرط البنية match بكونها تشمل كل الاحتمالات نُضيف () <= _ بعد معالجة متغاير واحد، وهذا الأسلوب في الكتابة مزعج وهناك بديل أفضل. بدلًا مما سبق، نستطيع اختصار الكتابة عن طريق استخدام if let، وتوضح الشيفرة البرمجية التالية استخدامها، إذ تؤدي الغرض ذاته مقارنةً بالشيفرة السابقة الموجودة في الشيفرة 6 باستخدام match: let config_max = Some(3u8); if let Some(max) = config_max { println!("The maximum is configured to be {}", max); } نستخدم مع if let نمطًا وتعبيرًا مفصولَين بإشارة مساواة، ويعمل هذا بطريقة مماثلة للبنية match، إذ يُعطى التعبير إلى match ويمثّل النمط الذراع الأولى له، وفي هذه الحالة هو Some(max)‎، وبالتالي تُربط قيمة المتغير max إلى القيمة الموجودة داخل Some، ويمكننا بعد ذلك استخدام max داخل كتلة if let بطريقة مماثلة لاستخدامنا max في ذراع البنية match الموافقة، ولن تُنفَّذ كتلة if let إذا لم تطابق القيمة النمط الخاص بها. نقلّل -باستخدامنا if let- من كتابة السطور البرمجية ومحاذاتها ونتفادى كتابة سطور برمجية نمطية مكرّرة، إلا أننا نفقد ميزة التفقد من شمولية الحالات كما هو الحال في بنية match، إذ يعتمد اختيار match أو if let على الشيء الذي تريد تنفيذه باستخدامهما والحالة الموجودة أمامنا، بحيث نقايض الكتابة المختصرة بإمكانية التأكد من شمولية الحالات الموجودة. بكلمات أخرى، يمكنك النظر إلى if let بكونها طريقةً أنيقةً لكتابة البنية match التي تنفّذ الشيفرة البرمجية عندما تتطابق قيمة مع نمط واحد محدّد ومن ثم تتجاهل جميع القيم الأخرى. يمكننا تضمين else مع if let، إذ أن كتلة else مطابقة لحالة _ في تعبير match المساوي لكل من if let و else. تذكّر مثال تعريف التعداد Coin في الشيفرة 4، إذ كان متغاير Quarter يحمل قيمةً من النوع UsState، ونستطيع استخدام تعبير match عندها إذا أردنا عدّ جميع الأرباع التي رأيناها بينما نُعلن عن ولاية كل من الأرباع كما يلي: let mut count = 0; match coin { Coin::Quarter(state) => println!("State quarter from {:?}!", state), _ => count += 1, } أو يمكننا استخدام تعبير if let و else بدلًا من ذلك كما يلي: let mut count = 0; if let Coin::Quarter(state) = coin { println!("State quarter from {:?}!", state); } else { count += 1; } إذًا، إذا صادفت موقفًا احتاج فيه برنامجك إلى المنطق الخاص بالبنية match، وكانت كتابة البنية طويلة ورتيبة تذكّر وجود if let في مخزونك أيضًا. ترجمة -وبتصرف- لقسم من الفصل Enums and Pattern Matching من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: التعدادات enums في لغة رست Rust الملكية Ownership في لغة رست أنواع البيانات Data Types في لغة رست Rust
  21. سننظر في هذا المقال إلى التعدادات enumerations -أو اختصارًا enums- وهي تسمح لنا بتعريف نوع من خلال تعدّد متغيراته variants المحتملة. سنعرّف أولًا المعدّدات ونستخدمها حتى نعرف إمكانياتها وقدرتها على ترميز البيانات، ثم سنستعرض التعداد المهم المسمى option، والذي يدل على قيمة إذا كان يحتوي على شيء أو لا شيء، ومن ثمّ سننظر إلى مطابقة الأنماط pattern matching باستخدام التعبير match الذي يجعل من عملية تشغيل شيفرات برمجية مختلفة بحسب قيم التعداد المختلفة عملية سهلة، وأخيرًا سنتكلم عن الباني if let وهو طريقة ومصطلح مختصر آخر مُتاح لنا للتعامل مع التعدادات في برامجنا. تعريف تعداد التعدادات هي طريقة لتعريف أنواع بيانات مُخصصة بصورةٍ مختلفة عن الهياكل structs؛ إذ تمنحنا الهياكل طريقةً لتجميع الحقول والبيانات ذات الصلة معًا، فمثلًا يُعرّف المستطيل Rectangle من خلال طوله height وعرضه width؛ بينما تعطينا التعدادات الخيار للقول أن القيمة هي واحدة من القيم الممكنة، فقد نرغب مثلًا بالقول أن المستطيل هو أحد الخيارات من مجموعة أشكال متاحة تتضمن أيضًا الدائرة Circle والمثلث Triangle. يسمح لنا رست بترميز هذه الاحتمالات على هيئة تعداد. دعنا ننظر إلى حالة قد تستخدم فيها التعدادات في شيفرتك البرمجية ولننظر إلى فائدتها والأشياء التي تميزها عن الهياكل؛ لنقل أن تطبيقنا سيتعامل مع عناوين بروتوكول الإنترنت IP address. المعياران المُستخدمان حاليًا في عناوين بروتوكول الإنترنت هما الإصدار الرابع والإصدار السادس، لذلك نستطيع استخدام التعداد لجميع المتغيرات variants في الحالتين، وهذا هو السبب في تسمية التعدادات بهذا الاسم. يمكن لأي عنوان أن يكون من الإصدار الرابع أو السادس فقط، وليس من الممكن أن يكون العنوان من الإصدارين معًا، وهذا ما يجعل استخدام التعداد مع عناوين الإنترنت استخدامًا مناسبًا، لأن قيمته تكون أحد متغيراته فقط، وكلّ من عناوين الإصدار الرابع والسادس هي عناوين إنترنت في نهاية المطاف ويجب أن تُعامل كأنها نوع واحد عندما تتعامل شيفرتنا البرمجية مع العناوين. يمكننا التعبير عن هذا المفهوم في شيفرتنا البرمجية عن طريق تعريف التعداد IpAddrKind وإضافة أنواع عناوين الإنترنت الممكنة ألا وهي V6 و V4 والتي هي متغيرات التعداد: enum IpAddrKind { V4, V6, } أصبح لدينا الآن التعداد IpAddrKind وهو نوع بيانات مخصص يمكننا استخدامه في أي مكان ضمن شيفرتنا البرمجية. قيم التعداد يُمكننا إنشاء نسخةٍ من كلا المتغيرَين في التعداد IpAddrKind على النحو التالي: let four = IpAddrKind::V4; let six = IpAddrKind::V6; لاحظ أن فضاء أسماء متغيرات التعداد موجود ضمن المعرّف identifier، ويمكننا استخدام نقطتين مزدوجتين :: لفصل كل من اسم المتغيّر والمعرف، وهذا الأمر مفيد لأن كلا القيمَتين IpAddrKind::V4 و IpAddrKind::V6 من نفس النوع وهو IpAddrKind. يمكننا بعد ذلك تعريف دالة تأخذ أي قيمة من النوع IpAddrKind: fn route(ip_kind: IpAddrKind) {} يمكننا بعدها استدعاء الدالة باستخدام أي من متغيّرات التعداد على النحو التالي: route(IpAddrKind::V4); route(IpAddrKind::V6); لاستخدام التعدادات فوائد أكثر، فبالنظر إلى نوع عنوان الإنترنت الذي أنشأناه نلاحظ أنه لا توجد أي طريقة لنا لتخزين بيانات العنوان الفعلي، بل نستطيع فقط معرفة نوع العنوان، وقد تطبق ما تعلمناه عن الهياكل سابقًا بكتابة الشيفرة 1. enum IpAddrKind { V4, V6, } struct IpAddr { kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1"), }; let loopback = IpAddr { kind: IpAddrKind::V6, address: String::from("::1"), }; [الشيفرة 1: تخزين البيانات ومتغيّر النوع IpAddrKind باستخدام الهيكل struct] عرفنا هنا هيكلًا IpAddr يحتوي على حقلين أحدهما kind من النوع IpAddrKind (وهو التعداد الذي عرفناه سابقًا) وحقل address من النوع String، ثم أنشأنا نسختين من هذا الهيكل، وأول نسخة هي home وتحمل القيمة IpAddrKind::V4 في الحقل kind والقيمة 127.0.0.1 في الحقل address، وثاني النسخ هي loopback وتحمل القيمة IpAddrKind::V6 (المتغيّر الثاني من النوع IpAddrKind) في حقل kind ويحتوي على القيمة ‎::1 في حقل العنوان address، وبالتالي استخدمنا هنا هيكلًا لتجميع القيمتين kind و address مع بعضهما وربطنا المتغيّر مع القيمة. يُعد تنفيذ المفهوم ذاته باستخدام التعدادات فقط أكثر بساطة من ذلك بكثير، إذ بدلًا من وجود التعداد داخل الهيكل يمكننا وضع البيانات مباشرةً داخل كل متغيّر من التعداد. إليك التعريف الجديد للتعداد IpAddr الذي يحتوي على V4 و V6 ويحمل كل منهما قيمةً من النوع String: enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V6(String::from("::1")); نربط البيانات إلى كل متغيّر من التعداد مباشرةً، مما يجنبنا إضافة هيكل إضافي كما يجعل من رؤية تفاصيل عمل التعداد عمليةً أكثر وضوحًا؛ إذ يصبح اسم كل متغيّر معرّف في التعداد دالةً تُنشئ نسخةً من التعداد أي أن IpAddr::V4()‎ هو استدعاء لدالة تأخذ وسيطًا من النوع String وتُعيد نسخةً من النوع IpAddr ونحصل على هذه الدالة البانية constructor function تلقائيًا عند تعريف التعداد. هناك ميزةٌ أخرى لاستخدام التعدادات عوضًا عن الهياكل، ألا وهي أن كل متغيّر يحصل على نوع مختلف وكمية مختلفة من البيانات المرتبطة به، إذ سيحصل نوع الإصدار الرابع من عنوان الإنترنت على أربع مكونات عددية تحمل قيمة تنتمي إلى المجال من 0 إلى 255. إذا أردنا تخزين عناوين الإصدار الرابع V4 بأربع قيم من النوع u8 مع المحافظة على إمكانية تمثيل عناوين الإصدار السادس V6 مثل قيمة واحدة من النوع String، وهذا لن يكون هذا ممكنًا باستخدام الهياكل. إليك كيف تسمح لنا التعدادات بفعل ذلك: enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1")); استعرضنا عدة طرق في تعريف هياكل البيانات لتخزين الإصدار الرابع والسادس من عناوين بروتوكول الإنترنت، إلا أن تخزين عناوين الإنترنت في الحقيقة ممكن التخزين والترميز بسهولة عن طريق تعريف ضمن المكتبة القياسية. دعنا ننظر إلى كيفية تعريف المكتبة القياسية للنوع IpAddr، إذ تُعرّف المكتبة القياسية التعداد مع معدّداته بصورةٍ مماثلة لما فعلناه سابقًا إلا أنها تُضمّن عناوين الإنترنت داخل المتغيّرات ضمن هيكلين مختلفين مُعرّفَين على نحوٍ مختلف: struct Ipv4Addr { // --snip-- } struct Ipv6Addr { // --snip-- } enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr), } توضح لك الشيفرة البرمجية أنه بإمكانك تخزين أي نوع من البيانات داخل متغيّر التعداد مثل السلاسل النصية والأنواع العددية أو الهياكل، كما يمكنك أيضًا تضمين تعداد آخر داخلها. أنواع المكتبة القياسية بسيطة ويمكنك فهمها بسهولة أو كتابتها من الصفر لوحدك. لاحظ أنه بالرغم من احتواء المكتبة القياسية على تعريف النوع IpAddr، إلا أنه ما زال بإمكاننا إنشاء واستخدام تعريفنا الخاص دون أي تعارض وذلك لأننا لم نُضِف تعريف المكتبة القياسية إلى نطاقنا، وسنتحدث على نحوٍ مفصل عن إضافة الأنواع إلى النطاق لاحقًا. دعنا ننظر إلى مثال آخر على التعدادات في الشيفرة 2، إذ يحتوي هذا المثال على أنواع متعددة ضمن متغيّرات التعداد. enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } [الشيفرة 2: تعداد Message يحتوي على متغيّرات، يخزّن كل منها عدد ونوع مختلف من أنواع القيم] يحتوي التعداد السابق على أربع متغيّرات من أنواع مختلفة: المتغيّر Quit، الذي لا يحتوي على أي بيانات مرتبطة به إطلاقًا. المتغيّر Move، الذي يحتوي على حقول مُسمّاة بصورةٍ مشابهة للهياكل. المتغيّر Write، الذي يحتوي على نوع String وحيد. المتغيّر ChangeColor، الذي يحتوي على ثلاث قيم من النوع i32. عملية تعريف التعداد السابق في الشيفرة 2 هي عملية مشابهة لتعريف أنواع مختلفة من الهياكل، والفارق الوحيد هنا هو أن التعدادات لا تستخدم الكلمة المفتاحية struct وكل المتغيّرات هنا مجمعة مع بعضها بعضًا ضمن النوع Message. تُخزّن الهياكل التالية البيانات التي يحملها التعداد السابق: struct QuitMessage; // unit هيكل وحدة struct MoveMessage { x: i32, y: i32, } struct WriteMessage(String); // هيكل صف struct ChangeColorMessage(i32, i32, i32); // هيكل صف إلا أننا لن نصبح قادرين على تعريف دالة تأخذ أي من أنواع الرسائل بسهولة إذا استخدمنا هياكل مختلفة لكلّ منها نوعها المختلف كما هو الحال في التعداد Message المعرف في الشيفرة 2 الذي يمثّل نوعًا واحدًا فقط. هناك تشابه آخر بين التعدادات والهياكل؛ إذ يمكننا تعريف توابع في التعدادات بطريقة مشابهة لما يحدث في الهياكل باستخدام impl. إليك تابعًا باسم call يمكننا تعريفه ضمن التعداد Message: impl Message { fn call(&self) { // يُعرّف محتوى التابع هنا } } let m = Message::Write(String::from("hello")); m.call(); تُستخدم self في متن التابع للحصول على القيمة المُعادة من استدعاء التابع، وفي هذا المثال نُنشئ متغيرًا m يحمل القيمة Message::Write(String::from("hello"))‎، وهذا ما ستكون قيمة self عليه داخل التابع call عند تنفيذ السطر m.call()‎. دعنا ننظر إلى تعداد شائع مفيد آخر ضمن المكتبة القياسية ألا وهو Option. التعداد Option وميزاته بما يخص القيم الفارغة نستعرض في هذه الفقرة التعداد Option، وهو تعداد معرّف داخل المكتبة القياسية. يُستخدم النوع Option لترميز حالة شائعة عندما تكون القيمة شيئ أو لا شيء، فعلى سبيل المثال إذا طلبت أول عناصر القائمة ستحصل على قيمة (شيء) إلا إذا كانت القائمة فارغة فستحصل على لا شيء، والتعبير عن هذا المفهوم في سياق أنواع البيانات يعني أن المصرف قادر على معرفة إذا ما كنت تعاملت مع جميع الحالات التي يجب عليك التعامل معها، وتساعد هذه الميزة في منع حدوث الأخطاء الشائعة جدًا في باقي لغات البرمجة. يُنظر إلى تصميم لغات البرمجة بسياق المزايا التي تتضمنها، إلا أن المزايا التي لن تُضمّن مهمةٌ أيضًا، إذ لا تحتوي رست على ميزة القيمة الفارغة null التي تمتلكها العديد من لغات البرمجة الأخرى؛ والقيمة الفارغة null تعني أنه لا يوجد أي قيمة هناك، وتكون المتغيرات في لغات البرمجة التي تحتوي على القيمة الفارغة في حالتين، إما حالة فارغة أو حالة غير فارغة not-null. صرّح توني هواري Tony Hoare -مخترع القيمة الفارغة- في عام 2009 في عرض تقديمي بعنوان "المراجع الفارغة: خطأ بقيمة مليار دولار Null Reference: The Billion Dollar Mistake" ما يلي: المشكلة في القيم الفارغة هي أنك ستحصل على خطأ من نوع ما إذا حاولت استخدامها مثل قيمة غير فارغة، ومن السهل ارتكاب ذلك الخطأ لأن خاصية الفراغ وعدم الفراغ مستخدمة جدًا، إلا أن المفهوم الذي تحاول قيمة الفراغ أن تشير إليه ما زال مفيدًا ألا وهو أن القيمة الفارغة غير صالحة أو مفقودة لسبب ما. المشكلة ليست بالمفهوم وإنما بالحل المُطبّق، ولهذا السبب لا تحتوي رست على النوع فارغ null، وإنما تحتوي على تعداد يدل على مفهوم عدم وجود قيمة أو عدم صلاحيتها ألا وهو التعداد Option<T>‎ المعرف في المكتبة القياسية كما يلي: enum Option<T> { None, Some(T), } التعداد Option<T>‎ مفيد جدًا حتى أنه مضمّن في مقدمة البرنامج وليس عليك أن تُضيفه إلى النطاق يدويًا، كما أن متغيّراته مضمّنة أيضًا ويمكنك استخدام Some و None مباشرةً دون البدء بالبادئة Option::‎، إلا أن التعداد Option<T>‎ هو تعداد مثل أي تعداد آخر و Some(T)‎ و None هي متغيّرات من النوع Option<T>‎. طريقة كتابة <T> هي ميزة من ميزات رست التي لم نتحدث عنها بعد، وهي معامل نوع مُعمّم generic type parameter وسنتكلم عن الأنواع المعممة لاحقًا، وكل ما عليك معرفته الآن هو أن <T> يعني أن متغيّر Some من التعداد Option يستطيع تخزين جزء من أي نوع من البيانات وأي نوع يُستخدم في مكان T يجعل من النوع Option<T>‎ نوعًا مختلفًا. إليك بعض الأمثلة التي تستخدم قيم Option لتخزين عدة أنواع من البيانات: let some_number = Some(5); let some_char = Some('e'); let absent_number: Option<i32> = None; نوع some_number هو Option‎، ونوع some_string هو Option<char>‎ وهو نوع مختلف تمامًا عن سابقه، وتستطيع رست تحديد هذه الأنواع بسبب استخدامنا للقيمة داخل متغيّر Some، إلا أن رست تتطلب منا تحديد نوع Option الكلي بالنسبة للمتغير absent_number ولا يستطيع المصرّف تحديد النوع الخاص بالمتغير Some بنفسه عن طريق النظر إلى قيمة None فقط، وعلينا إخبار رست هنا أننا نقصد أن absent_number هو من النوع Option<i32>‎. نعلم أن هناك قيمة موجودة عندما يكون لدينا قيمة في Some، أما عندما يكون لدينا قيمة في None نعلم أن هذا الأمر مكافئ للقيمة الفارغة null أي أنه لا يوجد لدينا قيمة صالحة. إذًا، لمَ وجود Option<T>‎ هو أفضل من وجود القيمة الفارغة؟ لأن Option<T>‎ و T (إذ يمكن أن تدل T على أي نوع) هي من أنواع مختلفة ولن يسمح لنا المصرف باستخدام القيمة Option<T>‎ على أنها قيمة صالحة. على سبيل المثال، لن تُصرَّف الشيفرة البرمجية التالية لأننا نحاول إضافة النوع i8 إلى نوع Option<i8>‎: let x: i8 = 5; let y: Option<i8> = Some(5); let sum = x + y; إذا نفذنا الشيفرة البرمجية السابقة، نحصل على رسالة الخطأ التالية: $ cargo run Compiling enums v0.1.0 (file:///projects/enums) error[E0277]: cannot add `Option<i8>` to `i8` --> src/main.rs:5:17 | 5 | let sum = x + y; | ^ no implementation for `i8 + Option<i8>` | = help: the trait `Add<Option<i8>>` is not implemented for `i8` = help: the following other types implement trait `Add<Rhs>`: <&'a f32 as Add<f32>> <&'a f64 as Add<f64>> <&'a i128 as Add<i128>> <&'a i16 as Add<i16>> <&'a i32 as Add<i32>> <&'a i64 as Add<i64>> <&'a i8 as Add<i8>> <&'a isize as Add<isize>> and 48 others For more information about this error, try `rustc --explain E0277`. error: could not compile `enums` due to previous error تعني رسالة الخطأ السابقة أن رست لا تعرف كيفية إضافة i8 إلى Option<i8>‎ لأنهما من نوعين مختلفين. يتأكد المصرف عندما نحصل على قيمة من نوع مشابه إلى النوع i8 في رست من أنه لدينا قيمة صالحة، ويمكننا تجاوز عملية التحقق بأمان دون الحاجة للتحقق من قيمة فارغة قبل استخدام هذه القيمة، إلا أنه يجب علينا أخذ الحيطة فقط في حال كان لدينا Option<i8>‎ (أو أي نوع من قيمة نتعامل معها) وذلك إذا كان النوع لا يحمل أي قيمة وسيتأكد المصرف حينها من تعاملنا مع هذه الحالة قبل استخدام القيمة. بكلمات أخرى، يجب عليك تحويل النوع Option<T>‎ إلى T قبل تنفيذ عمليات النوع T على القيمة، ويساعدنا هذا عمومًا على تشخيص أحد أكثر المشاكل شيوعًا في القيم الفارغة ألا وهي افتراض أن قيمة ما ليست فارغة وهي في الحقيقة فارغة. سيجعلك التخلص من المشكلة السابقة أكثر ثقة بشيفرتك البرمجية، إذ يتوجب عليك تحويل القيمة إلى النوع Option<T>‎ يدويًا إذا أردت الحصول على القيمة التي من الممكن أن تكون فارغة، وعليك التعامل مع حالة كون القيمة فارغة إذا استخدمت هذه الطريقة، وتستطيع الافتراض بأمان أن أي قيمة ليست من النوع Option<T>‎ ليست بقيمة فارغة، وهذا تصميم مقصود في لغة رست للحدّ من انتشار القيمة الفارغة وزيادة أمان شيفرة رست البرمجية. كيف تُخرج القيمة T خارج متغيّر Some عندما يكون لديك قيمة من النوع Option<T>‎ وتريد استخدام القيمة؟ لدى التعداد Option<T>‎ العديد من التوابع المفيدة في حالات متعددة ويمكنك النظر إليها من التوثيق، وستكون معرفة هذه التوابع الخاصة بالنوع Option<T>‎ مفيدةً جدًا في رحلتك مع رست. يجب عليك كتابة شيفرة برمجية تتعامل مع كل متغيّر إذا أردت استخدام القيمة الموجودة في Option<T>‎، إذ يجب على شيفرتك البرمجية أن تُنفّذ فقط في حال كان داخل Option<T>‎ قيمةً ما، والسماح لهذه الشيفرة البرمجية باستخدام القيمة T الداخلية، كما ينبغي وجود شيفرة برمجية أخرى تُنفّذ في حال كان هناك قيمة None بحيث لا تستخدم هذه الشيفرة البرمجية القيمة T. يسمح لنا تعبير match الذي يمثل باني للتحكم بسير البرنامج باستخدام التعدادات بتنفيذ هذا الأمر تمامًا، إذ سينفذ شيفرةً برمجيةً مختلفةً بحسب المتغيّر الموجود داخل التعداد، ويمكن للشيفرة البرمجية حينها استخدام هذه البيانات داخل القيمة الموافقة. ترجمة -وبتصرف- لقسم من الفصل Enums and Pattern Matching من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: استخدام التوابع methods ضمن الهياكل structs في لغة رست Rust المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست التحكم بسير تنفيذ برامج راست Rust أنواع البيانات Data Types في لغة رست Rust
  22. التوابع methods مشابهة للدوال functions، إذ نُصرّح عنها باستخدام الكلمة المفتاحية fn متبوعةً باسم التابع، ويمكن للتوابع أن تمتلك عدّة معاملات وأن تُعيد قيمةً ما، ويحتوي التابع بداخله على جزء من شيفرة برمجية تعمل عند استدعاء التابع في مكان آخر، إلا أن التوابع -على عكس الدوال- تُعرّف داخل الهيكل (أو داخل المعدّد enum، أو كائن سمة trait object وهو ما سنتكلم عنه لاحقًا)، ويكون المعامل الأول دائمًا هو self الذي يمثّل نسخةً من الهيكل التي يُستدعى التابع منها. تعريف التوابع دعنا نُعدّل الدالة area -في المقال السابق- التي تأخذ نسخةً من الهيكل Rectangle معاملًا لها، ونُنشئ بدلًا من ذلك تابع area معرّف داخل الهيكل Rectangle كما هو موضح في الشيفرة 13. اسم الملف: src/main.rs #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", rect1.area() ); } [الشيفرة 13: تعريف التابع area داخل الهيكل Rectangle] نبدأ بكتابة كتلة تطبيق implementation‏ impl داخل الهيكل Rectangle حتى نستطيع تعريف الدالة داخل سياق الهيكل، وسيكون كل شيء ضمن هذه الكتلة مرتبطًا بالنوع Rectangle، من ثمّ ننقل الدالة area إلى داخل أقواس الكتلة impl ونعدّل المعامل الأول -وفي هذه الحالة هو المعامل الوحيد- ليصبح self في بصمة الدالة وأي مكان آخر ضمنها. ننتقل إلى الدالة main وعوضًا عن استدعاء الدالة area وتمرير rect1 مثل وسيط، سنستخدم طريقة كتابة التابع لاستدعاء التابع area على نسخة الهيكل Rectangle، إذ تتلخّص الطريقة بإضافة نقطة (.) بعد نسخة الهيكل متبوعةً باسم التابع ومن ثم القوسين وبداخلهما أي وسطاء. نستخدم ‎&self في بصمة الدالة area عوضًا عن rectangle: &Rectangle، وفي الحقيقة ‎&self هو اختصار إلى self: &Self، ويمثّل Self داخل الكتلة impl اسمًا مستعارًا للنوع الذي يحتوي داخله الكتلة impl،وهي في هذه الحالة Rectangle. يجب على التوابع أن تحتوي على وسيط باسم self من النوع Self مثل مُعامل أوّل، إذ تسمح لك رست باختصار الاسم إلى self في المعامل الأول للتابع، لاحظ أنّنا ما زلنا بحاجة الرمز & أمام الاسم المختصر self وذلك للإشارة إلى أن التابع يستعير نسخةً من النوع Self كما فعلنا سابقًا باستخدام rectangle: &Rectangle. يمكن للتوابع أن تمتلك الاسم self أو أن تستعير self على نحوٍ غير قابل للتعديل -كما فعلنا هنا- أو أن تستعير self مع إمكانية تعديل كما هو الحال في أي مُعامل آخر. اخترنا ‎&self هنا مثل معامل للسبب ذاته الذي دفعنا لاستخدام ‎&Rectangle في إصدار البرنامج السابق، وهو أننا لا نريد أخذ الملكيّة بل نريد الاكتفاء فقط بقراءة البيانات الموجودة في الهيكل دون التعديل عليها. إذا أردنا تغيير النسخة التي استدعينا التابع عليها مثل جزء من وظيفة التابع، فيجب علينا استخدام ‎&mut self مثل معامل أوّل بدلًا من ذلك. من النادر أن تجد تابعًا يأخذ ملكية نسخة ما باستخدام self فقط في المعامل الأول وتُستخدم هذه الطريقة عادةً عندما يحوِّل التابع الوسيط self إلى شيء آخر وتريد أن تمنع مستدعي التابع من استخدام النسخة الأصل بعد عملية التحويل. السبب الرئيسي في استخدامنا التوابع بدلًا من الدوال هو التنظيم، إضافةً إلى أننا لا نُكرّر نوع الوسيط self في كل بصمةٍ للتابع، إذ سمحت لنا التوابع بوضع جميع الأشياء التي يمكننا فعلها ضمن نسخة النوع داخل كتلة impl واحدة عوضًا عن إجبار قارئ الشيفرة البرمجية بالبحث عن الدوال التي تشير لإمكانيات الهيكل Rectangle بنفسه في أماكن مختلفة من المكتبة التي كتبناها. لاحظ أنه يمكننا اختيار اسم التابع على نحوٍ مماثل لاسم حقول الهيكل، على سبيل المثال يمكننا تعريف تابع داخل الهيكل Rectangle باستخدام اسم الحقل width: اسم الملف: src/main.rs impl Rectangle { fn width(&self) -> bool { self.width > 0 } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; if rect1.width() { println!("The rectangle has a nonzero width; it is {}", rect1.width); } } نختار هنا أن نجعل التابع width يُعيد القيمة true إذا كانت القيمة width في نسخة الهيكل أكبر من 0، وإلا فالقيمة false إذا كانت القيمة مساوية إلى الصفر، ويمكننا الاستفادة من الحقل داخل التابع الذي يحمل الاسم ذاته لأي هدف كان، ثم ننتقل إلى الدالة main ونستخدم التابع بالشكل rect1.width مع الأقواس حتى تعلم رست أننا نقصد التابع width، إذ ستعلم رست أننا نقصد الحقل width، إذا لم نستخدم الأقواس. قد نحتاج في بعض الأحيان عندما نُعطي التابع الاسم ذاته لحقل ما أن يُعيد التابع هذا القيمة الموجودة في الحقل ولا شيء آخر، وتُدعى التوابع من هذا النوع بالتوابع الجالبة getters ولا تُطبّقها رست تلقائيًا كما هو الحال في معظم اللغات الأخرى. تُعد التوابع الجالبة مفيدة لأنها تُمكّنك من جعل الحقل خاصًّا private وجعل التابع عامًا public في ذات الوقت مما يمكنك من الوصول إلى الحقل وقراءته فقط بمثابة جزء من الواجهة البرمجية API العامة للنوع، وسنناقش معنى خاص وعام وكيفية جعل الحقل أو التابع خاصًا أو عامًا لاحقًا. أين العامل '<-' ؟ لدى لغة سي C و C++‎ معاملان مختلفان لاستدعاء التوابع، إذ يُمكنك استخدام . إذا أردت استدعاء التابع على الكائن مباشرةً، أو استخدام المعامل <- إذا أردت استدعاء التابع على مؤشر يُشير إلى الكائن وتريد أن تحصل dereference على المؤشر عن الكائن أولًا؛ أي بكلمات أخرى، إذا كان object مؤشرًا فكتابة object->something()‎ مشابهة إلى ‎(‎*object).something()‎. لا يوجد في رست مُكافئ للمعامل <-، بل لدى رست ميزة تُدعى بالمرجع لعنوان الذاكرة والتحصيل التلقائي automatic referencing and dereferencing بدلًا من ذلك، واستدعاء التوابع هو واحدة من الأجزاء في لغة رست التي تتبع هذا السلوك. إليك كيفية عمل هذه الميزة: عند استدعاء التابع باستخدام object.something()‎ تُضيف رست & أو ‎&mut أو * تلقائيًا حتى يُطابق object بصمة التابع، أي بكلمات أخرى، السطرين البرمجيين متماثلين، إلا أن السطر البرمجي الأول يبدو أكثر ترتيبًا: p1.distance(&p2); (&p1).distance(&p2); يعمل سلوك المرجع لعنوان الذاكرة التلقائي لأن للتوابع مستقبل receiver واضح ألا وهو النوع self، وتستطيع رست باستخدام المستقبل الواضح واسم التابع أن تعرف دون شك إذا ما كان التابع يقرأ (‎&self) أو يعدّل (‎&mut self) أو يستهلك (self)، وتُعدّ حقيقة أن رست تجعل من الاستعارة مباشرة لمستقبل التابع من أهم أجزاء ميزة الملكية في رست. التوابع التي تحتوي على عدة معاملات دعنا نتدرب على استخدام التوابع بتطبيق تابع ثاني ضمن الهيكل Rectangle، ونريد في هذه المرة أن تأخذ نسخةٌ من الهيكل Rectangle نسخةً أخرى من الهيكل ذاته وأن تُعيد true إذا كانت النسخة الثانية تتسع كاملةً داخل النسخة الأولى (self) وإلا فيجب أن تُعيد false، وسنستطيع كتابة الشيفرة 14 بعد تعريف التابع can_hold. اسم الملف: src/main.rs fn main() { let rect1 = Rectangle { width: 30, height: 50, }; let rect2 = Rectangle { width: 10, height: 40, }; let rect3 = Rectangle { width: 60, height: 45, }; println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); } [الشيفرة 14: استخدام التابع can_hold الذي لم نكتبه بعد] سيبدو خرج الشيفرة البرمجية السابقة كما يلي، وذلك لأن كلا أبعاد النسخة rect2 أصغر من أبعاد النسخة rect1 إلا أن أبعاد النسخة rect3 أكبر من النسخة rect1: Can rect1 hold rect2? true Can rect1 hold rect3? false نعلم أننا نريد تعريف تابع، ولذلك سنكتب ذلك ضمن الكتلة impl Rectangle، وسيكون اسم التابع can_hold وسيستعير نسخةً من Rectangle غير قابلة للتعديل مثل معامل، ويمكننا معرفة نوع المعامل بالنظر إلى السطر البرمجي الذي سيستدعي التابع، إذ يمرر الاستدعاء rect1.can_hold(&rect2)‎ الوسيط ‎&rect2 وهو نُسخة من الهيكل Rectangle مُستعارة غير قابلة للتعديل، وهذا الأمر منطقي لأننا نريد فقط أن نقرأ بيانات النسخة rect2 ولا حاجة لنا في التعديل عليها مما سيتطلب نسخةً مُستعارةً قابلة للتعديل، إذ نريد هنا أن تحتفظ الدالة main بملكية rect2 حتى نستطيع استخدامها مجددًا بعد استدعاء التابع can_hold. ستكون القيمة المُعادة من التابع can_hold بوليانية boolean وسيتحقق تطبيقنا فيما إذا كان الطول والعرض الخاص بالمعامل self أكبر من الطول والعرض الخاص بنسخة Rectangle الأخرى. دعنا نُضيف التابع can_hold الجديد إلى كتلة impl الموجودة في الشيفرة 13 كما هو موضح في الشيفرة 15. اسم الملف: src/main.rs impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } [الشيفرة 15: تطبيق التابع can_hold ضمن الهيكل Rectangle الذي يأخذ نسخةً أخرى من الهيكل Rectangle بمثابة معامل] سنحصل على الخرج المطلوب عند تشغيل الشيفرة البرمجية ضمن main في الشيفرة 14. يمكن أن تأخذ التوابع عدة معاملات إن أردنا وذلك بإضافتها إلى بصمة التابع بعد المعامل self، وتعمل هذه المعاملات كما تعمل في الدوال عادةً. الدوال المترابطة تُدعى جميع الدوال المُعرّفة داخل الكتلة impl بالدوال المترابطة associated functions، وذلك لأنها مرتبطة بالنوع الموجود بعد impl، ويمكننا تعريف الدوال المترابطة التي لا تحتوي على المعامل الأول self (وبالتالي فهي لا تُعدّ توابعًا)، لأننا لا نحتاج إلى نسخة من النوع عند تنفيذها، وقد استخدمنا دالةً مشابهةً لهذه سابقًا، ألا وهي الدالة String::from والمعرّفة داخل النوع String. تُستخدم الدوال المترابطة التي لا تُعدّ بمثابة توابع في الباني constructor الذي يعيد نسخةً جديدةً من الهيكل، وتُسمى عادةً الدوال المترابطة new لكن هذا الاسم غير مخصص في هذه اللغة، إذ يمكننا مثلًا كتابة دالة مترابطة تحتوي على معامل من بُعدٍ واحد وأن نستخدم هذا المعامل للطول والعرض وبالتالي يُسهّل هذا الأمر إنشاء مربّع باستخدام الهيكل Rectangle بدلًا من تحديد القيمة ذاتها مرتين: اسم الملف: src/main.rs impl Rectangle { fn square(size: u32) -> Self { Self { width: size, height: size, } } } تُعد كلمات Self المفتاحية في النوع المُعاد ومتن الدالة بمثابة أسماء مستعارة للنوع الذي يظهر بعد الكلمة المفتاحية impl، والتي هي في حالتنا Rectangle. نستخدم :: مع اسم الهيكل لاستدعاء الدالة المترابطة، والسطر let sq = Rectangle::square(3)‎ هو مثال على ذلك، ويقع فضاء أسماء namespace هذه الدالة داخل الهيكل، ويُستخدم الرمز :: لكلٍّ من الدوال المترابطة وفضاءات الأسماء المُنشأة من قبل الوحدات modules التي سنناقشها لاحقًا. كتل impl متعددة يمكن أن يحتوي كل هيكل على عدّة كُتَل impl، فعلى سبيل المثال الشيفرة 15 مكافئة للشيفرة 16 التالية التي تحتوي على كل تابع داخل كتلة impl مختلفة. impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } [الشيفرة 16: إعادة كتابة الشيفرة 15 باستخدام كتَل impl متعددة] لا يوجد أي سبب لتفرقة التوابع ضمن كتل impl متعددة هنا، إلا أنها كتابة صحيحة وسنستعرض حالة قد تكون فيها هذه الكتابة مفيدة لاحقًا عندما نُناقش الأنواع المُعمّمة generic types والسمات. ‎ترجمة -وبتصرف- لقسم من الفصل Using Structs to Structure Related Data من كتاب The Rust Programming Language اقرأ أيضًا المقال السابق: استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust التحكم بسير تنفيذ برامج راست Rust أنواع البيانات Data Types في لغة رست Rust المتغيرات والتعديل عليها في لغة رست
  23. بعد أن تكلمنا عن المكتبات القياسية المعرفة حسب المعيار، لم يبقَ إلا توضيح هيئة البرامج الكاملة المكتوبة بهذه اللغة، وسنتطرق في هذا المقال إلى بعض الأمثلة التي توضح كيفية جمع هذه العناصر لبناء البرامج. إلا أن هناك بعض النقاط التي يجب مناقشتها في لغة سي قبل عرض هذه الأمثلة. وسطاء الدالة main تقدم وسطاء الدالة main فرصةً مفيدةً لكل من يكتب البرامج ويشغلها في بيئة مُستضافة hosted environment وذلك بإعطاء المعاملات للبرنامج، ويُستخدم ذلك عادةً لتوجيه البرنامج لكيفية تنفيذ مهامه، ومن الشائع أن تكون هذه المعاملات هي أسماء ملفات تُمرّر مثل وسيط. يبدو تصريح الدالة main على النحو التالي: int main(int argc, char *argv[]); يُشير التصريح إلى أن الدالة main تُعيد عددًا صحيحًا، وتُمرّر هذه القيمة عادةً، أو حالة الخروج exit status في البيئات المُستضافة، مثل أنظمة دوس DOS أو يونيكس UNIX إلى مفسّر سطر الأوامر command line interpreter، فعلى سبيل المثال تُستخدم حالة الخروج في نظام يونيكس للدلالة على إتمام البرنامج لمهمته بنجاح (تمثلّه القيمة صفر)، أو حدوث خطأ أثناء التنفيذ (تمثًله قيمة غير صفرية). اتّبع المعيار هذا الاصطلاح أيضًا، إذ تُستخدم exit(0)‎ لإعادة حالة النجاح إلى البيئة المُستضافة وأي قيمة أخرى تدلّ على حدوث خطأ ما، وستُترجم exit القيمة لمعناها إذا كانت البيئة المستضافة تستخدم اصطلاحًا مخالفًا. بما أن الترجمة معرفة حسب التنفيذ، فمن الأفضل استخدام القيمتان المُعرّفتان في ملف الترويسة <stdlib.h>، وهما: EXIT_SUCCESS و EXIT_FAILURE. هناك على الأقل وسيطان للدالة main، وهُما: argc و argv، إذ يدل أولهما على عدد الوسطاء المزودة للبرنامج، بينما يدل الثاني على مصفوفة من المؤشرات تشير إلى سلاسل نصية تمثّل الوسطاء، وهي من النوع "مصفوفة من المؤشرات تشير إلى محرف char"، وتُمرّر هذه الوسطاء إلى البرنامج باستخدام مفسّر سطر الأوامر الخاص بالبيئة المُستضافة، أو لغة التحكم بالوظائف job control language. يُعدّ تصريح الوسيط argv أوّل مصادفة للمبرمجين المبتدئين مع المؤشرات التي تشير إلى مصفوفات من المؤشرات، وقد يكون هذا محيّرًا بعض الشيء في البداية، إلا أن الأمر بسيط الفهم. بما أن argv تُستخدم للإشارة إلى مصفوفة السلاسل النصية، يكون تصريحها على النحو التالي: char *argv[] تذكر أيضًا أن اسم المصفوفة يُحوّل إلى عنوان أول عنصر ضمنها عندما تُمرّر إلى دالة، وهذا يعني أنه يمكننا التصريح عن argv كما يلي: char **argv والتصريحان يؤديان الغرض ذاته في هذه الحالة. سترى تصريح الدالة main مكتوبًا بهذه الطريقة معظم الأحيان، والتصريح التالي مكافئ للتصريح السابق: int main(int argc, char **argv); تُهيًّأ وسطاء الدالة main عند بداية تشغيل البرنامج على نحوٍ موافق للشروط التالية: الوسيط argc أكبر من الصفر. يمثّل argv[argc]‎ مؤشرًا فارغًا null. تمثّل العناصر بدءًا من argv[0]‎ وصولًا إلى argv[argc-1]‎ مؤشرات تشير إلى سلاسل نصية يُحدِّد البرنامج معناها. يحتوي العنصر argv[0]‎ السلسلة النصية التي تحتوي اسم البرنامج أو سلسلة نصية فارغة إذا لم تكن هذه المعلومة متاحة، وتمثل العناصر المتبقية من argv الوسطاء المزودة للبرنامج. يُزوّد محتوى السلاسل النصية إلى البرنامج بحالة الأحرف الصغيرة lower-case في حال توفر الدعم فقط للأحرف الوحيدة single. لتوضيح هذه النقطة، إليك مثالًا عن برنامج يكتب وسطاء الدالة main إلى خرج البرنامج القياسي: #include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { while(argc--) printf("%s\n", *argv++); exit(EXIT_SUCCESS); } [مثال 1] إذا كان اسم البرنامج show_args وكانت وسطاءه abcde و text و hello عند تشغيله، ستكون حالة الوسطاء وقيمة argv موضّحة في الشكل التالي: [شكل 1 وسطاء البرنامج] تنتقل argv إلى العنصر التالي عند كل زيادة لها، وبالتالي وبعد أول تكرار للحلقة ستُشير argv إلى المؤشر الذي بدوره يشير إلى الوسيط abcde، وهذا الأمر موضح بالشكل التالي: [شكل 2 وسطاء البرنامج بعد زيادة argv] سيعمل البرنامج على النظام الذي جرّبنا فيه البرنامج السابق عن طريق كتابة اسمه، ثم كتابة وسطاءه وفصلهم بمسافات فارغة. إليك ما الذي يحدث (الرمز $ هو رمز الطرفية): $ show_args abcde text hello show_args abcde text hello $ تفسير وسطاء البرنامج الحلقة المُستخدمة لفحص وسطاء البرنامج في المثال السابق شائعة الاستخدام في برامج سي C وستجدها في العديد من البرامج الأخرى، ويُعد استخدام "الخيارات options" للتحكم بسلوك البرنامج طريقةً شائعة أيضًا (تُدعى أيضًا في بعض الأحيان المُبدّلات switches أو الرايات flags)، إذ يدل الوسيط الذي يبدأ بالمحرف - على أنه وسيط يقدّم حرفًا وحيدًا أو أكثر يشير إلى خيار، ويمكن تشغيل الخيارات سويًا أو على نحوٍ منفرد: progname -abxu file1 file2 progname -a -b -x -u file1 file2 يحدّد كلًا من الخيارات جانبًا معينًا من مزايا البرنامج، وقد يُسمح لكل خيار بأخذ وسيط خاص به امتدادًا لهذه الفكرة، فعلى سبيل المثال إذا كان الخيار ‎-x يأخذ وسيطًا خاصًا به، سيبدو ذلك على النحو التالي: progname -x arg file1 وبذلك، فإن arg مرتبطة مع الخيار. تسمح لنا دالة options في الأسفل بأتمتة معالجة أسلوب الاستخدام هذا عن طريق الدعم الإضافي (شائع الاستخدام إلا أنه قد عفا عليه الزمن) لإمكانية تقديم خيار الوسيط مباشرةً بعد حرف الخيار كما يلي: progname -xarg file1 تُعيد برامج الخيارات السابقة في كلٍّ من الحالتين المحرف 'x' وتضبط المؤشر العام global المسمى OptArg ليشير إلى القيمة arg. يجب أن يقدم البرنامج لائحةً من أحرف الخيارات الصالحة بهيئة سلسلة نصية حتى نستطيع استخدام برامج الخيارات، عندما يُلحق حرفٌ ضمن هذه السلسلة النصية بالنقطتين ':'، فهذا يعني أن ما يتبع حرف الخيار هو وسيط. يُستدعى برنامج options مرارًا عند تشغيل البرنامج حتى انتهاء أحرف الخيار. يبدو أن الدوال التي تقرأ السلاسل النصية وتبحث عن تشكيلات مختلفة أو أنماط ضمن السلسلة صعبة القراءة، وإن كان في ذلك عزاءً لكن ليست عملية القراءة بتلك البساطة فعليًا، والشيفرة البرمجية التي تطبّق الخيارات هي واحدةٌ من هذه الدوال، إلا أنها ليست الأصعب ضمن هذا التصنيف. تفحص الدالة options()‎ أحرف الخيار ووسطاء الخيار من قائمة argv، وتُعيد استدعاءات متتابعة للدالة أحرف خيار متتابعة متوافقة مع واحدة من بنود القائمة legal. قد تتطلب أحرف الخيار وسطاء خيار ويُشار إلى ذلك بالنقطتين ':' اللتين تتبعان الحرف في القائمة legal. على سبيل المثال، تشير لائحة legal التي تحتوي على "ab:c" على أن a و b و c جميعها خيارات صالحة وأن b تأخذ وسيط خيار، ويُمرّر وسيط الخيار فيما بعد إلى الدالة التي استُدعيت سابقًا في قيمة المؤشر العام المُسمّى OptArg. يُعطي OptIndex السلسلة النصية التالية في مصفوفة argv[]‎ التي لم تُعالج بعد من قبل الدالة options()‎. تُعيد الدالة options()‎ القيمة ‎-1 إذا لم يكُن هناك أي أحرف خيار أخرى، أو إذا عُثر على SwitchChar مضاعف، ويُجبر ذلك الدالة options()‎ على إنهاء عملية معالجة الخيارات؛ بينما تُعيد ? إذا كان هناك خيار لا ينتمي إلى مجموعة legal، أو إذا عُثر على خيار ما يحتاج لوسيط دون وجود وسيط يتبعه. #include <stdio.h> #include <string.h> static const char SwitchChar = '-'; static const char Unknown = '?'; int OptIndex = 1; // يجب أن يكون أول خيار هو‫ argv‫[‫1] char *OptArg = NULL; // مؤشر عام لوسيط الخيار int options(int argc, char *argv[], const char *legal) { static char *posn = ""; // ‫الموضع في argv‫[‫OptIndex]‫ char *legal_index = NULL; int letter = 0; if(!*posn){ // ‫لا يوجد المزيد من args أو SwitchChar أو حرف خيار if((OptIndex >= argc) || (*(posn = argv[OptIndex]) != SwitchChar) || !*++posn) return -1; // ‫إيجاد SwitchChar مضاعف if(*posn == SwitchChar){ OptIndex++; return -1; } } letter = *posn++; if(!(legal_index = strchr(legal, letter))){ if(!*posn) OptIndex++; return Unknown; } if(*++legal_index != ':'){ /*لا يوجد وسيط للخيار */ OptArg = NULL; if(!*posn) OptIndex++; } else { if(*posn) // ‫لا يوجد مسافة فارغة بين opt و opt arg OptArg = posn; else if(argc <= ++OptIndex){ posn = ""; return Unknown; } else OptArg = argv[OptIndex]; posn = ""; OptIndex++; } return letter; } [مثال 2] برنامج لإيجاد الأنماط نقدّم في هذا القسم برنامجًا كاملًا يستخدم أحرف الخيار مثل وسطاء للبرنامج بهدف التحكم بطريقة عمله. يُعالج البرنامج أولًا أي وسيط يمثل خيارًا، ويُحفظ الوسيط الأول -ليس خيار- بكونه "سلسلة بحث نصية search string"، في حين تُستخدم أي وسطاء أخرى متبقية لتحديد أسماء الملفات التي يجب أن تُقرأ دخلًا للبرنامج، وإذا لم يُعثر على أي اسم ملف فسيقرأ البرنامج من دخله القياسي بدلًا من ذلك، وإذا وُجد تطابق لسلسلة البحث النصية ضمن سطر من نص الدخل، يُطبع كامل السطر على الخرج القياسي. تُستخدم الدالة options لمعالجة جميع أحرف الخيار المزودة للبرنامج، ويميّز برنامجنا هنا خمسة خيارات، هي: ‎-c و ‎-i و ‎-l و ‎-n و ‎-v، ولا يُشترط لأي من الخيارات السابقة أن تُتبع بوسيط اختياري. يحدد الخيار -أو الخيارات- سلوك البرنامج عند تشغيله على النحو التالي: الخيار ‎-c: يطبع البرنامج عدد الأسطر الكلية الموافقة لسلسلة البحث النصية التي عُثر عليها في ملف -أو ملفات- الدخل، ولا تُطبع أي أسطر نصية. الخيار ‎-i: تُتجاهل حالة الأحرف لكل من سطر ملف الدخل وسلسلة البحث النصية عند البحث عن تطابق بينها. الخيار ‎-l: يُطبع كل سطر نصي على الخرج مسبوقًا برقم السطر المفحوص في ملف الدخل الحالي. الخيار ‎-n: يُطبع كل سطر نصي على الخرج مسبوقًا باسم الملف الذي يحتوي هذا السطر. الخيار ‎-v: يطبع البرنامج الأسطر فقط دون مطابقة سلسلة البحث النصية المزودة. يُعيد البرنامج بعد الانتهاء من تنفيذه حالةً تدل على واحدة من الحالات التالية: الحالة EXIT_SUCCESS: عُثر على تطابق واحد على الأقل. الحالة EXIT_FAILURE: لم يُعثر على أي تطابق، أو حدث خطأ ما. يعتمد البرنامج جدًا على دوال المكتبة القياسية لإنجاز الجزء الأكبر من العمل، فعلى سبيل المثال تُعالج جميع الملفات باستخدام دوال stdio. لاحظ اعتماد جوهر البرنامج أيضًا على مطابقة السلاسل النصية باستخدام استدعاءات لدالة strstr. إليك الشيفرة البرمجية الكاملة الخاصة بالبرنامج. حتى يعمل البرنامج، عليك طبعًا تصريفه مع الشيفرة البرمجية الخاصة به التي تعالج أحرف الخيارات، والتي استعرضناها سابقًا. /* برنامج بسيط يطبع الأسطر من ملف نصي بحيث يحوي ذلك السطر الكلمة المزودة في سطر الأوامر */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> /* * تصاريح لبرنامج الأنماط * */ #define CFLAG 0x001 // احصِ عدد الأسطر المتطابقة فقط #define IFLAG 0x002 // تجاهل حالة الأحرف #define LFLAG 0x004 // اعرض رقم السطر #define NFLAG 0x008 // اعرض اسماء ملفات الدخل #define VFLAG 0x010 // اعرض السطور التي لاتتطابق extern int OptIndex; // الدليل الحالي للمصفوفة‫ ‫argv‫[‎] extern char *OptArg; /* مؤشر وسيط الخيار العام /* * ‫جلب وسطاء سطر الأوامر إلى الدالة main‫() */ int options(int, char **, const char *); /* تسجيل الخيارات المطلوبة للتحكم بسلوك البرنامج */ unsigned set_flags(int, char **, const char *); /* تفقد كل سطر من الدخل لحالة المطابقة */ int look_in(const char *, const char *, unsigned); /* اطبع سطرًا من ملف الدخل إلى الخرج القياسي بالتنسيق المُحدد بواسطة خيارات سطر الأوامر */ void print_line(unsigned mask, const char *fname, int lnno, const char *text); static const char /* الخيارات الممكنة للنمط */ *OptString = "cilnv", /*الرسالة التي ستُعرض عندما تُدخل الخيارات بصورةٍ غير صحيحة */ *errmssg = "usage: pattern [-cilnv] word [filename]\n"; int main(int argc, char *argv[]) { unsigned flags = 0; int success = 0; char *search_string; if(argc < 2){ fprintf(stderr, errmssg); exit(EXIT_FAILURE); } flags = set_flags(argc, argv, OptString); if(argv[OptIndex]) search_string = argv[OptIndex++]; else { fprintf(stderr, errmssg); exit(EXIT_FAILURE); } if(flags & IFLAG){ /*تجاهل حالة الحرف والتعامل فقط مع الأحرف الصغيرة */ char *p; for(p = search_string ; *p ; p++) if(isupper(*p)) *p = tolower(*p); } if(argv[OptIndex] == NULL){ // لم يُزوّد أي اسم ملف، لذا نستخدم‫ stdin success = look_in(NULL, search_string, flags); } else while(argv[OptIndex] != NULL) success += look_in(argv[OptIndex++], search_string, flags); if(flags & CFLAG) printf("%d\n", success); exit(success ? EXIT_SUCCESS : EXIT_FAILURE); } unsigned set_flags(int argc, char **argv, const char *opts) { unsigned flags = 0; int ch = 0; while((ch = options(argc, argv, opts)) != -1){ switch(ch){ case 'c': flags |= CFLAG; break; case 'i': flags |= IFLAG; break; case 'l': flags |= LFLAG; break; case 'n': flags |= NFLAG; break; case 'v': flags |= VFLAG; break; case '?': fprintf(stderr, errmssg); exit(EXIT_FAILURE); } } return flags; } int look_in(const char *infile, const char *pat, unsigned flgs) { FILE *in; /* ‫يخزن [0]line سطر الدخل كما يُقرأ ‫بينما يحول line[1]‎ السطر إلى حالة أحرف صغيرة إن لزم الأمر */ char line[2][BUFSIZ]; int lineno = 0; int matches = 0; if(infile){ if((in = fopen(infile, "r")) == NULL){ perror("pattern"); return 0; } } else in = stdin; while(fgets(line[0], BUFSIZ, in)){ char *line_to_use = line[0]; lineno++; if(flgs & IFLAG){ /* حالة تجاهل */ char *p; strcpy(line[1], line[0]); for(p = line[1] ; *p ; *p++) if(isupper(*p)) *p = tolower(*p); line_to_use = line[1]; } if(strstr(line_to_use, pat)){ matches++; if(!(flgs & VFLAG)) print_line(flgs, infile, lineno, line[0]); } else if(flgs & VFLAG) print_line(flgs, infile, lineno, line[0]); } fclose(in); return matches; } void print_line(unsigned mask, const char *fname, int lnno, const char *text) { if(mask & CFLAG) return; if(mask & NFLAG) printf("%s:", *fname ? fname : "stdin"); if(mask & LFLAG) printf(" %d :", lnno); printf("%s", text); } [مثال 3] مثال أكثر طموحا أخيرًا نقدّم هنا مجموعةً من البرامج المصمّمة للتلاعب بملف بيانات وحيد والتعامل معه بطريقة مترابطة وسليمة؛ إذ تهدف هذه البرامج لمساعدتنا بتتبع نتائج عدة لاعبين يتنافسون مع بعضهم بعضًا في لعبةٍ ما، مثل الشطرنج، أو الإسكواش على سبيل المثال. يمتلك كل لاعب تصنيفًا من واحد إلى n، إذ تمثل n عدد اللاعبين الكلي وواحد تصنيف أعلى لاعب. يستطيع اللاعبون من تصنيف منخفض تحدي لاعبين آخرين فوق تصنيفهم وينتقل اللاعب إلى تصنيف اللاعب الآخر الأعلى منه إذا انتصر عليه، وفي هذه الحالة يُنقل اللاعب الخاسر وأي لاعبين آخرين بين اللاعب الخاسر والفائز إلى تصنيف واحد أقل، وتبقى التصنيفات مثل ما هي إن لم ينتصر اللاعب الأقل تصنيفًا. لتقديم بعض الضوابط للتوازن في التصنيف، يمكن لأي لاعب أن يتحدى لاعبًا أعلى منه تصنيفًا، إلا أن التحديات مع اللاعبين ذوي التصنيف الذي يزيد عن ثلاثة أو أقل مراتب هي الوحيدة التي ستسمح لهذا اللاعب بالتقدم في التصنيف، وهذا من شأنه أن يُجبر اللاعبين الجدُد المضافين إلى أسفل التصنيف أن يلعبوا أكثر من لعبة واحدة للوصول إلى أعلى التصنيف. هناك ثلاث مهام أساسية يجب تنفيذها للمحافظة على تتبع سليم لنتائج التصنيفات: طباعة التصنيف. إضافة لاعبين جدُد. تسجيل النتائج. سيأخذ تصميم برنامجنا هنا صورة ثلاثة برامج جزئية لتنفيذ كل واحدة من هذه المهام على حدة، ومن الواضح بعد اتخاذنا لهذا القرار أن هناك عددًا من العمليات التي يحتاجها كل برنامج على نحوٍ مشترك بين البرامج الثلاث؛ فعلى سبيل المثال، ستحتاج البرامج الثلاثة إلى قراءة سجلات اللاعب من ملف البيانات وستحتاج اثنان من البرامج على الأقل لكتابة سجلات اللاعب إلى ملف البيانات. قد يكون الخيار الجيد هنا هو تصميم مكتبة من الدوال التي تتلاعب بسجلات اللاعبين وملف البيانات، واستخدام هذه المكتبة مع البرامج الثلاثة للتعامل مع تصنيف اللاعبين، إلا أننا بحاجة تعريف هيكل البيانات الذي سيمثل سجلات اللاعب قبل ذلك. تتألف المعلومات الدنيا اللازمة لإنشاء سجل لكل لاعب من اسمه وتصنيفه، إلا أننا سنحتفظ بعدد التحديات التي فاز بها اللاعب، إضافةً للتحديات التي خسرها وآخر لعبة لعبها لمنح بعض الإمكانيات الإحصائية عند تشكيل لائحة التصنيف، ومن الواضح أن هذه المجموعة من المعلومات يجب تخزينها في هيكل ما. نجد التصريح عن هيكل اللاعب والتصريح عن دوال مكتبة اللاعب في ملف الترويسة player.h، وتُخزّن البيانات في ملف البيانات مثل أسطر نصية، إذ يشير كل سطر إلى معلومات لاعب معين. يتطلب ذلك إجراء تحويلات الدخل والخرج، لكنها تقنيةٌ مفيدةٌ إذا لم تكلّف هذه التحويلات أداءً إضافيًا. /* * * التصاريح والتعاريف للدوال التي تتلاعب بسجلات اللاعب بناءً على ترتيبهم * */ #include <stddef.h> #include <stdio.h> #include <stdlib.h> #include <time.h> #define NAMELEN 12 /* الطول الأعظمي لاسم اللاعب */ #define LENBUF 256 /* الطول الأعظمي لذاكرة الدخل المؤقتة */ #define CHALLENGE_RANGE 3 // عدد اللاعبين الأعلى تصنيفًا الذين من الممكن للاعب أن يتحداهم ليزيد تصنيفه extern char *OptArg; typedef struct { char name[NAMELEN+1]; int rank; int wins; int losses; time_t last_game; } player; #define NULLPLAYER (player *)0 extern const char *LadderFile; extern const char *WrFmt; /* يُستخدم عند كتابة السجلات */ extern const char *RdFmt; /* يُستخدم عند قراءة السجلات */ /* تصاريح البرامج التي تُستخدم للتلاعب بسجلات اللاعب وملف لائحة التصنيف المعرفة في ملف‫ player.c */ int valid_records(FILE *); int read_records(FILE *, int, player *); int write_records(FILE *, player *, int); player *find_by_name(char *, player *, int); player *find_by_rank(int, player *, int); void push_down(player *, int, int, int); int print_records(player *, int); void copy_player(player *, player *); int compare_name(player *, player *); int compare_rank(player *, player *); void sort_players(player *, int); [مثال 4] إليك شيفرة ملف player.c الذي يستخدم بعض الدوال العامة للتلاعب بسجلات اللاعبين وملف البيانات، ويمكن أن تُستخدم هذه الدوال مع برامج أخرى محدد لتشكيل ثلاثة برامج تتعامل مع لائحة النتائج. لاحظ أنه يجب على كل برنامج أن يقرأ كامل البيانات من الملف إلى مصفوفة ديناميكية حتى نستطيع التلاعب بسجلات اللاعبين، ومن المفترض أن تكون جميع السجلات المحتواة داخل المصفوفة مُرتبةً حسب التصنيف قبل كتابتها مجددًا إلى ملف البيانات، وستولّد الدالة push_down بعض النتائج المثيرة للاهتمام إن لم تكن السجلات مرتبة. /* * الدوال الاعتيادية المستخدمة للتلاعب ببيانات ملف لائحة النتائج وسجلات اللاعبين */ #include "player.h" const char *LadderFile = "ladder"; const char *WrFmt = "%s %d %d %d %ld\n"; const char *RdFmt = "%s %d %d %d %ld"; /* تنبيه المستخدم بخصوص ضمّ السلاسل النصية */ const char *HeaderLine = "Player Rank Won Lost Last Game\n" "===============================================\n"; const char *PrtFmt = "%-12s%4d %4d %4d %s\n"; /*إعادة رقم السجلات الموجودة في الملف */ int valid_records(FILE *fp) { int i = 0; long plrs = 0L; long tmp = ftell(fp); char buf[LENBUF]; fseek(fp, 0L, SEEK_SET); for(i = 0; fgets(buf, LENBUF, fp) != NULL ; i++) ; /* استعادة مؤشر الملف إلى حالته الأصلية*/ fseek(fp, tmp, SEEK_SET); return i; } // ‫قراءة القيمة num من سجل اللاعب من الملف fp إلى المصفوفة them int read_records(FILE *fp, int num, player *them) { int i = 0; long tmp = ftell(fp); if(num == 0) return 0; fseek(fp, 0L, SEEK_SET); for(i = 0 ; i < num ; i++){ if(fscanf(fp, RdFmt, (them[i]).name, &((them[i]).rank), &((them[i]).wins), &((them[i]).losses), &((them[i]).last_game)) != 5) break; // خطأ عند‫ fscanf } fseek(fp, tmp, SEEK_SET); return i; } // كتابة‫ num الخاص بسجل اللاعب إلى الملف fp من المصفوفة them int write_records(FILE *fp, player *them, int num) { int i = 0; fseek(fp, 0L, SEEK_SET); for(i = 0 ; i < num ; i++){ if(fprintf(fp, WrFmt, (them[i]).name, (them[i]).rank, (them[i]).wins, (them[i]).losses, (them[i]).last_game) < 0) break; // ‫خطأ عند fprintf } return i; } /* إعادة مؤشر يشير إلى اللاعب في المصفوفة‫ them ذو اسم مطابق للقيمة name */ player *find_by_name(char * name, player *them, int num) { player *pp = them; int i = 0; for(i = 0; i < num; i++, pp++) if(strcmp(name, pp->name) == 0) return pp; return NULLPLAYER; } /* إعادة مؤشر يشير إلى لاعب في مصفوفة‫ them تُطابق رتبته القيمة rank */ player *find_by_rank(int rank, player *them, int num) { player *pp = them; int i = 0; for(i = 0; i < num; i++, pp++) if(rank == pp->rank) return pp; return NULLPLAYER; } /* ‫خفّض رتبة جميع اللاعبين في مصفوفة them إذا كانت رتبتهم بين start و end */ void push_down(player *them, int number, int start, int end) { int i; player *pp; for(i = end; i >= start; i--){ if((pp = find_by_rank(i, them, number)) == NULLPLAYER){ fprintf(stderr, "error: could not find player ranked %d\n", i); free(them); exit(EXIT_FAILURE); } else (pp->rank)++; } } // ‫طباعة سجل اللاعب num بصورةٍ مُنسّقة من المصفوفة them int print_records(player *them, int num) { int i = 0; printf(HeaderLine); for(i = 0 ; i < num ; i++){ if(printf(PrtFmt, (them[i]).name, (them[i]).rank, (them[i]).wins, (them[i]).losses, asctime(localtime(&(them[i]).last_game))) < 0) break; /* error on printf! */ } return i; } /* نسخ القيم من لاعب إلى آخر */ void copy_player(player *to, player *from) { if((to == NULLPLAYER) || (from == NULLPLAYER)) return; *to = *from; return; } /* مقارنة اسم اللاعب الأول مع اسم اللاعب الثاني */ int compare_name(player *first, player *second) { return strcmp(first->name, second->name); } /* مقارنة رتبة اللاعب الأول مع رتبة اللاعب الثاني */ int compare_rank(player *first, player *second) { return (first->rank - second->rank); } // ترتيب‫ num الذي يدل على سجل اللاعب في المصفوفة them void sort_players(player *them, int num) { qsort(them, num, sizeof(player), compare_rank); } [مثال 5] صُرّفت الشيفرة السابقة عند تجربتها إلى كائن ملف object file، الذي كان مربوطًا (مع كائن ملف يحتوي على الشيفرة البرمجية الخاصة بالدالة options) بواحدٍ من البرامج الثلاثة الخاصة بالتعامل مع لائحة النتائج. إليك الشيفرة البرمجية لواحدة من أبسط البرامج هذه، ألا وهو "showlddr"، الذي تحتوي على الملف "showlddr.c". يأخذ هذا البرنامج خيارًا واحدًا وهو ‎-f وقد تلاحظ أن هذا الخيار يأخذ وسيطًا اختياريًا أيضًا، والهدف من هذا الوسيط هو السماح بطباعة ملف بيانات لائحة التصنيف باستخدام اسم مغاير للاسم الافتراضي ladder. يجب أن تُخزّن سجلات اللاعب في ملف البيانات قبل ترتيبها، إلا أن showddlr يرتبها قبل أن يطبعها فقط بهدف التأكُّد. /* برنامج يطبع حالة لائحة النتائج الحالية */ #include "player.h" const char *ValidOpts = "f:"; const char *Usage = "usage: showlddr [-f ladder_file]\n"; char *OtherFile; int main(int argc, char *argv[]) { int number; char ch; player *them; const char *fname; FILE *fp; if(argc == 3){ while((ch = options(argc, argv, ValidOpts)) != -1){ switch(ch){ case 'f': OtherFile = OptArg; break; case '?': fprintf(stderr, Usage); break; } } } else if(argc > 1){ fprintf(stderr, Usage); exit(EXIT_FAILURE); } fname = (OtherFile == 0)? LadderFile : OtherFile; fp = fopen(fname, "r+"); if(fp == NULL){ perror("showlddr"); exit(EXIT_FAILURE); } number = valid_records (fp); them = (player *)malloc((sizeof(player) * number)); if(them == NULL){ fprintf(stderr,"showlddr: out of memory\n"); exit(EXIT_FAILURE); } if(read_records(fp, number, them) != number){ fprintf(stderr, "showlddr: error while reading" " player records\n"); free(them); fclose(fp); exit(EXIT_FAILURE); } fclose(fp); sort_players(them, number); if(print_records(them, number) != number){ fprintf(stderr, "showlddr: error while printing" " player records\n"); free(them); exit(EXIT_FAILURE); } free(them); exit(EXIT_SUCCESS); } [مثال 6] يعمل البرنامج "showlddr" فقط إذا كان هناك ملف بيانات يحتوي على سجلات اللاعب بالتنسيق الصحيح، ويُنشئ البرنامج "newplyr" ملفًا إذا لم يكن هناك أي ملف مُسبقًا ومن ثم يضيف بيانات اللاعب الجديد بالتنسيق الصحيح إلى ذلك الملف. يُدرج اللاعبون الجُدد عادةً أسفل التصنيف إلا أن هناك بعض الحالات الاستثنائية التي يسمح فيها "newplyr" بإدراج اللاعبين وسط التصنيف. يجب أن يظهر اللاعب مرةً واحدةً في التصنيف (إلا إذا تشابهت أسماء اللاعبين المستعارة) ويجب أن يكون لكل تصنيف لاعب واحد فقط، ولذا فإن البرنامج يفحص الإدخالات المتكررة وإذا كان من الواجب إدخال اللاعب الجديد إلى تصنيف مجاور لتصنيف اللاعبين الآخرين، يُزاح اللاعبون بعيدًا عن تصنيف اللاعب الجديد. يتعرّف البرنامج "newplyr" على الخيار ‎-f بصورةٍ مشابهة للبرنامج "showlddr"، ويفسره على أنه طلب إضافة اللاعب الجديد إلى ملف يُسمى باستخدام وسيط الخيار بدلًا من اسم الملف الافتراضي ألا وهو "ladder". يتطلب البرنامج "newplyr" أيضًا خيارين إضافيين ألا وهما n- و r- ويحدد كل وسيط خيار اسم اللاعب الجديد وتصنيفه الأوّلي بالترتيب. /* برنامج يُضيف لاعب جديد إلى لائحة التصنيفات، ويفترض أن تُسنِد رتبةً بقيمة واقعية إلى اللاعب */ #include "player.h" const char *ValidOpts = "n:r:f:"; char *OtherFile; static const char *Usage = "usage: newplyr -r rank -n name [-f file]\n"; /* تصاريح مسبقة للدوال المعرفة في هذا الملف*/ void record(player *extra); int main(int argc, char *argv[]) { char ch; player dummy, *new = &dummy; if(argc < 5){ fprintf(stderr, Usage); exit(EXIT_FAILURE); } while((ch = options(argc, argv, ValidOpts)) != -1){ switch(ch){ case 'f': OtherFile=OptArg; break; case 'n': strncpy(new->name, OptArg, NAMELEN); new->name[NAMELEN] = 0; if(strcmp(new->name, OptArg) != 0) fprintf(stderr, "Warning: name truncated to %s\n", new->name); break; case 'r': if((new->rank = atoi(OptArg)) == 0){ fprintf(stderr, Usage); exit(EXIT_FAILURE); } break; case '?': fprintf(stderr, Usage); break; } } if((new->rank == 0)){ fprintf(stderr, "newplyr: bad value for rank\n"); exit(EXIT_FAILURE); } if(strlen(new->name) == 0){ fprintf(stderr, "newplyr: needs a valid name for new player\n"); exit(EXIT_FAILURE); } new->wins = new->losses = 0; time(& new->last_game); // ‫أسند الوقت الحالي إلى last_game record(new); exit(EXIT_SUCCESS); } void record(player *extra) { int number, new_number, i; player *them; const char *fname =(OtherFile==0)?LadderFile:OtherFile; FILE *fp; fp = fopen(fname, "r+"); if(fp == NULL){ if((fp = fopen(fname, "w")) == NULL){ perror("newplyr"); exit(EXIT_FAILURE); } } number = valid_records (fp); new_number = number + 1; if((extra->rank <= 0) || (extra->rank > new_number)){ fprintf(stderr, "newplyr: rank must be between 1 and %d\n", new_number); exit(EXIT_FAILURE); } them = (player *)malloc((sizeof(player) * new_number)); if(them == NULL){ fprintf(stderr,"newplyr: out of memory\n"); exit(EXIT_FAILURE); } if(read_records(fp, number, them) != number){ fprintf(stderr, "newplyr: error while reading player records\n"); free(them); exit(EXIT_FAILURE); } if(find_by_name(extra->name, them, number) != NULLPLAYER){ fprintf(stderr, "newplyr: %s is already on the ladder\n", extra->name); free(them); exit(EXIT_FAILURE); } copy_player(&them[number], extra); if(extra->rank != new_number) push_down(them, number, extra->rank, number); sort_players(them, new_number); if((fp = freopen(fname, "w+", fp)) == NULL){ perror("newplyr"); free(them); exit(EXIT_FAILURE); } if(write_records(fp, them, new_number) != new_number){ fprintf(stderr, "newplyr: error while writing player records\n"); fclose(fp); free(them); exit(EXIT_FAILURE); } fclose(fp); free(them); } [مثال 7] البرنامج الأخير المطلوب هو البرنامج الذي يسجل نتائج الألعاب، ألا وهو برنامج "result". يقبل "result" خيار ‎-f كما هو الحال في البرنامجين الآخرين، مصحوبًا باسم الملف لتحديد بديل عن اسم ملف اللاعب الافتراضي. يعرض برنامج "result" عملية الإدخال للاعبين الخاسرين والرابحين على نحوٍ تفاعلي على عكس برنامج "newplyr"، ويصرّ على أن الأسماء يجب أن تكون للاعبين موجودين مسبقًا. يجري التحقّق من الأسماء بعد إعطاء اسمين صالحين، وذلك فيما إذا كان الخاسر أعلى تصنيفًا من الفائز، أو أن الفائز ذو تصنيف مقارب مما يسمح له بتغيير تصنيفه؛ وإذا لزُم تغيير للتصنيف، يأخذ المنتصر رتبة الخاسر ويُخفّض الخاسر رتبةً واحدةً (إضافةً إلى أي لاعب من تصنيف متداخل). إليك الشيفرة البرمجية الخاصة ببرنامج result. /* * برنامج يسجل النتائج * */ #include "player.h" /* تصريحات استباقية للدوال المعرفة في هذا الملف */ char *read_name(char *, char *); void move_winner(player *, player *, player *, int); const char *ValidOpts = "f:"; const char *Usage = "usage: result [-f file]\n"; char *OtherFile; int main(int argc, char *argv[]) { player *winner, *loser, *them; int number; FILE *fp; const char *fname; char buf[LENBUF], ch; if(argc == 3){ while((ch = options(argc, argv, ValidOpts)) != -1){ switch(ch){ case 'f': OtherFile = OptArg; break; case '?': fprintf(stderr, Usage); break; } } } else if(argc > 1){ fprintf(stderr, Usage); exit(EXIT_FAILURE); } fname = (OtherFile == 0)? LadderFile : OtherFile; fp = fopen(fname, "r+"); if(fp == NULL){ perror("result"); exit(EXIT_FAILURE); } number = valid_records (fp); them = (player *)malloc((sizeof(player) * number)); if(them == NULL){ fprintf(stderr,"result: out of memory\n"); exit(EXIT_FAILURE); } if(read_records(fp, number, them) != number){ fprintf(stderr, "result: error while reading player records\n"); fclose(fp); free(them); exit(EXIT_FAILURE); } fclose(fp); if((winner = find_by_name(read_name(buf, "winner"), them, number)) == NULLPLAYER){ fprintf(stderr,"result: no such player %s\n",buf); free(them); exit(EXIT_FAILURE); } if((loser = find_by_name(read_name(buf, "loser"), them, number)) == NULLPLAYER){ fprintf(stderr,"result: no such player %s\n",buf); free(them); exit(EXIT_FAILURE); } winner->wins++; loser->losses++; winner->last_game = loser->last_game = time(0); if(loser->rank < winner->rank) if((winner->rank - loser->rank) <= CHALLENGE_RANGE) move_winner(winner, loser, them, number); if((fp = freopen(fname, "w+", fp)) == NULL){ perror("result"); free(them); exit(EXIT_FAILURE); } if(write_records(fp, them, number) != number){ fprintf(stderr,"result: error while writing player records\n"); free(them); exit(EXIT_FAILURE); } fclose(fp); free(them); exit(EXIT_SUCCESS); } void move_winner(player *ww, player *ll, player *them, int number) { int loser_rank = ll->rank; if((ll->rank - ww->rank) > 3) return; push_down(them, number, ll->rank, (ww->rank - 1)); ww->rank = loser_rank; sort_players(them, number); return; } char *read_name(char *buf, char *whom) { for(;;){ char *cp; printf("Enter name of %s : ",whom); if(fgets(buf, LENBUF, stdin) == NULL) continue; /* حذف السطر الجديد */ cp = &buf[strlen(buf)-1]; if(*cp == '\n') *cp = 0; /* محرف واحد على الأقل */ if(cp != buf) return buf; } } [مثال 8] ترجمة -وبتصرف- للفصل Complete Programs in C من كتاب The C Book. اقرأ أيضًا المقال السابق: دوال التعامل مع السلاسل النصية والوقت والتاريخ في لغة سي C .بعض البرامج البسيطة بلغة سي C: المصفوفات والعمليات الحسابية العوامل في لغة سي C
  24. الهيكل struct أو البنية structure هو نوع بيانات مُخصّص يسمح لنا باستخدام عدة قيم بأسماء مختلفة في مجموعة واحدة ذات معنًى ما. يشبه الهيكل سمات attributes بيانات الكائن وفقًا لمفهوم البرمجة كائنية التوجه أو OOP. سنقارن في هذا المقال بين الصفوف tuples والهياكل، ونستعرض كل منها بناءً على ما تعلمته سابقًا، وسنوضح الحالات التي يكون فيها استخدام الهياكل خيارًا أفضل لتجميع البيانات، وكذلك كيفية تعريف وإنشاء الهياكل، إضافةً إلى تعريف الدوال المرتبطة بها وبالأخص الدوال التي تحدد السلوك المرتبط بنوع الهيكل، والتي تُدعى التوابع methods. تُعدّ الهياكل والمُعدّدات enums (سنناقشها لاحقًا) من لبنات بناء نوع بيانات جديد ضمن نطاق برنامجك وذلك للاستفادة الكاملة من خاصية التحقق من الأنواع في رست عند وقت التصريف. تعريف وإنشاء نسخة من الهياكل تُشابه الهياكل الصفوف التي ناقشناها سابقًا في هذه السلسلة البرمجة بلغة رست وذلك من حيث تخزينها لعدة قيم مترابطة، إذ يمكن للهياكل أن تُخزّن أنواع بيانات مختلفة كما هو الحال في الصفوف، إلا أننا نسمّي كل جزء من البيانات ضمن الهيكل بحيث يكون الهدف منها واضحًا على عكس الصفوف، وهذا يعني أن الهياكل أكثر مرونة من الصفوف إذ أننا لا نعتمد على ترتيب البيانات بعد الآن لتحديد القيمة التي نريدها. نستخدم الكلمة المفتاحية struct لتعريف الهيكل ونُلحقها باسمه، ويجب أن يصِف اسم الهيكل استخدام البيانات التي يجمعها ويحتويها، ومن ثمّ نستخدم الأقواس المعقوصة curly brackets لتعريف أسماء وأنواع البيانات التي يحتويها الهيكل وتُدعى هذه البيانات باسم الحقول fields، فعلى سبيل المثال توضح الشيفرة 1 هيكلًا يحتوي داخله معلومات تخص معلومات عن حساب مستخدم. struct User { active: bool, username: String, email: String, sign_in_count: u64, } [الشيفرة 1: تعريف الهيكل User] نُنشئ نسخةً instance من الهيكل الذي عرّفناه حتى نستطيع استخدامه، ومن ثم نحدّد قيمًا لكل حقل ضمنه. نستطيع إنشاء نسخة من الهيكل عن طريق ذكر اسم الهيكل ومن ثم إضافة أقواس معقوصة تحتوي على ثنائيات من التنسيق key: value، إذ يمثّل المفتاح key اسم الحقل، بينما تمثّل القيمة value البيانات التي نريد تخزينها في ذلك الحقل، وليس من الواجب علينا ذكر الحقول بالترتيب المذكور في تصريح الهيكل؛ أي بكلمات أخرى، يُعدّ تعريف الهيكل بمثابة قالب عام للنوع وتملأ النُسخ ذلك القالب ببيانات معينة لإنشاء قيمة من نوع الهيكل، ويمكن مثلًا التصريح عن مستخدم ما كما هو موضح في الشيفرة 2. fn main() { let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; } [الشيفرة 2: إنشاء نسخة من الهيكل User] نستخدم النقطة (.) للحصول على قيمة محددة من هيكل ما؛ فإذا أردنا مثلًا الحصول على البريد الإلكتروني الخاص بالمستخدم فقط، فيمكننا كتابة user1.email عندما نريد استخدام تلك القيمة؛ وإذا كانت النسخة تلك قابلة للتعديل mutable، فيمكننا تغيير القيمة باستخدام الطريقة ذاتها أيضًا وإسناد الحقل إلى قيمة جديدة. توضح الشيفرة 3 كيفية تغيير القيمة في الحقل email الخاصة بالهيكل User القابل للتعديل. fn main() { let mut user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; user1.email = String::from("anotheremail@example.com"); } [الشيفرة 3: تعديل قيمة الحقل email الخاصة بنسخة من الهيكل User] لاحظ أنه يجب أن تكون كامل النسخة قابلة للتعديل، إذ لا تسمح لنا رست بتعيين حقول معينة من الهيكل على أنها حقول قابلة للتعديل. يمكننا إنشاء نسخة جديدة من الهيكل في آخر تعبير من الدالة، بحيث تُعيد الدالة نسخةً جديدةً من ذلك الهيكل كما هو الحال في التعابير الأخرى. توضح الشيفرة 4 الدالة build_user التي تُعيد نسخةً من الهيكل User باستخدام اسم مستخدم وبريد إلكتروني يُمرّران إليها، إذ يحصل الحقل active على القيمة true، بينما يحصل الحقل sign_in_count على القيمة "1". fn build_user(email: String, username: String) -> User { User { email: email, username: username, active: true, sign_in_count: 1, } } [الشيفرة 4: الدالة build_user التي تأخذ بريد إلكتروني واسم مستخدم ومن ثمّ تُعيد نسخة من الهيكل User] من المنطقي أن نُسمّي معاملات الدالة وفق أسماء حقول الهيكل، إلا أن تكرار كتابة أسماء الحقول والمتغيرات email و username يصبح رتيبًا بعض الشيء، وقد يصبح الأمر مزعجًا مع زيادة عدد حقول الهيكل، إلا أن هناك اختصارًا لذلك لحسن الحظ. ضبط قيمة حقول الهيكل بطريقة مختصرة يُمكننا استخدام طريقة مختصرة في ضبط قيمة حقول الهيكل بما أن أسماء معاملات الدالة وأسماء حقول الهيكل متماثلة في الشيفرة 4، ولاستخدام هذه الطريقة نُعيد كتابة الدالة build_user بحيث تؤدي الغرض ذاته دون تكرار أي من أسماء الحقول email و username كما هو موضح في الشيفرة 5. fn build_user(email: String, username: String) -> User { User { email, username, active: true, sign_in_count: 1, } } [الشيفرة 5: دالة build_user تستخدم طريقة إسناد قيم الحقول المختصر لأن لمعاملات email و username الاسم ذاته لحقول الهيكل] نُنشئ هنا نسخةً جديدةً من الهيكل User الذي يحتوي على هيكل يدعى email، ونضبط قيمة الحقل email إلى قيمة المعامل email المُمرّر إلى الدالة build_user، وذلك لأن للحقل والمعامل الاسم ذاته وبذلك نستطيع كتابة email بدلًا من كتابة email: email. إنشاء نسخ من نسخ أخرى عن طريق صيغة تحديث الهيكل من المفيد غالبًا إنشاء نسخة جديدة من الهيكل، الذي يتضمن معظم القيم من نسخةٍ أخرى ولكن مع بعض التغييرات وذلك باستخدام صيغة تحديث الهيكل struct update syntax. تظهر الشيفرة 6 كيفية إنشاء نسخة مستخدم user جديد في المستخدم user2 بصورةٍ منتظمة دون استخدام صيغة تحديث الهيكل، إذ ضبطنا قيمة جديدة لحقل email بينما استخدمنا نفس القيم للمستخدم user1 المُنشأ في الشيفرة 5. fn main() { // --snip-- let user2 = User { active: user1.active, username: user1.username, email: String::from("another@example.com"), sign_in_count: user1.sign_in_count, }; } [الشيفرة 6: إنشاء نسخة user باستخدام إحدى قيم المستخدم user1] يمكننا باستخدام صيغة تحديث الهيكل تحقيق نفس التأثير وبشيفرة أقصر، كما هو موضح في الشيفرة 7، إذ تدل الصيغة .. على أن باقي الحقول غير المضبوطة ينبغي أن يكون لها نفس قيم الحقول في النسخة المحددة. fn main() { // --snip-- let user2 = User { email: String::from("another@example.com"), ..user1 }; } [الشيفرة 7: استخدام صيغة تحديث الهيكل لضبط قيمة email لنسخة هيكل المستخدم User واستخدام بقية قيم المستخدم user1] تنشئ الشيفرة 7 أيضًا نسخةً في الهيكل user2 بنفس قيم الحقول username و active و sign_in_count للهيكل user1 ولكن لها قيمة email مختلفة. يجب أن يأتي المستخدم user1.. أخيرًا ليدل على أن الحقول المتبقية يجب أن تحصل على قيمها من الحقول المقابلة في الهيكل user1، ولكن يمكننا تحديد قيم أي حقل من الحقول دون النظر إلى ترتيب الحقول الموجود في تعريف الهيكل. تستخدم صيغة تحديث الهيكل الإسناد =، لأنه ينقل البيانات تمامًا كما هو الحال في القسم "طرق التفاعل مع البيانات والمتغيرات: النقل" من مقال الملكية ownership في لغة رست. لم يعد بإمكاننا في هذا المثال استخدام user1 بعد إنشاء user2 بسبب نقل السلسلة النصية String الموجودة في الحقل username من المستخدم user1 إلى user2. إذا أعطينا قيم سلسلة نصية جديدة إلى user2 لكلا الحقلين username و email واستخدمنا فقط قيم الحقلين active و sign_in_count من الهيكل user1، سيبقى الهيكل user1 في هذه الحالة صالحًا بعد إنشاء user2. تكون أنواع بيانات الحقلين active و sign_in_count بحيث تنفّذ السمة Copy، وبالتالي سينطبق هنا الأسلوب الذي ناقشناه في القسم الأول من الفصل المشار إليه آنفًا. استخدام هياكل الصفوف دون حقول مسماة لإنشاء أنواع مختلفة تدعم رست الهياكل التي تبدو مشابهةً للصفوف، وتُدعى بهياكل الصفوف tuple structs، ولهياكل الصفوف حقول مشابهة للهياكل العادية إلا أنها عديمة التسمية، وإنما لكل حقل نوع بيانات فقط. هياكل الصفوف مفيدة عندما تريد تسمية صف ما وأن تجعل الصف من نوع مختلف عن الصفوف الأخرى، أو عندما لا تكون تسمية كل حقل في الهيكل الاعتيادي عملية ضرورية. لتعريف هيكل صف، نبدأ بالكلمة المفتاحية struct ومن ثم اسم الصف متبوعًا بالأنواع الموجودة في الصف. على سبيل المثال، نعرّف هنا هيكلا صف بالاسم Color و Point ونستخدمهما: struct Color(i32, i32, i32); struct Point(i32, i32, i32); fn main() { let black = Color(0, 0, 0); let origin = Point(0, 0, 0); } لاحظ أن black و origin من نوعين مختلفين، وذلك لأنهما نسختان من هياكل صف مختلفة، إذ يُعدّ كل هيكل تُعرفه نوعًا مختلفًا حتى لو كانت الحقول داخل الهيكل من نوع مماثل لهيكل آخر، على سبيل المثال، لا يمكن لدالة تأخذ النوع Color وسيطًا أن تأخذ النوع Point على الرغم من أن النوعين يتألفان من قيم من النوع i32. إضافةً لما سبق، يُماثل تصرف هياكل الصفوف تصرف الصفوف؛ إذ يمكنك تفكيكها إلى قطع متفرقة أو الوصول إلى قيم العناصر المختلفة بداخلها عن طريق استخدام النقطة (.) متبوعةً بدليل العنصر، وهكذا. الهياكل الشبيهة بالوحدات بدون أي حقول يُمكنك أيضًا تعريف هياكل لا تحتوي على أي حقول، وتدعى الهياكل الشبيهة بالوحدات unit-like structs لأنها مشابهة لنوع الوحدة unit type () الذي تحدثنا عنه سابقًا. يُمكن أن نستفيد من الهياكل الشبيهة بالوحدات عندما نريد تطبيق سمة trait على نوع ما ولكننا لا نملك أي بيانات نريد تخزينها داخل النوع، وسنناقش مفهوم السمات لاحقًا. إليك مثالًا عن تصريح وإنشاء نسخة من هيكل شبيه بالوحدات يدعى AlwaysEqual: struct AlwaysEqual; fn main() { let subject = AlwaysEqual; } تُستخدم الكلمة المفتاحية struct لتعريف AlwaysEqual، ثم تُلحق بالاسم الذي نريده ومن ثم الفاصلة المنقوطة، ولا حاجة هنا للأقواس إطلاقًا. يمكننا بعد ذلك الحصول على نسخة من الهيكل AlwaysEqual في المتغير subject بطريقة مماثلة، وذلك باستخدام الاسم الذي عرّفناه مسبقًا دون أي أقواس عادية أو معقوصة. نستطيع تطبيق سلوك على النوع فيما بعد بحيث تُساوي كل نسخة من AlwaysEqual قيمة أي نوع مُسند إليه، والتي من الممكن أن تكون قيمةً معروفة بهدف التجريب، وفي هذه الحالة لن نحتاج إلى أي بيانات لتطبيق هذا السلوك، وسترى لاحقًا كيف بإمكاننا تعريف السمات وتطبيقها على أي نوع بما فيه الهياكل الشبيهة بالوحدات. ملكية بيانات الهيكل استخدمنا في الشيفرة 1 النوع String في الهيكل User الموجود بدلًا عن استخدام نوع شريحة سلسلة نصية string slice type بالشكل ‎&str، وهذا استخدام مقصود لأننا نريد لكل نسخة من هذا الهيكل أن تمتلك جميع بياناتها وأن تكون بياناتها صالحة طالما الهيكل بكامله صالح. من الممكن للهياكل أيضًا أن تُخزِّن مرجعًا لبيانات يمتلكها شيء آخر، إلا أن ذلك يتطلب استخدام دورات الحياة lifetimes وهي ميزة في رست سنتحدث عنها لاحقًا، إذ تضمن دورات الحياة أن البيانات المُشار إليها باستخدام الهيكل هي بيانات صالحة طالما الهيكل صالح. دعنا نفترض أنك تريد تخزين مرجع في هيكل ما دون تحديد دورات الحياة كما يلي (لن ينجح الأمر): اسم الملف: src/main.rs struct User { active: bool, username: &str, email: &str, sign_in_count: u64, } fn main() { let user1 = User { email: "someone@example.com", username: "someusername123", active: true, sign_in_count: 1, }; } سيشكو المصرّف ويخبرك أنه بحاجة محدّدات دورات الحياة lifetime specifiers: $ cargo run Compiling structs v0.1.0 (file:///projects/structs) error[E0106]: missing lifetime specifier --> src/main.rs:3:15 | 3 | username: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 ~ username: &'a str, | error[E0106]: missing lifetime specifier --> src/main.rs:4:12 | 4 | email: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 | username: &str, 4 ~ email: &'a str, | For more information about this error, try `rustc --explain E0106`. error: could not compile `structs` due to 2 previous errors سنناقش كيفية حل هذه المشكلة لاحقًا، بحيث يمكنك تخزين المراجع في الهياكل، إلا أننا سنستخدم حاليًا الأنواع المملوكة owned types مثل String بدلًا عن المراجع مثل ‎&str لتجنب هذه المشكلة. مثال على برنامج يستخدم الهياكل دعنا نكتب برنامجًا يحسب مساحة مستطيل حتى نفهم فائدة استخدام الهياكل وأين نستطيع الاستفادة منها. نبدأ أوّلًا بكتابة البرنامج باستخدام متغيرات منفردة، ومن ثمّ نعيد كتابة البرنامج باستخدام الهياكل بدلًا من ذلك. نُنشئ مشروعًا ثنائيًا جديدًا باستخدام كارجو Cargo باسم "rectangles"، ويأخذ هذا البرنامج طول وعرض المستطيل بالبيكسل pixel ويحسب مساحة المستطيل. توضح الشيفرة 8 برنامجًا قصيرًا ينفذ ذلك بإحدى الطرق ضمن الملف "src/main.rs". fn main() { let width1 = 30; let height1 = 50; println!( "The area of the rectangle is {} square pixels.", area(width1, height1) ); } fn area(width: u32, height: u32) -> u32 { width * height } [الشيفرة 8: حساب مساحة المستطيل المُحدّد بمتغيّرَي الطول والعرض] دعنا الآن نُنفّذ البرنامج باستخدام الأمر cargo run: $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) Finished dev [unoptimized + debuginfo] target(s) in 0.42s Running `target/debug/rectangles` The area of the rectangle is 1500 square pixels. ينجح برنامجنا في حساب مساحة المستطيل باستدعاء الدالة area باستخدام بُعدَي المستطيل، إلا أنه يمكننا تعديل الشيفرة البرمجية لتصبح أكثر وضوحًا وسهلة القراءة. تكمن المشكلة في شيفرتنا البرمجية الحالية في بصمة signature الدالة area: fn area(width: u32, height: u32) -> u32 { يُفترض أن تحسب الدالة area مساحة مستطيل واحد، إلا أن الدالة التي كتبناها تأخذ مُعاملَين وليس من الواضح حاليًا أن المُعاملين مرتبطان فيما بينهما، ومن الأفضل هنا أن نجمع قيمة الطول والعرض سويًّا، وقد ناقشنا كيف قد نفعل ذلك سابقًا باستخدام الصفوف. إعادة كتابة البرنامج باستخدام الصفوف توضح الشيفرة 9 إصدارًا آخر من برنامجنا باستخدام الصفوف. fn main() { let rect1 = (30, 50); println!( "The area of the rectangle is {} square pixels.", area(rect1) ); } fn area(dimensions: (u32, u32)) -> u32 { dimensions.0 * dimensions.1 } [الشيفرة 9: تخزين طول وعرض المستطيل في صف] البرنامج الحالي أفضل، إذ تسمح لنا الصفوف بتنظيم القيم على نحوٍ أفضل، إضافةً إلى أننا نمرّر للدالة الآن معاملًا واحدًا، إلا أن هذا الإصدار أقل وضوحًا، لأن الصفوف لا تُسمّي عناصرها وبالتالي علينا استخدام دليل العنصر للوصول إلى القيمة التي نريدها مما يجعل من حساباتنا أقل وضوحًا. الخلط بين الطول والعرض في حساب المساحة ليس بالأمر الجلل، إلا أن التمييز بين القيمتين يصبح مهمًّا في حال أردنا رسم المستطيل على الشاشة، وفي هذه الحالة علينا تذكّر أن قيمة العرض width مُخزّنة في دليل الصف "0" وقيمة العرض height مخزّنة في دليل الصف "1"، وقد يكون هذا الأمر صعبًا إذا أراد أحدٌ التعديل على شيفرتنا البرمجية، أي أن شيفرتنا البرمجية لن تكون مُعبّرة وواضحة مما يرفع نسبة الأخطاء واردة الحدوث. إعادة كتابة البرامج باستخدام الهياكل وبوضوح أكبر نستخدم الهياكل حتى نجعل من بياناتنا ذات معنى بتسميتها، إذ يمكننا تحويل الصف المُستخدَم سابقًا إلى هيكل باسم له، إضافةً إلى اسمٍ لكل حقلٍ داخله كما هو موضح في الشيفرة 10. اسم الملف: src/main.rs struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", area(&rect1) ); } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height } [الشيفرة 10: تعريف الهيكل Rectangle] عرّفنا هنا هيكلًا اسمه Rectangle، ثم عرّفنا داخل الأقواس المعقوصة حقلَي الهيكل width و height من النوع u32، وننشئ بعدها نسخةً من الهيكل ضمن الدالة main بعرض 30 بيكسل وطول 50 بيكسل. الدالة area في هذا الإصدار من البرنامج معرّفة بمعامل واحد، وقد سمّيناه rectangle وهو من نوع الهيكل Rectangle القابل للإستعارة دون تعديل، وكما ذكرنا سابقًا، يمكننا استعارة borrow الهيكل بدلًا من الحصول على ملكيته وبهذه الطريقة تحافظ الدالة main على ملكيته ويمكننا استخدامه عن طريق النسخة rect1 وهذا هو السبب في استخدامنا للرمز & في بصمة الدالة وعند استدعائها. تُستخدَم الدالة area في الوصول لحقلَي نسخة الهيكل Rectangle وهُما width و height، وتدلّ بصمة الدالة هنا على ما نريد فعله بوضوح: احسب مساحة Rectangle باستخدام قيمتَي الحقلين width و height، وهذا يدلّ قارئ الشيفرة البرمجية على أن القيمتين مترابطتين فيما بينهما ويُعطي اسمًا واصفًا واضحًا لكل من القيمتُين بدلًا من استخدام قيم دليل الصف "0" و"1" كما سبق. وهذا الإصدار الأوضح حتى اللحظة. بعض الإضافات المفيدة باستخدام السمات المشتقة سيكون من المفيد أن نطبع نسخةً من الهيكل Rectangle عند تشخيص أخطاء برنامجنا للتحقق من قيم الحقول، نُحاول في الشيفرة 11 فعل ذلك باستخدام الماكرو !println كما عهدنا في المقالات السابقة إلا أن هذا الأمر لن ينجح. اسم الملف: src/main.rs struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {}", rect1); } [الشيفرة 11: محاولة طباعة نسخة من الهيكل Rectangle] نحصل على رسالة الخطأ التالية عندما نُصرّف الشيفرة البرمجية السابقة: error[E0277]: `Rectangle` doesn't implement `std::fmt::Display` يمكّننا الماكرو !println من تنسيق الخرج بعدّة أشكال، إلا أن التنسيق الافتراضي هو التنسيق المعروف باسم Display الذي يستخدم أسلوب كتابة الأقواس المعقوصة، وهو تنسيق موجّه لمستخدم البرنامج، وتستخدم أنواع البيانات الأولية primitive التي استعرضناها حتى الآن تنسيق Display افتراضيًا، وذلك لأن هناك طريقةً واحدةً لعرض القيمة "1" -أو أي قيمة نوع أولي آخر- للمستخدم، لكن الأمر مختلف مع الهياكل إذ أن هناك عدّة احتمالات لعرض البيانات التي بداخلها؛ هل تريد الطباعة مع الفواصل أم بدونها؟ هل تريد طباعة الأقواس المعقوصة؟ هل تريد عرض جميع الحقول؟ وبسبب هذا لا تحاول رست تخمين الطريقة التي نريد عرض الهيكل بها ولا يوجد أي تطبيق لطباعة الهيكل باستخدام النمط Display في الماكرو println!‎ باستخدام الأقواس المعقوصة {}. إذا قرأنا رسالة الخطأ نجد الملاحظة المفيدة التالية: = help: the trait `std::fmt::Display` is not implemented for `Rectangle` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead تخبرنا الملاحظة أنه يجب علينا استخدام تنسيق محدّد لطباعة الهيكل، لنجرّب ذلك! يصبح استدعاء الماكرو println!‎ بالشكل التالي: println!("rect1 is {:?}, rect1);‎ يدّل المحدد ?:‎ داخل الأقواس المعقوصة على أننا نريد استخدام تنسيق طباعة يدعى Debug، إذ يُمكّننا هذا النمط من طباعة الهيكل بطريقة مفيدة للمطورين عند تشخيص أخطاء الشيفرة البرمجية. صرّف الشيفرة البرمجية باستخدام التغيير السابقة، ونفّذ البرنامج. حصلنا على خطأ من جديد. error[E0277]: `Rectangle` doesn't implement `Debug` إلا أن المصرف يساعدنا مجددًا بملاحظة مفيدة: = help: the trait `Debug` is not implemented for `Rectangle` = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle` لا تُضمّن رست إمكانية طباعة المعلومات المتعلقة بتشخيص الأخطاء، إذ عليك أن تحدّد صراحةً أنك تريد استخدام هذه الوظيفة ضمن الهيكل الذي تريد طباعته، ولفعل ذلك نُضيف السمة الخارجية ‎#[derive(Debug)]‎ قبل تعريف الهيكل كما هو موضح في الشيفرة 12. اسم الملف: src/main.rs #[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {:?}", rect1); } [الشيفرة 12: إضافة سمة للهيكل للحصول على السمة المُشتقة Debug وطباعة نسخة من الهيكل Rectangle باستخدام تنسيق تشخيص الأخطاء] لن نحصل على أي أخطاء أخرى عندما ننفذ البرنامج الآن، وسنحصل على الخرج التالي: $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) Finished dev [unoptimized + debuginfo] target(s) in 0.48s Running `target/debug/rectangles` rect1 is Rectangle { width: 30, height: 50 } رائع، فعلى الرغم من أن تنسيق الخرج ليس جميلًا إلا أنه يعرض لنا جميع حقول النسخة، وهذا سيساعدنا بالتأكيد خلال عملية تشخيص الأخطاء. من المفيد استخدام المُحدد {?#:} بدلًا من المحدد {?:} عندما يكون لدينا هياكل ضخمة، ونحصل على الخرج بالطريقة التالية عند استخدام المحدد السابق: $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) Finished dev [unoptimized + debuginfo] target(s) in 0.48s Running `target/debug/rectangles` rect1 is Rectangle { width: 30, height: 50, } يمكننا طباعة القيم بطريقة أخرى، وهي باستخدام الماكرو dbg!‎، إذ يأخذ هذا الماكرو ملكية التعبير ويطبع الملف ورقم السطر الذي ورد فيه الماكرو، إضافةً إلى القيمة الناتجة من التعبير، ثمّ يُعيد الملكية للقيمة. ملاحظة: يطبع استدعاء الماكرو dbg!‎ الخرج إلى مجرى أخطاء الطرفية القياسي standard error console stream أو كما يُدعى "stderr" على عكس الماكرو println!‎ الذي يطبع الخرج إلى مجرى خرج الطرفية القياسي standard output console stream أو كما يُدعى "stdout"، وسنتحدث بصورةٍ موسعة عن "stderr" و "stdout" لاحقًا. إليك مثالًا عمّا سيبدو عليه برنامجنا إذا كُنّا مهتمين بمعرفة القيمة المُسندة إلى الحقل width إضافةً إلى قيم كامل الهيكل في النسخة rect1: #[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let scale = 2; let rect1 = Rectangle { width: dbg!(30 * scale), height: 50, }; dbg!(&rect1); } يُمكننا كتابة dbg!‎ حول التعبير 30‎ *‎ scale وسيحصل الحقل width على القيمة ذاتها في حالة عدم استخدامنا لاستدعاء dbg!‎ هنا لأن dbg!‎ يُعيد الملكية إلى قيمة التعبير، إلا أننا لا نريد للماكرو dbg!‎ أن يأخذ ملكية rect1، لذلك سنستخدم مرجعًا إلى rect1 في الاستدعاء الثاني. إليك ما سيبدو عليه الخرج عند تنفيذ البرنامج: $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) Finished dev [unoptimized + debuginfo] target(s) in 0.61s Running `target/debug/rectangles` [src/main.rs:10] 30 * scale = 60 [src/main.rs:14] &rect1 = Rectangle { width: 60, height: 50, } يُمكننا ملاحظة أن الجزء الأول من الخرج طُبِعَ نتيجةً للسطر البرمجي العاشر ضمن الملف src/main.rs، إذ أردنا تشخيص الخطأ في التعبير 30‎ *‎ scale والقيمة 60 الناتجة عنه (يطبع تنسيق Debug فقط القيمة في حالة الأعداد الصحيحة)، بينما يطبع الاستدعاء الثاني للماكرو dbg!‎ الوارد في السطر الرابع عشر ضمن الملف src/main.rs قيمة ‎&rect1 الذي هو بدوره نسخةٌ من الهيكل Rectangle، ويستخدم الخرج تنسيق الطباعة Debug ضمن النوع Rectangle. يُمكن للماكرو dbg!‎ أن يكون مفيدًا في العديد من الحالات التي تريد فيها معرفة ما الذي تفعله شيفرتك البرمجية. تزوّدنا رست بعدد من السمات الأخرى بجانب السمة Debug، ونستطيع استخدامها بواسطة السمة derive مما يُمكّننا من إضافة سلوكيات مفيدة إلى أنواع البيانات المُخصصة، نوضح السمات وسلوكياتها في الملحق (ت)، وسنغطي كيفية إنشاء سمات مُخصصة لاحقًا. هناك العديد من السمات الأخرى بجانب derive، للحصول على معلومات أكثر انظر Attributes. الدالة area الموجودة لدينا الآن ضيقة الاستخدام، إذ أنها تحسب فقط مساحة المستطيل ومن المفيد أن نربط سلوكها بصورةٍ أكبر مع الهيكل Rectangle لأنها لا تعمل مع أي نوع آخر، وسننظر لاحقًا إلى إصدارات أخرى من هذا البرنامج التي ستحوّل الدالة area إلى تابع area ضمن النوع Rectangle. ‎ترجمة -وبتصرف- لقسم من الفصل Using Structs to Structure Related Data من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست الملكية Ownership في لغة رست التحكم بسير تنفيذ برامج راست Rust أنواع البيانات Data Types في لغة رست Rust
  25. نتطرّق في هذا المقال إلى طرق مختلفة في التعامل مع السلاسل النصية والتلاعب بها، وذلك عن طريق دوال مكتبة string.h، ومن ثمّ ننتقل إلى دوال الوقت والتاريخ المحتواة في مكتبة time.h. التعامل مع السلاسل النصية هناك العديد من الدوال التي تسمح لنا بالتعامل مع السلاسل النصية، إذ تكون السلسلة النصية في لغة سي مؤلفةً من مصفوفة من المحارف تنتهي بمحرف فارغ null، وتتوقع الدوال في جميع الحالات تمرير مؤشر يشير إلى المحرف الأول ضمن السلسلة النصية، ويعرّف ملف الترويسة <string.h> هذا النوع من الدوال. النسخ يضم هذا التصنيف الدوال التالية: #include <string.h> void *memcpy(void *s1, const void *s2, size_t n); void *memmove (void *s1, const void *s2, size_t n); char *strcpy(char *s1, const char *s2); char *strncpy(char *s1, const char *s2, size_t n); char *strcat(char *s1, const char *s2); char *strncat(char *s1, const char *s2, size_t n); دالة memcpy: تنسخ هذه الدالة n بايت من المكان الذي يشير إليه المؤشر s2 إلى المكان الذي يشير إليه المؤشر s1، ونحصل على سلوك غير محدد إذا كان الكائنان متداخلان overlapping objects. تعيد الدالة s1. دالة memmove: هذه الدالة مطابقة لعمل دالة memcpy إلا أنها تعمل على الكائنات المتداخلة، إلا أنها قد تكون أبطأ. دالتَي strcpy وstrncpy: تنسخ كلا الدالتين السلسلة النصية التي يشير إليها المؤشر s2 إلى سلسلة نصية يشير المؤشر s1 إليها متضمنًا ذلك المحرف الفارغ في نهاية السلسلة. تنسخ strncpy سلسلةً نصيةً بطول n بايت على الأكثر، وتحشو ما تبقى بمحارف فارغة إذا كانت s2 أقصر من n محرف، ونحصل على سلوك غير معرّف، إذا كانت السلسلتان متقاطعتين، وتُعيد كلا الدالتين s1. الدالتان strcat و strncat: تُضيف كلا الدالتين السلسلة النصية s2 إلى السلسلة s1 بالكتابة فوق overwrite المحرف الفارغ في نهاية السلسلة s1، بينما يُضاف المحرف الفارغ دائمًا إلى نهاية السلسلة. يمكن إضافة n محرف على الأكثر من السلسلة s2 باستخدام الدالة strncat مما يعني أن السلسلة النصية الهدف (أي s1) يجب أن تحتوي على مساحة لطولها الأصلي (دون احتساب المحرف الفارغ) زائد n+1 محرف للتنفيذ الآمن. تعيد الدالتين s1. مقارنة السلسلة النصية والبايت تُستخدم هذه الدوال في مقارنة مصفوفات من البايتات، وهذا يتضمن طبعًا السلاسل النصية في لغة سي إذ أنها سلسلةٌ من المحارف char (أي البايتات) بمحرف فارغ في نهايتها. تعمل جميع هذه الدوال التي سنذكرها على مقارنة بايت تلو الآخر وتتوقف فقط في حالة اختلف بايت مع بايت آخر (في هذه الحالة تُعيد الدالة إشارة الفرق بين البايت والآخر) أو عندما تكون المصفوفتان متساويتين (أي لم يُعثر على أي فرق بينهما وكان طولهما مساوٍ إلى الطول المحدد أو -في حالة المقارنة بين السلاسل النصية- وُجد المحرف الفارغ في النهاية). القيمة المُعادة في جميع الدوال عدا الدالة strxfrm هي أصغر من الصفر، أو تساوي الصفر، أو أكبر من الصفر وذلك في حالة كان الكائن الأول أصغر من الكائن الثاني، أو يساوي الكائن الثاني، أو أكبر من الكائن الثاني على الترتيب. #include <string.h> int memcmp(const void *s1, const void *s2, size_t n); int strcmp(const char *s1, const char *s2); int strncmp(const char *s1, const char *s2, size_t n); size_t strxfrm(char *to, const char *from, int strcoll(const char *s1, const char *s2); دالة memcmp: تُقارن أول n محرف في الكائن الذي يشير إليه المؤشر s1 وs2، إلا أن مقارنة الهياكل بهذه الطريقة ليست مثالية، إذ قد تحتوي الاتحادات unions أو "الثقوب holes" المُسببة بواسطة محاذاة ذاكرة التخزين على بيانات غير صالحة. دالة strcmp: تُقارن سلسلتين نصيتين وهي إحدى أكثر الدوال استخدامًا عند التعامل مع السلاسل النصية. الدالة strncmp: تُطابق عمل الدالة strcmp إلا أنها تقارن n محرف على الأكثر. الدالة strxfrm: تُحوّل السلسلة النصية المُمرّرة إليها (بصورةٍ خاصة ومميزة)، وتُخزّن إلى موضع المؤشر، ويُكتب maxsize محرف على الأكثر إلى موضع المؤشر (متضمنًا المحرف الفارغ في النهاية)، وتضمن طريقة التحويل أننا سنحصل على نتيجة المقارنة ذاتها لسلسلتين نصيتين محوّلتين ضمن إعدادات المستخدم المحلية عند استخدام الدالة strcmp بعد تطبيق الدالة strcoll على السلسلتين النصيتين الأساسيتين. نحصل على طول السلسلة النصية الناتجة مثل قيمة مُعادة في جميع الحالات (باستثناء المحرف الفارغ في نهاية السلسلة)، وتكون محتويات المؤشر to غير معرّفة إذا كانت القيمة مساوية أو أكبر من maxsize، وقد تكون s1 مؤشرًا فارغًا إذا كانت maxsize صفر، ونحصل على سلوك غير معرّف إذا كان الكائنان متقاطعين. دالة strcoll تُقارن هذه الدالة سلسلتين نصيتين بحسب سلسلة الترتيب collating sequence المحدد في إعدادات اللغة المحلية. دوال بحث المحارف والسلاسل النصية يتضمن التصنيف الدوال التالية: #include <string.h> void *memchr(const void *s, int c, size_t n); char *strchr(const char *s, int c); size_t strcspn(const char *s1, const char *s2); char *strpbrk(const char *s1, const char *s2); char *strrchr(const char *s, int c); size_t strspn(const char *s1, const char *s2); char *strstr(const char *s1, const char *s2); char *strtok(const char *s1, const char *s2); دالة memchr: تُعيد مؤشرًا يشير إلى أول ظهور ضمن أول n محرف من s* للمحرف c (من نوع unsigned char)، وتُعيد فراغًا null إن لم يُعثر على أي تطابق. دالة strchr: تُعيد مؤشرًا يشير إلى أول ظهور للمحرف c ضمن s* ويتضمن البحث المحرف الفارغ، وتُعيد فراغًا null إذا لم يُعثر على أي تطابق. دالة strcspn: تُعيد طول الجزء الأولي للسلسلة النصية s1 الذي لا يحتوي أيًّا من محارف السلسلة s2، ولا يؤخذ المحرف الفارغ في نهاية السلسلة s2 بالحسبان. دالة strpbrk: تُعيد مؤشرًا إلى أول محرف ضمن s1 يطابق أي محرف من محارف السلسلة s2 أو تُعيد فراغًا إن لم يُعثر على أي تطابق. دالة strrchr: تُعيد مؤشرًا إلى آخر محرف ضمن s1 يطابق المحرف c آخذة بالحسبان المحرف الفارغ على أنه جزء من السلسلة s1 وتُعيد فراغ إن لم يُعثر على تطابق. دالة strspn: تُعيد طول الجزء الأولي ضمن السلسلة s1 الذي يتألف كاملًا من محارف السلسلة s1. دالة strstr: تُعيد مؤشرًا إلى أول تطابق للسلسلة s2 ضمن السلسلة s1 أو تُعيد فراغ إن لم يُعثر على تطابق. دالة strtok: تقسّم السلسلة النصية s1 إلى "رموز tokens" يُحدّد كل منها بمحرف من محارف السلسلة s2 وتُعيد مؤشرًا يشير إلى الرمز الأول أو فراغًا إن لم يوجد أي رموز. تُعيد استدعاءات لاحقة للدالة باستخدام ‎(char *)0 قيمةً للوسيط s1 الرمز التالي ضمن السلسلة، إلا أن s2 (المُحدِّد) قد يختلف عند كل استدعاء، ويُعاد مؤشر فارغ إذا لم يبق أي رمز. دوال متنوعة أخرى هناك بعض الدوال الأخرى التي لا تنتمي لأي من التصنيفات السابقة: void *memset(void *s, int c, size_t n); char *strerror(int errnum); size_t strlen(const char *s); دالة memset: تضبط n بايت يشير إليها المؤشر s إلى قيمة المحرف c وهو من النوع unsigned char، وتُعيد الدالة المؤشر s. دالة strlen: تُعيد طول السلسلة النصية s دون احتساب المحرف الفارغ في نهاية السلسلة، وهي دالة شائعة الاستخدام. دالة strerror: تُعيد مؤشرًا يشير إلى سلسلة نصية تصف الخطأ رقم errnum، وقد تُعدَّل هذه السلسلة النصية عن طريق استدعاءات لاحقة للدالة sterror، وتعدّ هذه الدالة مفيدةٌ لمعرفة معنى قيم errno. التاريخ والوقت تتعامل هذه الدوال إما مع الوقت المُنقضي elapsed time، أو وقت التقويم calendar time ويحتوي ملف الترويسة <time.h> على تصريح كلا النوعين من الدوال بالاعتماد على التالي: القيمة CLOCKS_PER_SEC: عدد الدقات ticks في الثانية المُعادة من الدالة clock. النوعين clock_t و time_t: أنواع حسابية تُستخدم لتمثيل تنسيقات مختلفة من الوقت. الهيكل struct tm: يُستخدم لتخزين القيمة المُمثّلة لأوقات التقويم، ويحتوي على الأعضاء التالية: int tm_sec // الثواني بعد الدقيقة من 0 إلى 61، وتسمح 61 بثانيتين كبيستين‫ leap-second int tm_min // الدقائق بعد الساعة من 0 إلى 59 int tm_hour // الساعات بعد منتصف الليل من 0 إلى 23 int tm_mday //اليوم في الشهر من 1 إلى 31 int tm_mon // الشهر في السنة من 0 إلى 11 int tm_year // السنة الحالية من 1900 int tm_wday // الأيام منذ يوم الأحد من 0 إلى 6 int tm_yday // الأيام منذ الأول من يناير من 0 إلى 365 int tm_isdst // مؤشر التوقيت الصيفي يكون العنصر tm_isdst موجبًا إذا كان التوقيت الصيفي daylight savings فعّالًا، وصفر إن لم يكن كذلك، وسالبًا إن لم تكن هذه المعلومة متوفرة. إليك دوال التلاعب بالوقت: #include <time.h> clock_t clock(void); double difftime(time_t time1, time_t time2); time_t mktime(struct tm *timeptr); time_t time(time_t *timer); char *asctime(const struct tm *timeptr); char *ctime(const time_t *timer); struct tm *gmtime(const time_t *timer); struct tm *localtime(const time_t *timer); size_t strftime(char *s, size_t maxsize, const char *format, const struct tm *timeptr); تتشارك كل من الدوال asctime و ctime و gmtime و localtime و strftime بهياكل بيانات ساكنة static من نوع struct tm أو من نوع char []‎، وقد يتسبب استدعاء أحد منها بعملية الكتابة فوق البيانات المخزنة بسبب استدعاء سابق لإحدى الدوال الأخرى، ولذلك يجب على مستخدم الدالة نسخ المعلومات إذا كان هذا سيسبب أية مشاكل. الدالة clock: تُعيد أفضل تقريب للوقت الذي انقضى منذ تشغيل البرنامج مقدرًا بدقات الساعة ticks، وتُعاد القيمة ‎(clock_t)-1 إذا لم يُعثر على أي قيمة. من الضروري العثور على الفرق بين وقت بداية تشغيل البرنامج والوقت الحالي إذا أردنا إيجاد الوقت المُنقضي اللازم لتشغيل البرنامج، وهناك ثابتٌ معرفٌ حسب التنفيذ يعدل على القيمة المُعادة من clock. يجب تقسيم القيمة على CLOCKS_PER_SEC لتحديد الوقت بالثواني. الدالة difftime: تُعيد الفرق بين وقت تقويم ووقت تقويم آخر بالثواني. الدالة mktime: تُعيد وقت تقويم يوافق القيم الموجودة في هيكل يشير إلى المؤشر timeptr، أو تُعيد القيمة ‎(time_t)-1 إذا لم يكن من الممكن تمثيل القيمة. يُتجاهل العضوان tm_wday و tm_yday، ولا تُقيَّد باقي الأعضاء بقيمهم الاعتيادية، إذ يُضبط أعضاء الهيكل إلى قيم مناسبة ضمن النطاق الاعتيادي عند التحويل الناجح، وهذه الدالة مفيدة للعثور على التاريخ والوقت الموافق لقيمة من نوع time_t. الدالة time: تُعيد أفضل تقريب لوقت التقويم الحالي باستخدام ترميز غير محدد unspecified encoding، وتُعيد القيمة ‎(time_t)-1 إذا كان الوقت غير متوفر. الدالة asctime: تُحول الوقت ضمن هيكل يشير إليه المؤشر timptr إلى سلسلة نصية بالتنسيق التالي: Sun Sep 16 01:03:52 1973\n\0 المثال السابق مأخوذٌ من المعيار، إذ يعرِّف المعيار الخوارزمية المستخدمة أيضًا، إلا أنه من المهم ملاحظة أن جميع الحقول ضمن السلسلة النصية ذات عرض ثابت وينطبق استخدامها على المجتمعات التي تتحدث باللغة الإنجليزية فقط. تُخزّن السلسلة النصية في هيكل ساكن static structure ويُمكن إعادة كتابته عن طريق استدعاءات لاحقة لأحد دوال التلاعب بالوقت (المذكورة أعلاه). الدالة ctime: تكافئ عمل asctime(localtime(timer))‎. اقرأ عن الدالة asctime لمعرفة القيمة المُعادة. الدالة gmtime: تُعيد مؤشرًا إلى struct tm، إذ يُضبط هذا المؤشر إلى وقت التقويم الذي يشير إليه المؤشر timer، ويُمثل الوقت بحسب شروط التوقيت العالمي المُنسّق Coordinated Universal Time -أو اختصارًا UTC-، أو المسمى سابقًا بتوقيت جرينتش Greenwich Mean Time، ونحصل على مؤشر فارغ إذا كان توقيت UTC غير مُتاح. الدالة localtime: تحوّل الوقت الذي يشير إليه المؤشر timer إلى التوقيت المحلي وتُخزن النتيجة في struct tm وتُعيد مؤشرًا يشير إلى ذلك الهيكل. الدالة strftime: تملأ مصفوفة المحارف التي يشير إليها المؤشر s بـمقدار maxsize محرف على الأكثر، وتُستخدم السلسلة النصية format لتنسيق الوقت المُمثّل في الهيكل الذي يشير إليه المؤشر timeptr، تُنسخ المحارف الموجودة في سلسلة التنسيق النصية (متضمنة المحرف الفارغ في نهاية السلسلة) دون أي تغيير إلى المصفوفة إلا إن كان وجِد توجيه تنسيق من التوجيهات التالية، فعندها تُسنخ القيمة المُحددة ضمن الجدول إلى المصفوفة الهدف بما يوافق الإعدادات المحلية. table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } ‎%a اسم يوم الأسبوع باختصار ‎%A اسم يوم الأسبوع كاملًا ‎%b اسم الشهر باختصار ‎%B اسم الشهر كاملًا ‎%c تمثيل التاريخ والوقت ‎%d تمثيل يوم الشهر عشريًا من 01 إلى 31 ‎%H الساعة من 00 إلى 23 (تنسيق 24 ساعة) ‎%I الساعة من 01 إلى 12 (تنسيق 12 ساعة) ‎%j يوم السنة من 001 إلى 366 ‎%m الشهر من 01 إلى 12 ‎%M الدقيقة من 00 إلى 59 ‎%p مكافئة PM أو AM المحلي ‎%S الثانية من 00 إلى 61 ‎%U ترتيب الأسبوع ضمن السنة من 00 إلى 53 (الأحد هو اليوم الأول) ‎%w يوم الأسبوع من 0 إلى 6 (الأحد مُمثّل بالرقم 0) ‎%W ترتيب الأسبوع ضمن السنة من 00 إلى 53 (الاثنين هو اليوم الأول) ‎%x تمثيل التاريخ محليًا ‎%X تمثيل الوقت محليًا ‎%y السنة دون سابقة القرن من 00 إلى 99 ‎%Y السنة مع سابقة القرن ‎%Z اسم المنطقة الزمنية، لا نحصل على محارف إن لم يكن هناك أي منطقة زمنية ‎%% محرف % يُعاد عدد المحارف الكلية المنسوخة إلى s* باستثناء محرف الفراغ في نهاية السلسلة، وتُعاد القيمة صفر إذا لم يكن هناك أي مساحة (بحسب قيمة maxsize) للمحرف الفارغ في النهاية. ترجمة -وبتصرف- لقسم من الفصل Libraries من كتاب The C Book. اقرأ أيضًا المقال التالي: تطبيقات عملية في لغة سي C المقال السابق: أدوات مكتبة stdlib في لغة سي C بنية برنامج لغة سي C الدوال في لغة C القيم الحدية والدوال الرياضية في لغة سي C
×
×
  • أضف...