بدأنا في المقال السابق كتابة برنامج سطر أوامر 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.