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

كتابة برنامج سطر أوامر بلغة رست: إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء


Naser Dakhel

بدأنا في المقال السابق كتابة برنامج سطر أوامر 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.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...