-
المساهمات
51 -
تاريخ الانضمام
-
تاريخ آخر زيارة
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو Naser Dakhel
-
كيف يمكننا الاختيار ما بين استدعاء الماكرو 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. اقرأ أيضًا المقال التالي: مقدمة إلى مفهوم الأنواع المعممة Generic Types في لغة Rust المقال السابق: الأخطاء والتعامل معها في لغة رست Rust التحكم بسير تنفيذ برامج راست Rust تخزين النصوص بترميز UTF-8 داخل السلاسل النصية في لغة رست Rust
-
لا مهرب من الأخطاء في دورة تطوير البرمجيات، لذا توفّر رست عددًا من المزايا للتعامل مع الحالات التي يحدث فيها شيء خاطئ، وتطلب رست منك في العديد من الحالات معرفتك باحتمالية حدوث الخطأ واتخاذ فعل ما قبل أن تُصرَّف 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. اقرأ أيضًا المقال التالي: الاختيار ما بين الماكرو panic! والنوع Result للتعامل مع الأخطاء في لغة Rust المقال السابق: كيفية استخدام النوع HashMap لتخزين البيانات في رست Rust أنواع البيانات Data Types في لغة رست Rust الحزم packages والوحدات المصرفة crates في لغة رست Rust الأخطاء السبع القاتلة لأيّ مشروع برمجيات
-
نستعرض في هذا المقال آخر التجميعات الشائعة في رست التي تحدثنا عنها سابقًا خلال رحلتنا في سلسلة البرمجة بلغة رست ألا وهي الخريطة المعمّاة 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. اقرأ أيضًا المقال التالي: الأخطاء والتعامل معها في لغة رست Rust المقال السابق: تخزين النصوص بترميز UTF-8 داخل السلاسل النصية في لغة رست Rust التحكم بسير تنفيذ برامج راست Rust تخزين لائحة من القيم باستخدام الأشعة Vectors في لغة رست Rust المسارات paths وشجرة الوحدة module tree في رست Rust
-
أتينا على ذكر السلاسل النصية سابقًا إلا أننا لم ننظر إليها بالتفصيل بعد، إذ يجد متعلمو لغة رست فهم السلاسل النصية صعبًا لثلاثة أسباب رئيسية: ميل رست لاستباق الخطأ قبل حدوثه وعرض رسالة خطأ تدلّ عليه، كما يبدو هيكل بيانات السلاسل النصية أكثر صعوبةً وتعقيدًا مما تبدو عليه، وأخيرًا ترميز 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 اقرأ أيضًا المقال التالي: كيفية استخدام النوع HashMap لتخزين البيانات في رست Rust المقال السابق: تخزين لائحة من القيم باستخدام الأشعة Vectors في لغة رست Rust التحكم بسير تنفيذ برامج راست Rust كيفية كتابة الدوال Functions والتعليقات Comments في لغة راست Rust المتغيرات والتعديل عليها في لغة رست
-
سننظر أولًا إلى نوع التجميعة 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. اقرأ أيضًا المقال التالي: تخزين النصوص بترميز UTF-8 داخل السلاسل النصية في لغة رست Rust المقال السابق: المسارات paths والنطاق الخاص بها في لغة رست Rust بنية match للتحكم بسير برامج لغة رست Rust التعدادات enums في لغة رست Rust التحكم بسير تنفيذ برامج راست Rust
-
قد تكون عملية كتابة مسارات استدعاء الدوال في بعض الأحيان عملية غير مريحة ورتيبة، كان علينا في الشيفرة 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. اقرأ أيضًا المقال التالي: تخزين لائحة من القيم باستخدام الأشعة Vectors في لغة رست Rust المقال السابق: المسارات paths وشجرة الوحدة module tree في رست Rust أنواع البيانات Data Types في لغة رست Rust الحزم packages والوحدات المصرفة crates في لغة رست Rust
-
يُستخدم المسار 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. اقرأ أيضًا المقال التالي: المسارات paths والنطاق الخاص بها في لغة رست Rust المقال السابق: الحزم packages والوحدات المصرفة crates في لغة رست Rust بنية match للتحكم بسير برامج لغة رست Rust الملكية Ownership في لغة رست
-
سنُغطّي أولى أجزاء نظام الوحدة ألا وهو الحزم 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. اقرأ أيضًا المقال التالي: المسارات paths وشجرة الوحدة module tree في رست Rust المقال السابق: بنية match للتحكم بسير برامج لغة رست Rust استخدام التوابع methods ضمن الهياكل structs في لغة رست Rust استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست
-
لدى رست بنية 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. اقرأ أيضًا المقال التالي: الحزم packages والوحدات المصرفة crates في لغة رست المقال السابق: التعدادات enums في لغة رست Rust الملكية Ownership في لغة رست أنواع البيانات Data Types في لغة رست Rust
-
سننظر في هذا المقال إلى التعدادات 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. اقرأ أيضًا المقال التالي: بنية match للتحكم بسير برامج لغة رست Rust المقال السابق: استخدام التوابع methods ضمن الهياكل structs في لغة رست Rust المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست التحكم بسير تنفيذ برامج راست Rust أنواع البيانات Data Types في لغة رست Rust
-
التوابع 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 اقرأ أيضًا المقال التالي: التعدادات enums في لغة رست Rust المقال السابق: استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust التحكم بسير تنفيذ برامج راست Rust أنواع البيانات Data Types في لغة رست Rust المتغيرات والتعديل عليها في لغة رست
-
بعد أن تكلمنا عن المكتبات القياسية المعرفة حسب المعيار، لم يبقَ إلا توضيح هيئة البرامج الكاملة المكتوبة بهذه اللغة، وسنتطرق في هذا المقال إلى بعض الأمثلة التي توضح كيفية جمع هذه العناصر لبناء البرامج. إلا أن هناك بعض النقاط التي يجب مناقشتها في لغة سي قبل عرض هذه الأمثلة. وسطاء الدالة 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
-
الهيكل 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. اقرأ أيضًا المقال التالي: استخدام التوابع methods ضمن الهياكل structs في لغة رست Rust المقال السابق: المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست الملكية Ownership في لغة رست التحكم بسير تنفيذ برامج راست Rust أنواع البيانات Data Types في لغة رست Rust
-
نتطرّق في هذا المقال إلى طرق مختلفة في التعامل مع السلاسل النصية والتلاعب بها، وذلك عن طريق دوال مكتبة 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
-
نستعرض هنا الأدوات الموجودة في ملف الترويسة <stdlib.h> الذي يصرح عن عدد من الأنواع والماكرو وعدة دوال للاستخدام العام، تتضمن الأنواع والماكرو التالي: النوع size_t: تكلمنا عنه سابقًا. النوع div_t: نوع من الهياكل التي تعيدها الدالة div. النوع ldiv_t: نوع من الهياكل التي تعيدها الدالة ldiv. النوع NULL: تكلمنا عنه سابقًا. القيمة EXIT_FAILURE و EXIT_SUCCESS: يمكن استخدامهما مثل وسيط للدالة exit. القيمة MB_CUR_MAX: العدد الأعظمي للبايتات في محرف متعدد البايتات multibyte character من مجموعة المحارف الإضافية والمحددة حسب إعدادت اللغة المحلية locale. القيمة RAND_MAX: القيمة العظمى المُعادة من استدعاء دالة rand. دوال تحويل السلسلة النصية هناك ثلاث دوال تقبل سلسلة نصية وسيطًا لها وتحوّلها إلى عدد من نوع معين كما هو موضح هنا: #include <stdlib.h> double atof(const char *nptr); long atol(const char *nptr); int atoi(const char *nptr); نحصل على عدد مُحوّل مُعاد لكل من الدوال الثلاث السابقة، ولا تضمن لك أيٌ من الدوال أن تضبط القيمة errno (إلا أن الأمر محقّق في بعض التنفيذات)، وتكون النتائج التي نحصل عليها من تحويلات تتسبب بحدوث طفحان overflow ولا يمكننا تمثيلها غير معرّفة. هناك بعض الدوال أكثر تعقيدًا: #include <stdlib.h> double strtod(const char *nptr, char **endptr); long strtol(const char *nptr, char **endptr, int base); unsigned long strtoul(const char *nptr,char **endptr, int base); تعمل الدوال الثلاث السابقة بطريقة مشابهة، إذ يجري تجاهل أي مسافات فارغة بادئة ومن ثم يُعثر على الثابت المناسب subject sequence متبوعًا بسلسلة محارف غير معترف عليها، ويكون المحرف الفارغ في نهاية السلسلة النصية غير مُعترف عليه دائمًا. تُحدد السلسلة المذكورة حسب التالي: في الدالة strtod: إشارة "+" أو "-" اختيارية في البداية متبوعة بسلسلة أرقام تحتوي على محرف الفاصلة العشرية الاختياري وأس exponent اختياري أيضًا. لا يُعترف على أي لاحقة عائمة (ما بعد الفاصلة العشرية)، وتُعدّ الفاصلة العشرية تابعةً لسلسلة الأرقام إذا وُجدت. في الدالة strtol: إشارة "+" أو "-" اختيارية في البداية متبوعة بسلسلة أرقام، وتُؤخذ هذه الأرقام من الخانات العشرية أو من أحرف صغيرة lower case أو كبيرة upper case ضمن النطاق a إلى z في الأبجدية الإنجليزية ويُعطى لكل من هذه الأحرف القيم ضمن النطاق 10 إلى 35 بالترتيب. يحدِّد الوسيط base القيم المسموحة ويمكن أن تكون قيمة الوسيط صفر أو ضمن النطاق 2 إلى 36. يجري التعرُّف على الخانات التي تبلغ قيمتها أقل من الوسيط base، إذ تسمح القيمة 16 للوسيط base على سبيل المثال للمحارف 0x أو 0X أن تتبع الإشارة الاختيارية، بينما يسمح base بقيمة صفر أن تكون المحارف المدخلة على هيئة أعداد صحيحة ثابتة في سي، ولا يُعترف على أي لاحقة عدد صحيح. في الدالة strtoul: مطابقة للدالة strtol إلا أنها لا تسمح بوجود إشارة. يُخزّن عنوان أول محرف غير مُعترف عليه في الكائن الذي يشير إليه endptr في حال لم يكُن فارغًا، وتكون هذه قيمة nptr إذا كانت السلسلة فارغة أو ذات تنسيق خاطئ. تحوّل الدالة الرقم وتُعيده مع الأخذ بالحسبان كون وجود الإشارة البادئة مسموحًا أو لا، وذلك إذا كان إجراء التحويل ممكنًا، وإلا فإنها تعيد القيمة صفر. عند حدوث الطفحان أو حصول خطأ تُجرى العمليات التالية: في الدالة strtod: تُعيد عند الطفحان القيمة HUGE_VAL± وتعتمد الإشارة على إشارة النتيجة، وتُعيد القيمة صفر عند طفحان الحد الأدنى underflow ويُضبط errno في الحالتين إلى القيمة ERANGE. في الدالة strtol: تُعيد القيمة LONG_MAX أو LONG_MIN عند الطفحان بحسب إشارة النتيحة، ويُضبط errno في كلا الحالتين إلى القيمة ERANGE. في الدالة strtoul: تُعيد القيمة ULONG_MAX عند الطفحان، ويُضبط errno إلى القيمة ERANGE. قد يكون هناك سلاسل أخرى من الممكن التعرُّف عليها في بعض التنفيذات إذا لم تكن الإعدادات المحلية هي إعدادات سي التقليدية. توليد الأرقام العشوائية تقدِّم الدوال التالية طريقةً لتوليد الأرقام العشوائية الزائفة pseudo-random: #include <stdlib.h> int rand(void); void srand(unsigned int seed); تُعيد الدالة rand رقمًا عشوائيًا زائفًا ضمن النطاق من "0" إلى "RAND_MAX"، وهو ثابت قيمته على الأقل "32767". تسمح الدالة srand بتحديد نقطة بداية للنطاق المُختار طبقًا لقيمة الوسيط seed، وهي ذات قيمة "1" افتراضيًا إذا لم تُستدعى srand قبل rand، ونحصل على سلسلة قيم مطابقة من الدالة rand إذا استخدمنا قيمة seed ذاتها. يصف المعيار الخوارزمية المستخدمة في دالتي rand و srand، وتستخدم معظم التنفيذات هذه الخوارزمية عادةً. حجز المساحة تُستخدم هذه الدوال لحجز وتحرير المساحة، إذ يُضمن للمساحة التي حصلنا عليها أن تكون كبيرةً كفاية لتخزين كائن من نوع معين ومُحاذاة ضمن الذاكرة بحيث لا تتسبب بتفعيل استثناءات العنوان addressing exceptions، ولا يجب افتراض أي شيء آخر بخصوص عملها. #include <stdlib.h> void *malloc(size_t size); void *calloc(size_t nmemb, size_t size); void *realloc(void *ptr, size_t size); void *free(void *ptr); تُعيد جميع دوال حجز المساحة مؤشرًا يشير إلى المساحة المحجوزة التي يبلغ حجمها size بايت، وإذا لم يكن هناك أي مساحة فارغة فهي تعيد مؤشرًا فارغًا، إلا أن الفرق بين الدوال هو أن دالة calloc تأخذ وسيطًا يدعى nmemb الذي يحدد عدد العناصر في مصفوفة، وكل عنصر في هذه المصفوفة يبلغ حجمه size بايت، ولذا فهي تحجز مساحةً أكبر من التخزين عادةً من المساحة التي تحجزها malloc، كما أن المساحة المحجوز بواسطة malloc غير مُهيّئة بينما تُهيّأ جميع بتات مساحة التخزين المحجوزة بواسطة calloc إلى الصفر، إلا أن هذا الصفر ليس تمثيلًا مكافئًا للصفر ضمن الفاصلة العائمة floating-point أو مؤشرًا فارغًا null بالضرورة. تُستخدم الدالة realloc لتغيير حجم الشيء المُشار إليه بواسطة المؤشر ptr مما يتطلب بعض النسخ لإنجاز هذا الأمر ومن ثمّ تُحرّر مساحة التخزين القديمة. لا تُغيّر محتويات الكائن المُشار إليه بواسطة ptr بالنسبة للحجمين القديم والجديد، وتتصرف الدالة تصرفًا مماثلًا لتصرف malloc إذا كان المؤشر ptr مؤشرًا فارغًا وذلك للحجم المخصّص. تُستخدم الدالة free لتحرير المساحة المحجوزة مسبقًا من إحدى دوال حجز المساحة، ومن المسموح تمرير مؤشر فارغ إلى الدالة free وسيطًا لها، إلا أن الدالة في هذه الحالة لا تنفّذ أي شيء. إذا حاولت تحرير مساحة لم تُحجز مسبقًا تحصل على سلوك غير محدّد، ويتسبب هذا الأمر في العديد من التنفيذات باستثناء عنوان addressing exception مما يوقف البرنامج، إلا أن هذه ليست بدلالة يمكن الاعتماد عليها. التواصل مع البيئة سنستعرض مجموعةً من الدوال المتنوعة: #include <stdlib.h> void abort(void); int atexit(void (*func)(void)); void exit(int status); char *getenv(const char *name); int system(const char *string); دالة abort: تتسبب بإيقاف غير اعتيادي للبرنامج وذلك باستخدام الإشارة SIGABRT، ويمكن منع الإيقاف غير الاعتيادي فقط إذا حصلنا على الإشارة ولم يُعد معالج الإشارة signal handler أي قيمة، وإلا ستُحذف ملفات الخرج وقد يمكن أيضًا إزالة الملفات المؤقتة بحسب تعريف التنفيذ، وتُعاد حالة "إنهاء برنامج غير ناجح unsuccessful termination" إلى البيئة المُستضافة، كما أن هذه الدالة لا يمكن أن تُعيد أي قيمة. دالة atexit: يصبح وسيط الدالة func دالةً يمكن استدعاؤها دون استخدام أي وسطاء عندما يُغلق البرنامج، ويمكن استخدام ما لا يقل عن 32 دالة مشابهة لهذه الدالة وأن تُستدعى عند إغلاق البرنامج بصورةٍ مُعاكسة لتسجيل كلٍّ منها، ونحصل على القيمة المُعادة صفر للدلالة على النجاح وإلا فقيمة غير صفرية للدلالة على الفشل. دالة exit: تُستدعى هذه الدالة عادةً لإنهاء البرنامج على نحوٍ اعتيادي، وتُستدعى عند تنفيذ الدالة جميع الدوال المُسجّلة باستخدام دالة atexit، لكن انتبه، إذ ستُعد الدالة main بحلول هذه النقطة قد أعادت قيمتها ولا يمكن استخدام أي كائنات ذات مدة تخزين تلقائي automatic storage duration على نحوٍ آمن، من ثمّ تُحذف محتويات جميع مجاري الخرج output streams وتُغلق وتُزال جميع الملفات التلقائية المُنشأة بواسطة tmpfile، وأخيرًا يُعيد البرنامج التحكم إلى البيئة المستضافة بإعادة حالة نجاح أو فشل محدّدة بحسب التنفيذ، وتعتمد الحالة على إذا ما كان وسيط الدالة exit مساوٍ للقيمة EXITSUCCESS (وهذه حالة النجاح) أو EXITFAILURE (حالة الفشل). للتوافق مع لغة سي القديمة، تُستخدم القيمة صفر بدلًا من EXITFAILURE، بينما يكون لباقي القيم تأثيرات معرّفة بحسب التنفيذ. لا يمكن أن تُعاد حالة الخروج. دالة getenv: يجري البحث في لائحة البيئة environment list المعرفة بحسب التنفيذ بهدف العثور على عنصر يوافق السلسلة النصية المُشار إليها بواسطة وسيط الاسم name، إذ تُعيد الدالة مؤشرًا إلى العنصر يشير إلى مصفوفة لا يمكن تعديلها من قبل البرنامج ويمكن تعديلها باستدعاء لاحق للدالة getenv، ونحصل على مؤشر فارغ إذا لم يُعثر على عنصر موافق. يعتمد الهدف من لائحة البيئة وتنفيذها على البيئة المُستضافة. دالة system: تُمرّر سلسلة نصية إلى أمر معالج مُعرف حسب التنفيذ، ويتسبب مؤشر فارغ بإعادة القيمة صفر، وقيمة غير صفرية إذا لم يكن الأمر موجودًا، بينما يتسبب مؤشر غير فارغ بمعالجة الأمر. نتيجة الأمر والقيمة المُعادة معرف حسب التنفيذ. البحث والترتيب هناك دالتان ضمن هذا التصنيف، أولهما دالة للبحث ضمن لائحة مُرتّبة والأخرى لترتيب لائحة غير مرتبة، واستخدام الدالتان عام، إذ يمكن استخدامهما في مصفوفات من أي سعة وعناصرها من أي حجم. يجب أن يلجأ المستخدم إلى دالة مقارنة إذا أراد مقارنة عنصرين عند استخدام الدوال السابقة، إذ تُستدعى هذه الدالة باستخدام المؤشرين الذين يشيران إلى العنصرين مثل وسطاء الدالة، وتُعيد الدالة قيمةً أقل من الصفر إذا كانت قيمة المؤشر الأول أصغر من قيمة المؤشر الثاني، وقيمة أكبر من الصفر إذا كانت قيمة المؤشر الأول أكبر من المؤشر الثاني، والقيمة صفر إذا كانت قيمتا المؤشرين متساويين. #include <stdlib.h> void *bsearch(const void *key, const void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); void *qsort(const void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); يمثّل الوسيط nmemb في كلٍّ من الدالتين السابقتين عدد العناصر في المصفوفة، ويمثل الوسيط size حجم عنصر المصفوفة الواحد بالبايت و compar هي الدالة التي تُستدعى للمقارنة بين العنصرين، بينما يشير المؤشر base إلى أساس المصفوفة أي بدايتها. ترتّب الدالة qsort المصفوفة ترتيبًا تصاعديًا. تفترض الدالة bsearch أن المصفوفة مرتّبة مسبقًا وتُعيد مؤشرًا إلى أي عنصر يتساوى مع العنصر المُشار إليه بالمؤشر key، وتُعيد الدالة مؤشرًا فارغًا إذا لم تجد أي عناصر متساوية. دوال العمليات الحسابية الصحيحة تقدّم هذه الدوال طريقةً لإيجاد القيمة المطلقة لوسيط يمثل عدد صحيح، إضافةً لحاصل القسمة والباقي من العملية لكلٍّ من النوعين int و long. #include <stdlib.h> int abs(int j); long labs(long j); div_t div(int numerator, int denominator); ldiv_t ldiv(long numerator, long denominator); الدالتان abs و labs: تُعيدان القيمة المطلقة لوسيطهما ويجب اختيار الدالة المناسبة بحسب احتياجاتك. نحصل على سلوك غير معرّف إذا لم تكن القيمة ممكنة التمثيل وقد يحدث ذلك في الأنظمة التي تعمل بنظام المتمم الثنائي two's complement systems، إذ لا يوجد لأكثر رقم سلبيّة أي مكافئ إيجابي. الدالتان div وldiv: تُقسّمان الوسيط numerator على الوسيط denominator وتُعيدان هيكلًا structure للنوع المحدد، وفي أي حالة، سيحتوي الهيكل على عضو يدعى quot يحتوي على حاصل القسمة الصحيحة وعضوًا آخر يدعى rem يحتوي على باقي القسمة، ونوع العضوين هو int في الدالة div و long في الدالة ldiv، ويمكن تمثيل نتيجة العملية على النحو التالي: quot*denominator+rem == numerator الدوال التي تستخدم المحارف متعددة البايت يؤثر تصنيف LC_CTYPE ضمن الإعدادات المحلية الحالية على سلوك هذه الدوال، إذ تُضبط كل دالة إلى حالة ابتدائية باستدعاء يكون وسيطها s الذي يمثل مؤشر المحرف فارغًا null، وذلك في حالة الترميز المُعتمد على الحالة state-dependent endcoding، وتُغيَّر حالة الدالة الداخلية وفق الضرورة عن طريق استدعاءات لاحقة عندما لا يكون s مؤشرًا فارغًا. تُعيد الدالة قيمةً غير صفرية إذا كان الترميز معتمدًا على الحالة، وإلا فتعيد القيمة صفر إذا كان المؤشر s فارغًا. تصبح حالة الإزاحة shift state الخاصة بالدوال غير محددة indeterminate إذا حدث تغيير للتصنيف LC_TYPE. الدوال هي: #include <stdlib.h> int mblen(const char *s, size_t n); int mbtowc(wchar_t *pwc, const char *s, size_t n); int wctomb(char *s, wchar_t wchar); size_t mbstowcs(wchar_t *pwcs, const char *s, size_t n); size_t wcstombs(char *s, const wchar_t *pwcs, size_t n); الدالة mblen: تُعيد عدد البايتات المُحتواة بداخل محرف متعدد البايتات multibyte character المُشار إليه بواسطة المؤشر s أو تُعيد القيمة -1 إذا كان أول n بايت لا يشكّل محرف متعدد البايتات صالحًا، أو تُعيد القيمة صفر إذا كان المؤشر يشير إلى محرف فارغ. الدالة mbtowc: تُحوِّل محرف متعدد البايتات يُشير إليه المؤشر s إلى الرمز الموافق له من النوع wchar_t وتُخزّن النتيجة في الكائن المُشار إليه بالمؤشر pwc، إلا إن كان pwc مؤشرًا فارغًا، وتُعيد الدالة عدد البايتات المُحوّلة بنجاح، أو -1 إذا لم تشكّل أول n بايت محرفًا متعدد البايت صالحًا، ولا يُفحص أكثر من n بايت يُشير إليه المؤشر s، ولن تتعدى القيمة المُعادة قيمة n أو MB_CUR_MAX. دالة wctmob: تُحوِّل رمز القيمة wchar إلى سلسلة من البايتات تمثل محرف متعدد البايتات وتخزن النتيجة في مصفوفة يشير إليها المؤشر s وذلك إن لم يكن s مؤشرًا فارغًا، وتُعيد الدالة عدد البايتات المحتواة داخل محرف متعدد البايتات، أو -1 إذا كانت القيمة المخزنة في wchar لا تمثل محرف متعدد البايات، ومن غير الممكن معالجة عدد بايتات يتجاوز MB_CUR_MAX. دالة mbstowcs: تحوّل سلسلة محارف متعددة البايتات بدءًا من الحالة الأولية للإزاحة initial shift state وذلك ضمن المصفوفة التي يشير إليها المؤشر s إلى سلسلة من الرموز الموافقة ومن ثم تخزّنها في مصفوفة يشير إليها المؤشر pwcs، لا يُخزّن ما يزيد عن n قيمة في pwcs، وتُعيد الدالة 1- إذا صادفت محرفًا متعدد البايت غير صالح، وإلا فإنها تعيد عدد عناصر المصفوفة المُعدّلة باستثناء رمز إنهاء المصفوفة. نحصل على سلوك غير معرّف إذا وجد كائنين متقاطعين. الدالة wcstombs: تُحوِّل سلسلة من الرموز المُشار إليها بالمؤشر pwcs إلى سلسلة من المحارف متعددة البايتات بدءًا من الحالة الأولية للإزاحة وتُخزّن فيما بعد في مصفوفة مُشار إليها بالمؤشر s. تتوقف عملية التحويل عند مصادفة رمز فارغ، أو عند كتابة n بايت إلى s، وتُعيد الدالة -1 إذا كان الرمز المُصادف لا يمثل محرفًا متعدد البايتات صالحًا، وإلا فيُعاد عدد البايتات التي كُتبت باستثناء رمز الإنهاء الفارغ. نحصل على سلوك غير محدد إذا وجد كائنين متقاطعين. ترجمة -وبتصرف- لقسم من الفصل Libraries من كتاب The C Book. اقرأ أيضًا المقال التالي: دوال التعامل مع السلاسل النصية والوقت والتاريخ في لغة سي C المقال السابق: التعامل مع الدخل والخرج I/O وتنسيقه في لغة سي C التعامل مع المكتبات في لغة سي C التعامل مع المحارف وضبط إعدادات التوطين localization في لغة سي C
-
تكلّمنا سابقًا عن الدخل والخرج في لغة سي بالاستعانة بالمكتبات القياسية، وحان الوقت لنتعلم الآن مختلف الدوال الموجودة في هذه المكتبات التي تضمن لنا أساليب مختلفة في القراءة والكتابة. الدخل والخرج المنسق هناك عدد من الدوال المُستخدمة لتنسيق الدخل والخرج، وتحدد كلًا من هذه الدوال التنسيق المتبع للدخل والخرج باستخدام سلسلة التنسيق النصية format string، وتتألف سلسلة التنسيق النصية في حالة الخرج من نص عادي plain text يمثّل الخرج كما هو، إضافةً إلى مواصفات التنسيق format specifications التي تتطلب معالجة خاصة لواحد من الوسطاء المتبقية في الدالة، بينما تتألف سلسلة التنسيق النصية في حالة الخرج من نص عادي يطابق مجرى الدخل وتحدد هنا مواصفات التنسيق معنى الوسطاء المتبقية. يُشار إلى كل واحدة من مواصفات التنسيق باستخدام المحرف "%" متبوعًا ببقية التوصيف. الخرج: دوال printf تتخذ مواصفات التنسيق في دوال الخرج الشكل التالي، ونشير إلى الأجزاء الاختيارية بوضعها بين قوسين: %<flags><field width><precision><length>conversion نشرح معنى كل من الراية flag وعرض الحقل field width والدقة precision والطول length والتحويل conversion أدناه، إلا أنه من الأفضل النظر إلى وصف المعيار إذا أردت وصفًا مطولًا ودقيقًا. الرايات يمكن ألا تأخذ الرايات أي قيمة أو أن تأخذ أحد القيم التالية: 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; } قيمة الراية الشرح - ملئ سطر التحويل من اليسار ضمن حقوله. + يبدأ التحويل ذو الإشارة بإشارة زائد أو ناقص دائمًا. مسافة فارغة space إذا كان المحرف الأول من تحويل ذو إشارة ليس بإشارة، أضف مسافةً فارغة، ويمكن تجاوز الراية باستخدام "+" إذا وجدت. # يُجبر استخدام تنسيق مختلف للخرج، مثل: الخانة الأولى لأي تحويل ثماني لها القيمة "0"، وإضافة "0x" أمام أي تحويل ست عشري لا تساوي قيمته الصفر، ويُجبِر الفاصلة العشرية في جميع تحويلات الفاصلة العائمة حتى إن لم تكن ضرورية، ولا يُزيل أي صفر لاحق من تحويلات g و G. 0 يُضيف إلى تحويلات d و i و o و u و x و X و e و E و f و F و G أصفارًا إلى يسارها بملئ عرض الحقل، ويمكن تجاوزه باستخدام الراية "-"، وتُتجاهل الراية إذا كان هناك أي دقة محددة لتحويلات d، أو i، أو o، أو u، أو x، أو X، ونحصل على سلوك غير معرف للتعريفات الأخرى. عرض الحقل field width عدد صحيح عشري يحدد عرض حقل الخرج الأدنى ويمكن تجاوزه إن لزم الأمر، يُحوّل الوسيط التالي إلى عدد صحيح ويُستخدم مثل قيمة لعرض الحقل إن استُخدمت علامة النجمة "*"، وتُعامل هذه القيمة إذا كانت سالبة كأنها راية "-" متبوعة بعرض حقل ذي قيمة موجبة. يُملأ الخرج ذو الطول الأقصر من عرض الحقل بالمسافات الفارغة (أو بأصفار إذا كان العدد الصحيح المعبر عن عرض الحقل يبدأ بالصفر)، ويُملأ الخرج من الجهة اليسرى إلا إذا حُدّدَت راية تعديل اليسار left-adjustment. الدقة precision تبدأ قيمة الدقة بالنقطة '.'، وهي تحدد عدد الخانات الدنيا لتحويلات d، أو i، أو o، أو u، أو x، أو X، أو عدد الخانات التي تلي الفاصلة العشرية في تحويلات e، أو E، أو f، أو العدد الأعظمي لخانات تحويلات g وG، أو عدد المحارف المطبوعة من سلسلة نصية في تحويلات s. يتسبب تحديد كمية حشو الحقل padding بتجاهل قيمة field width. يُحوَّل الوسيط التالي في حال استخدامنا لعلامة النجمة "*" إلى عدد صحيح ويُستخدم بمثابة قيمة لعرض الحقل، وتعامل القيمة كأنها مفقودة إذا كانت سالبة، وتكون الدقة صفر إذا وجدت النقطة فقط. الطول length وهي h تسبق محدد specifier لطباعة نوع عدد صحيح integral ويتسبب ذلك في معاملتها وكأنها من النوع "short" (لاحظ أن الأنواع المختلفة القصيرة shorts تُرقّى إلى واحدة من أنواع القيم الصحيحة int عندما تُمرّر مثل وسيط). تعمل l مثل عمل h إلا أنها تُطبّق على وسيط عدد صحيح من نوع "long"، وتُستخدم L للدلالة على أنه يجب طباعة وسيط من نوع "long double"، ويطبَّق ذلك فقط على محددات الفاصلة العائمة. يتسبب استخدام هذا في سلوك غير معرف إذا كانت باستخدام النوع الخاطئ من التحويلات. يوضح الجدول التالي أنواع التحويلات: المحدد التأثير الدقة الافتراضية d عدد عشري ذو إشارة 1 i عدد عشري ذو إشارة 1 u عدد عشري عديم الإشارة 1 o عدد ثماني عديم الإشارة 1 x عدد ست عشري عديم الإشارة من 0 إلى f 1 X عدد ست عشري عديم الإشارة من 0 إلى F 1 تحدد الدقة عدد خانات الأدنى المُستبدل بأصفار إن لزم الأمر، ونحصل على خرج دون أي محارف عند استخدام الدقة صفر لطباعة القيمة صفر f يطبع قيمة من النوع double بعدد خانات الدقة (المقربة) بعد الفاصلة العشرية. استخدم دقة بقيمة صفر للحد من الفاصلة العشرية، وإلا فستظهر خانة واحدة على الأقل بعد الفاصلة العشرية 6 e, E يطبع قيمة من نوع double بالتنسيق الأسي مُقرّبًا بخانة واحدة قبل الفاصلة العشرية، وعدد من الخانات يبلغ الدقة المحددة بعده، وتُلغى الفاصلة العشرية عند استخدام الدقة صفر، وللأس خانتان على الأقل تطبع بالشكل 1.23e15 في تنسيق e أو 1.23E15 في حالة التنسيق E 6 g, G تستخدم أسلوب التنسيق f، أو e (E مع G) بحسب الأس، ولا يُستخدم التنسيق f إذا كان الأس أصغر من "-4" أو أكبر أو يساوي الدقة. تُحدّ الأصفار التي تتبع القيمة وتُطبع الفاصلة العشرية فقط في حال وجود خانات تابعة. غير محدد c يُحوّل الوسيط من نوع عدد صحيح إلى محرف عديم الإشارة ويُطبع المحرف الناتج عن التحويل s تُطبع سلسلة نصية بطول خانات الدقة، ويجب إنهاء السلسلة النصية باستخدام NUL إذا لم تُحدّد الدقة أو كانت أكبر من طول السلسلة النصية لا نهائي p إظهار قيمة مؤشر من نوع (void *) بطريقة تعتمد على النظام n يجب أن يكون الوسيط مؤشرًا يشير إلى عدد صحيح، ويكون عدد محارف الخرج باستخدام هذا الاستدعاء مُسندًا إلى العدد الصحيح % علامة "%" _ [جدول 1 التحويلات] تجد وصف الدوال التي تستخدم هذه التنسيقات في الجدول التالي، وجميع الدوال المُستخدمة مُضمّنة في المكتبة <stdio.h>، إليك تصاريح هذه الدوال: #include <stdio.h> int fprintf(FILE *stream, const char *format, ...); int printf(const char *format, ...); int sprintf(char *s, const char *format, ...); #include <stdarg.h> // بالإضافة إلى stdio.h int vfprintf(FILE *stream, const char *format, va list arg); int vprintf(const char *format, va list arg); int vsprintf(char *s, const char *format, va list arg); الاسم الاستخدام fprintf نحصل على الخرج المنسق العام بواسطتها كما وصفنا سابقًا، ويكتب الخرج إلى الملف المُحدد باستخدام المجرى stream printf دالة مُطابقة لعمل الدالة fprintf إلا أن وسيطها الأول هو stdout sprintf دالة مُطابقة لعمل الدالة fprintf باستثناء أن خرجها لا يُكتب إلى ملف، بل يُكتب إلى مصفوفة محارف يُشار إليها باستخدام المؤشر s vfprintf خرج مُنسَّق مشابه لخرج الدالة fprintf إلا أن لائحة الوسطاء المتغيرة تُستبدل بالوسيط arg الذي يجب أن يُهيَّأ باستخدام va_start، ولا تُستدعى va_end باستخدام هذه الدالة vprintf مطابقة للدالة vfprintf باستثناء أن الوسيط الأول يساوي إلى stdout vsprintf خرج مُنسَّق مشابه لخرج الدالة sprintf إلا أن لائحة الوسطاء المتغيرة تُستبدل بالوسيط arg الذي يجب أن يُهيَّأ باستخدام va_start، ولا تُستدعى va_end باستخدام هذه الدالة [جدول 2 الدوال التي تطبع خرجًا مُنسَّقًا] تُعيد كل الدوال السابقة عدد المحارف المطبوعة أو قيمة سالبة للدلالة على حصول خطأ، ولا يُحسب المحرف الفارغ الزائد بواسطة دالة sprintf و vsprintf. يجب أن تسمح التنفيذات بالحصول على 509 محارف على الأقل عند استخدام أي تحويل. الدخل: دوال scanf هناك عدة دوال مشابهة لمجموعة دوال printf بهدف الحصول على الدخل، والفارق الواضح بين مجموعتي الدوال هذه هو أن مجموعة دوال scanf تأخذ وسيطًا متمثلًا بمؤشر حتى تُسند القيم المقروءة إلى وجهتها المناسبة، ويُعد نسيان تمرير المؤشر خطأ شائع الحدوث ولا يمكن للمصرّف أن يكتشف حدوثه، إذ يمنع استخدام لائحة وسطاء متغيرة من ذلك. تُستخدم سلسلة التنسيق النصية للتحكم بتفسير المجرى للبيانات المُدخلة التي تحتوي غالبًا على قيم تُسند إلى كائنات يُشار إليها باستخدام وسطاء دالة scanf المتبقية، وقد تتألف سلسلة التنسيق النصية من: مساحة فارغة white space: تتسبب بقراءة مجرى الدخل إلى المحرف التالي الذي لا يمثّل محرف مسافة فارغة. محرف اعتيادي ordinary character: ويمثل المحرف أي محرف عدا محارف السلسلة الفارغة أو "%"، ويجب أن يطابق المحرف التالي في مجرى الدخل هذا المحرف المُحدّد. توصيف التحويل conversion specification: وهو محرف "%" متبوع بمحرف "*" اختياري (الذي يكبح التحويل)، ويُتبع بعدد عشري صحيح لا يساوي الصفر يحدد عرض الحقل الأعظمي، ومحرف "h"، أو "l"، أو "L" اختياري للتحكم بطول التحويل، وأخيرًا محدد تحويل إجباري. لاحظ أن استخدام "h"، أو "l"، أو "L" سيؤثر على على نوع المؤشر الواجب استخدامه. حقل الدخل -باستثناء المحددات "c" و "n" و "]"- هو سلسلة من المحارف التي لا تمثل مسافة فارغة وتبدأ من أول محرف في الدخل (بشرط ألا يكون المحرف مسافة فارغة)، وتُنهى السلسلة عند أول محرف متعارض أو عند الوصول إلى عرض الحقل المُحدّد. تُسند النتيجة إلى الشيء الذي يُشير إليه الوسيط إلا إذا كان الإسناد مكبوحًا باستخدام "*" المذكورة سابقًا، ويمكن استخدام محددات التحويل التالية: المحددات d i o u x: تُحوِّل d عدد صحيح ذو إشارة، وتحوّل i عدد صحيح ذو إشارة وتنسيق ملائم لـstrtol، وتحوِّل o عدد صحيح ثماني، وتحوّل u عدد صحيح عديم الإشارة، وتحول x عدد صحيح ست عشري. المحددات e f g: تحوِّل قيمة من نوع float (وليس double). المحدد s: يقرأ سلسلة نصية ويُضيف محرف فارغ في نهايته، وتُنهى السلسلة النصية باستخدام مسافة فارغة عند الدخل (ولا تُقرأ هذه المسافة الفارغة على أنها جزء من الدخل). المحدد ]: يقرأ سلسلة نصية، وتتبع ] لائحة من المحارف تُدعى مجموعة المسح scan set، ويُنهي المحرف [ هذه اللائحة. تُقرأ المحارف إلى (غير متضمنةً) المحرف الأول غير الموجود ضمن مجموعة المسح؛ فإذا كان المحرف الأول في اللائحة هو "^" فهذا يعني أن مجموعة القراءة تحتوي على أي محرف غير موجود في هذه القائمة، وإذا كانت السلسلة الأولية هي "[^]" أو "[]" فهذا يعني أن [ ليس محدّدًا بل جزءًا من السلسلة ويجب إضافة محرف "[" آخر لإنهاء اللائحة. إذا وجدت علامة ناقص "-" في اللائحة، يجب أن يكون موقعها المحرف الأول أو الأخير، وإلا فإن معناها معرف بحسب التنفيذ. المحدد c: يقرأ محرفًا واحدًا متضمنًا محارف المسافات الفارغة، ولقراءة المحرف الأول باستثناء محارف المسافات الفارغة استخدم %1s، ويحدد عرض الحقل مصفوفة المحارف التي يجب قراءتها. المحدد p: يقرأ مؤشرًا من النوع (void *) والمكتوب سابقًا باستخدام المحدد %p ضمن استدعاء سابق لمجموعة دوال printf. المحدد %: المحرف "%" متوقّع في الدخل ولا يُجرى أي إسناد. المحدد n: يُعاد عددًا صحيح يمثل عدد المحارف المقروءة باستخدام هذا الاستدعاء. يوضح الجدول التالي تأثير محددات الحجم size specifiers: المحدد يُحدِّد يُحوِّل l d i o u x عدد صحيح كبير long int h d i o u x عدد صحيح صغير short int l e f عدد عشري مضاعف double L e f عدد عشري مضاعف كبير long double [جدول 3 محددات الحجم] إليك وصف دوال مجموعة scanf مع تصاريحها: #include <stdio.h> int fscanf(FILE *stream, const char *format, ...); int sscanf(const char *s, const char *format, ...); int scanf(const char *format, ...); تأخذ الدالة fscanf دخلها من المجرى المُحدد، وتُطابق الدالة scanf الدالة fscanf مع اختلاف أن الوسيط الأول هو المجرى stdin، بينما تأخذ sscanf دخلها من مصفوفة محارف مُحدّدة. نحصل على القيمة EOF المُعادة في حال حدوث خطأ دخل قبل أي تحويلات، وإلا فنحصل على عدد التحويلات الناجحة الحاصلة وقد يكون هذا العدد صفر إن لم تُجرى أي تحويلات، ونحصل على خطأ دخل إذا قرأنا EOF، أو بوصولنا إلى نهاية سلسلة الدخل النصية، بينما نحصل على خطأ تحويل إذا فشل العثور على نمط مناسب يوافق التحويل المحدّد. عمليات الإدخال والإخراج على المحارف هناك عدد من الدوال التي تسمح لنا بإجراء عمليات الدخل والخرج على المحارف بصورةٍ منفردة، إليك تصاريحها: #include <stdio.h> /* دخل المحرف */ int fgetc(FILE *stream); int getc(FILE *stream); int getchar(void); int ungetc(int c, FILE *stream); /* خرج المحرف */ int fputc(int c, FILE *stream); int putc(int c, FILE *stream); int putchar(int c); /* دخل السلسلة النصية */ char *fgets(char *s, int n, FILE *stream); char *gets(char *s); /* خرج السلسلة النصية */ int fputs(const char *s, FILE *stream); int puts(const char *s); لنستعرض سويًّا كلًّا منها. دخل المحرف تقرأ مجموعة الدوال التي تنفذ هذه المهمة المحرف مثل قيمة من نوع "unsigned char" من مجرى الدخل المحدد أو من stdin، ونحصل على المحرف الذي يليه في كل حالة من مجرى الدخل. يُعامل المحرف مثل قيمة "unsigned char" ويُحوّل إلى "int" وهي القيمة المُعادة من الدالة. نحصل على الثابت EOF عند الوصول إلى نهاية الملف، ويُضبط مؤشر نهاية الملف end-of-file indicator إلى المجرى المحدد، كما نحصل على EOF في حالة الخطأ ويُضبط مؤشر الخطأ إلى المجرى المحدّد. نستطيع الحصول على المحارف بصورةٍ تتابعية باستدعاء الدالة تباعًا. قد نحصل على وسيط المجرى stream أكثر من مرة في حال استخدام هذه الدوال على أنها ماكرو، لذا لا تستخدم الآثار الجانبية هنا. هناك برنامج "ungetc" الداعم أيضًا، الذي يُستخدم لإعادة محرف إلى المجرى مما يجعله المحرف التالي الذي سيُقرأ، إلا أن هذه ليست بعملية خرج ولن تتسبب بتغيير محتوى الملف، ولذا تتسبب عمليات fflush و fseek و rewind على المجرى بين عملية إعادة المحرف وقراءته بتجاهل هذا المحرف، ويمكن إعادة محرف واحد فقط وأي محاولات لإعادة EOF تُتجاهل، ولا يُعدّل على موضع مؤشر الملف في جميع حالات إعادة قراءة مجموعة من المحارف وإعادة قرائتها أو تجاهلها. يتسبب استدعاء ungetc الناجح على مجرى ثنائي بتناقص موضع مؤشر الملف إلا أن ذلك غير محدد عند استخدام مجرى نصي، أو مجرى ثنائي موجود في بداية الملف. خرج المحرف هذه الدوال مطابقة لدوال الدخل الموصوفة سابقًا إلا أنها تجري عمليات الخرج، إذ تعيد المحرف المكتوب أو EOF عند حدوث خطأ ما، ولا يوجد ما يعادل نهاية الملف End Of File في ملف الخرج. خرج السلسلة النصية تكتب هذه الدوال سلاسلًا نصيةً إلى ملف الخرج باستخدام المجرى stream إن ذُكر وإلا فإلى المجرى stdout، ولا يُكتب محرف الإنهاء الفارغ. نحصل على قيمة لا تساوي الصفر عند حدوث خطأ وإلا فالقيمة صفر. تحذير: تضيف puts سطرًا جديدًا إلى سلسلة الخرج النصية بينما لا تفعل fputs ذلك. دخل السلسلة النصية تقرأ الدالة fgets السلسلة النصية إلى مصفوفة يُشار إليها باستخدام المؤشر s من المجرى stream، وتتوقف عن القراءة في حال الوصول إلى EOF أو عند أول سطر جديد (وتقرأ محرف السطر الجديد)، وتضيف محرفًا فارغًا null في النهاية. يُقرأ n-1 محرف على الأكثر (لترك حيز للمحرف الفارغ). تعمل الدالة gets على نحوٍ مشابه لمجرى stdin إلا أنها تتجاهل محرف السطر الجديد. تعيد كلا الدالتين s في حال نجاحهما وإلا فمؤشر فارغ، إذ نحصل على مؤشر فارغ عندما نصادف EOF قبل قراءة أي محرف ولا يطرأ أي تغيير على المصفوفة، بينما تصبح محتويات المصفوفة غير معرفة إذا ما واجهنا خطأ قراءة وسط السلسلة النصية بالإضافة إلى إعادة مؤشر فارغ. الدخل والخرج غير المنسق هذا الجزء بسيط، إذا هناك فقط دالتان تقدمان هذه الخاصية، واحدة منهما للقراءة والأخرى للكتابة ويصرَّح عنهما على النحو التالي: #include <stdio.h> size_t fread(void *ptr, size_t size, size_t nelem, FILE *stream); size_t fwrite(const void *ptr, size_t size, size_t nelem, FILE *stream); تُجرى عملية القراءة أو الكتابة المناسبة للبيانات المُشار إليها بواسطة المؤشر ptr وذلك على nelem عنصر، وبحجم size، ويفشل نقل ذلك المقدار الكامل من العناصر فقط عند الكتابة، إذ قد تعيق نهاية الملف دخل العناصر بأكملها، وتُعيد الدالة عدد العناصر التي نُقلت فعليًّا. نستخدم feof أو ferror للتمييز بين نهاية الملف عند الدخل أو للإشارة على خطأ. تُعيد الدالة القيمة صفر دون أي فعل إذا كانت قيمة size أو nelem تساوي إلى الصفر. قد يساعدنا المثال الآتي في توضيح عمل الدالتين المذكورتين: #include <stdio.h> #include <stdlib.h> struct xx{ int xx_int; float xx_float; }ar[20]; main(){ FILE *fp = fopen("testfile", "w"); if(fwrite((const void *)ar, sizeof(ar[0]), 5, fp) != 5){ fprintf(stderr,"Error writing\n"); exit(EXIT_FAILURE); } rewind(fp); if(fread((void *)&ar[10], sizeof(ar[0]), 5, fp) != 5){ if(ferror(fp)){ fprintf(stderr,"Error reading\n"); exit(EXIT_FAILURE); } if(feof(fp)){ fprintf(stderr,"End of File\n"); exit(EXIT_FAILURE); } } exit(EXIT_SUCCESS); } [مثال 1] ترجمة -وبتصرف- لقسم من الفصل Libraries من كتاب The C Book. اقرأ أيضًا المقال التالي: أدوات مكتبة stdlib في لغة سي C المقال السابق: مقدمة عن التعامل مع الدخل والخرج I/O في لغة سي C التعامل مع المحارف والسلاسل النصية في لغة سي C التعامل مع المحارف وضبط إعدادات التوطين localization في لغة سي C
-
يعدّ افتقار لغات البرمجة لدعمها للدخل والخرج إحدى أبرز الأسباب التي منعت التبني واسع النطاق واستخدامها في البرمجة العملية، وهو الموضوع الذي لم يرد أي مصمّم لغة أن يتعامل معه، إلا أن لغة سي تفادت هذه المشكلة، بعدم تضمينها لأي دعم للدخل والخرج، إذ كان سلوك سي هو أن تترك التعامل مع الدخل والخرج لدوال المكتبات، مما عنى أنه بالإمكان لمصمّمي الأنظمة استخدام طرق دخل وخرج مخصصة بدلًا من إجبارهم على تغيير اللغة بذات نفسها. تطوّرت حزمة مكتبات عُرفت باسم "مكتبة الدخل والخرج القياسي Standard I/O Library" -أو اختصارًا stdio- في الوقت ذاته الذي كانت لغة سي تتطوّر، وقد أثبتت هذه المكتبة مرونتها وقابلية نقلها وأصبحت الآن جزءًا من المعيار. اعتمدت حزمة الدخل والخرج القياسي القديمة كثيرًا على نظام يونيكس UNIX للوصول إلى الملفات وبالأخص الافتراض أنه لا يوجد أي فرق بين ملفات ثنائية غير مُهيكلة وملفات أخرى تحتوي على نص مقروء، إلا أن العديد من أنظمة التشغيل تفصل ما بين الاثنين وعُدّلت الحزمة فيما بعد لضمان قابلية نقل برامج لغة سي بين نوعَي نظام الملفات. هناك بعض التغييرات في هذا المجال التي تتسبب بالضرر لكثيرٍ من البرامج المكتوبة مسبقًا على الرغم من الجهود التي تحاول أن تحدّ من هذا الضرر. من المفترض أن تعمل برامج لغة سي القديمة بنجاح دون تعديل في بيئة يونيكس. نموذج الدخل والخرج لا يُميّز نموذج الدخل والخرج بين أنواع الأجهزة المادية التي تدعم الدخل والخرج، إذ يُعامل كل مصدر أو حوض من البيانات بالطريقة ذاتها ويُنظر إليه على أنه مجرًى من البايتات stream of bytes. بما أن الكائن الأصغر الذي يمكن تمثيله في لغة سي هو المحرف، فالوصول إلى الملف مسموحٌ باستخدام حدود أي محرف، وبالتالي يمكن قراءة أو كتابة أي عدد من المحارف انطلاقًا من نقطة متحركة تُعرف باسم مؤشر الموضع position indicator، وتُكتب أو تُقرأ المحارف تباعًا بدءًا من هذه النقطة ويُحرّك مؤشر الموضع خلال ذلك. يُضبط مؤشر الموضع مبدئيًا إلى بداية الملف عند فتحه، لكن من الممكن تحريكه باستخدام طلبات تحديد الموقع، ويُتجاهل مؤشر موضع الملف في حال كان الوصول العشوائي إلى الملف غير ممكن. لفتح الملف في نمط الإضافة append تأثيرات على مجرى موضع المؤشر في الملف معرفة بحسب التنفيذ. الفكرة العامة هي تقديم إمكانية القراءة أو الكتابة بصورةٍ تتابعية، باستثناء حالة فتح المجرى باستخدام نمط الإضافة، أو إذا حُرّك مؤشر موضع الملف مباشرةً. هناك نوعان من أنواع الملفات، هما: الملفات النصية text files والملفات الثنائية binary files التي يمكن التعامل معها داخل البرنامج على أنها مجاري نصية text streams أو مجاري ثنائية binary streams بعد فتحها لعمليات الإدخال والإخراج. لا تسمح حزمة stdio بالعمليات على محتوى الملف مباشرةً، بل بالتعديل على المجرى الذي يحتوي على بيانات الملف. المجاري النصية يحدّد المعيار المجرى النصي text stream، الذي يمثّل ملفًا يحتوي على أسطر نصية ويتألف السطر من صفر محرف أو أكثر ينتهي بمحرف نهاية السطر، ومن الممكن أن يكون تمثيل الأسطر الفعلي في البيئة الخارجية مختلفًا عن تمثيله هنا، كما من الممكن إجراء تحويلات على مجرى البيانات عند دخولها إلى أو خروجها من البرنامج، وأكثر المتطلبات شيوعًا هو ترجمة المحرف الذي ينهي السطر "'\n'" إلى السلسلة "'\r\n'" عند الخرج وإجراء عكس العملية عند الدخل، ومن الممكن تواجد بعض الترجمات الضرورية الأخرى. يُضمن للبيانات التي تُقرأ من المجرى النصي أن تكون مساويةً إلى البيانات المكتوبة سابقًا إلى الملف، وذلك إذا كانت هذه البيانات مؤلفةً من أسطر مكتملة تحتوي على محارف يمكن طباعتها، وكانت محارف التحكم control characters ومحارف مسافة الجدولة الأفقية horizontal-tab ومحارف الأسطر الجديدة newline فقط، ولم يُتبع أي محرف سطر جديد بمحرف مسافة فارغة space مباشرةً، وكان المحرف الأخير في المجرى هو محرف سطر جديد. كما أن هناك ضمان بأن المحرف الأخير المكتوب إلى الملف النصي هو محرف سطر جديد، ومن الممكن قراءة الملف مجددًا بمحتوياته المماثلة التي كُتبت إليه سابقًا. إلحاق المحرف الأخير المكتوب إلى الملف بمحرف سطر جديد معرفٌ بحسب التنفيذ، وذلك لأن الملفات النصية والملفات الثنائية تُعامل نفس المعاملة في بعض التنفيذات. قد تُجرّد بعض التنفيذات المسافة الفارغة البادئة من الأسطر التي تتألف من مسافات فارغة فقط متبوعةً بسطر جديد، أو تُجرّد المسافة الفارغة في نهاية السطر. يجب أن يدعم التنفيذ الملفات النصية التي تحتوي سطورها على 254 محرفًا على الأقل، ويتضمن ذلك محرف السطر الجديد الذي يُنهي السطر. قد نحصل على مجرى ثنائي عند فتح مجرى نصي بنمط التحديث update mode في بعض التنفيذات. قد تتسبب الكتابة على مجرًى نصي باقتطاع الملف عند نقطة الكتابة في بعض التنفيذات، أي ستُهمل جميع البيانات التي تتبع البايت الأخير المكتوب. المجاري الثنائية يمثل المجرى الثنائي سلسلةً من المحارف التي يمكن استخدامها لتسجيل البيانات الداخلية لبرنامج ما، مثل محتويات الهياكل structures، أو المصفوفات وذلك بالشكل الثنائي، إذ تكون البيانات المقروءة من المجاري الثنائية مساويةً للبيانات المكتوبة إلى المجرى ذاته سابقًا ضمن نفس التنفيذ، وقد يُضاف عددٌ من المحارف الفارغة "NULL" في بعض الظروف إلى نهاية المجرى الثنائي، ويكون عدد المحارف معرفًا بحسب التنفيذ. تعتمد بيانات الملفات الثنائية على الآلة التي تعمل عليها لأبعد حد، وهي غير قابلة للنقل عمومًا. المجاري الأخرى قد تتوفر بعض أنواع المجاري الأخرى، إلا أنها معرفة بحسب التنفيذ. ملف الترويسة <stdio.h> هناك عدد من الدوال والماكرو الموجودة لتقديم الدعم لمختلف أنواع المجاري، ويحتوي ملف الترويسة <stdio.h> العديد من التصريحات المهمة لهذه الدوال، إضافةً إلى الماكرو التالية وتصاريح الأنواع: النوع FILE: نوع الكائن المُستخدم لاحتواء معلومات التحكم بالمجرى، ولا يحتاج مستخدمو مكتبة "stdio" لمعرفة محتويات هذه الكائنات، إذ يكفي التعامل مع المؤشرات التي تشير إليهم. لا يُعد نسخ الكائنات هذه ضمن البرنامج آمنًا، إذ أن عناوينهم قد تكون في بعض الأحيان معقدة. النوع fpos_t: نوع الكائن الذي يُستخدم لتسجيل القيم الفريدة من نوعها التي تنتمي إلى مجرى مؤشر موضع الملف. القيم IOFBF_ و IOLBF_ و IONBF_: وهب قيم تُستخدم للتحكم بالتخزين المؤقت buffering للمجرى بالاستعانة بالدالة setvbuf. القيمة BUFSIZ: حجم التخزين المؤقت المُستخدم بواسطة الدالة setbuf، وهو تعبيرٌ رقم صحيح integral ثابت constant تكون قيمته 256 على الأقل. القيمة EOF: تعبير رقم صحيح سالب ثابت يحدد نهاية الملف end-of-file ضمن مجرى، أي عند الوصول إلى نهاية الدخل. القيمة FILENAME_MAX: الطول الأعظمي الذي يمكن لاسم ملف أن يكون إذا كان هناك قيد على ذلك، وإلا فهو الحجم الذي يُنصح به لمصفوفة تحمل اسم ملف. القيمة FOPEN_MAX: العدد الأدنى من الملفات التي يضمن التنفيذ فتحها في وقت آني، وهو ثمانية ملفات. لاحظ أنه من الممكن إغلاق ثلاث مجاري مُعرفة مسبقًا إذا احتاج البرنامج فتح أكثر من خمسة ملفات مباشرةً. القيمة L_tmpnam: الطول الأعظمي المضمون لسلسلة نصية في tmpnam، وهو تعبير رقم صحيح ثابت. القيم SEEK_CUR و SEEK_END و SEEK_SET: تعابير رقم صحيح ثابتة تُستخدم للتحكم بأفعال fseek. القيمة TMP_MAX: العدد الأدنى من أسماء الملفات الفريدة من نوعها المولدة من قبل tmpnam، وهو تعبير رقم صحيح ثابت بقيمة لا تقل عن 25. الكائنات stdin و stdout و stderr: وهي كائنات معرفة مسبقًا من النوع "* FILE" وتشير إلى مجرى الدخل القياسي ومجرى الخرج القياسي ومجرى الخطأ بالترتيب، وتُفتح هذه المجاري تلقائيًا عند بداية تنفيذ البرنامج. العمليات على المجاري بعد أن تعرفنا على أنواع المجاري وطبيعتها والقيم المرتبطة بها، نستعرض الآن العمليات الأساسية عليها ألا وهي فتح المجرى وإغلاقه، إضافةً إلى التخزين المؤقت. فتح المجرى يتصل المجرى بالملف عن طريق دالة fopen، أو freopen، أو tmpfile، إذ تعيد هذه الدوال -إذا كان استدعاؤها ناجحًا- مؤشرًا يشير إلى كائن من نوع FILE. هناك ثلاث أنواع من المجاري المتاحة افتراضيًا دون أي جهد إضافي مطلوب منك، وتتصل هذه المجاري عادةً بالجهاز المادي المُرتبط بالبرنامج المُنفّذ ألا وهو الطرفية Terminal عادةً، ويشار إلى هذه المجاري بالأسماء: stdin: وهو مجرى الدخل القياسي standard input. stdout: وهو مجرى الخرج القياسي standard output. stderr: وهو مجرى الخطأ القياسي standard error. ويكون دخل لوحة المفاتيح في الحالة الطبيعية من المجرى stdin وخرج الطرفية هو stdout، بينما تُوجّه رسائل الأخطاء إلى المجرى stderr. الهدف من فصل رسائل الأخطاء عن رسائل الخرج العادية هو السماح بربط مجرى stdout إلى شيءٍ آخر مغاير لجهاز للطرفية مثل ملف ما والحصول على رسائل الخطأ بنفس الوقت على الشاشة أمامك عوضًا عن توجيه الأخطاء إلى الملف، وتخزّن كامل الملفات مؤقتًا إذا لم توجّه إلى أجهزة تفاعلية. كما ذكرنا سابقًا، قد يكون مؤشر موضع الملف قابلًا للتحريك أو غير قابل للتحريك بحسب الجهاز المُستخدم، إذ يكون مؤشر موضع الملف غير قابل للتحريك ضمن مجرى stdin على سبيل المثال إذا كان متصلًا إلى الطرفية (الحالة الاعتيادية له). جميع الملفات غير المؤقتة تمتلك اسمًا filename وهو سلسلةٌ نصية، والقوانين التي تحدد اسم الملف الصالح معرفةٌ حسب التنفيذ، وينطبق الأمر ذاته على إمكانية فتح الملف لعدة مرات بصورةٍ آنية. قد يتسبب فتح ملف جديد بإنشاء هذا الملف، وتتسبب إعادة إنشاء ملف موجود مسبقًا بإهمال محتوياته السابقة. إغلاق المجرى تُغلق المجاري عند استدعاء fclose أو exit بصورةٍ صريحة، أو عندما يعود البرنامج إلى الدالة main، وتُمسح جميع البيانات المخزنة مؤقتًا عند إغلاق المجرى. تصبح حالة الملفات المفتوحة غير معروفة إذا توقف البرنامج لسببٍ ما دون استخدام الطرق السابقة لإغلاقه. التخزين المؤقت للمجرى هناك ثلاث أنواع للتخزين المؤقت: دون تخزين مؤقت unbuffered: تُستخدم مساحة التخزين بأقل ما يمكن من قبل stdio بهدف إرسال أو تلقي البيانات أسرع ما يمكن. تخزين مؤقت خطي line buffered: تُعالج المحارف سطرًا تلو سطر، ويُستخدم هذا النوع من التخزين المؤقت كثيرًا في البيئات التفاعلية، وتُمسح محتويات الذواكر المؤقتة الداخلية internal buffers فقط عندما تمتلئ أو عندما يُعالج سطر جديد. التخزين المؤقت الكامل fully buffered: تُسمح الذواكر المؤقتة الداخلية فقط عندما تمتلئ. يُمكن مسح محتوى الذاكرة الداخلية المرتبطة بمجرى ما عن طريق استخدام fflush مباشرةً. يُعرَّف الدعم لأنواع التخزين المؤقت المختلفة بحسب التنفيذ، ويمكن التحكم به ضمن الحدود المعرفة باستخدام setbuf و setvbuf. التلاعب بمحتويات الملف مباشرة هناك عدة دوال تسمح لنا بالتعامل مع الملف مباشرةً. #include <stdio.h> int remove(const char *filename); int rename(const char *old, const char *new); char *tmpnam(char *s); FILE *tmpfile(void); الدالة remove: تتسبب بإزالة الملف، وستفشل محاولات فتح هذا الملف لاحقًا إلا في حال إنشاء الملف مجددًا. يكون سلوك الدالة remove عندما يكون الملف مفتوحًا معرفًا بحسب التنفيذ، وتعيد الدالة القيمة صفر للدلالة على النجاح، بينما تدل أي قيمة أخرى على فشل عملها. الدالة rename: تُغيّر اسم الملف المعرف بالكلمة old في مثالنا السابق إلى new، وستفشل محاولات فتح الملف باستخدام اسمه القديم، إلا إذا أنشئ ملفٌ جديد يحمل الاسم القديم ذاته، وكما هو الحال في remove فإن الدالة rename تُعيد القيمة صفر للدلالة على نجاح العملية وأي قيمة مغايرة لذلك تدل على حصول خطأ. السلوك معرف حسب التنفيذ إذا حاولنا تسمية الملف باسم جديد باستخدام rename وكان هناك ملف بالاسم ذاته مسبقًا. لن يُعدّل على الملف إذا فشلت الدالة rename لأي سببٍ كان. الدالة tmpnam: تولّد سلسلة نصية لتُستخدم اسمًا لملف، ويضمن لهذه السلسلة النصية أن تكون فريدةً من نوعها بالنسبة لأي اسم ملف آخر موجود، ويمكن أن تُستدعى بصورةٍ متتالية للحصول على اسم جديد كل مرة. يُستخدم الثابت TMP_MAX لتحديد عدد مرات استدعاء الدالة tmpnam قبل أن يتعذر عليه العثور على اسماء فريدة، وقيمته 25 على الأقل، ونحصل على سلوك غير معرّف من قبل المعيار في حال استدعاء الدالة tmpnam عدد مرات يتجاوز هذا الثابت إلا أن الكثير من التنفيذات تقدم حدًّا لا نهائيًا. تستخدم tmpnam ذاكرة مؤقتة داخلية لبناء الاسم وتُعيد مؤشرًا يشير إليه وذلك إذا ضُبط الوسيط s إلى القيمة NULL، وقد تغيّر الاستدعاءات اللاحقة للدالة الذاكرة المؤقتة الداخلية ذاتها. يمكن استخدام مؤشر يشير إلى مصفوفة مثل وسيط بدلًا من السابق، بحيث تحتوي المصفوفة على L_tmpnam محرف على الأقل، وفي هذه الحالة سُيملأ الاسم إلى الذاكرة المؤقتة المزوّدة (المصفوفة)، ويمكن فيما بعد إنشاء ملف بهذا الاسم واستخدامه ملفًا مؤقتًا. لن يكون اسم الملف مفيدًا ضمن سياقات أخرى غالبًا، بالنظر إلى توليده من قبل الدالة. لا تُزال الملفات المؤقتة من هذا النوع إلا إن استدعيت دالة الحذف، وغالبًا ما تُستخدم هذه الملفات لتمرير البيانات المؤقتة بين برنامجين منفصلين. الدالة tmpfile: تُنشئ ملف ثنائي مؤقت يُمكن التعديل على محتوياته، وتعيد الدالة مؤشرًا يشير إلى مجرى الملف، ويُزال هذا الملف فيما بعد عند إغلاق مجراه، وتُعيد الدالة tmpfile مؤشرًا فارغًا null إذا لم ينجح فتح الملف. فتح الملفات بالاسم يمكن فتح الملفات الموجودة بالاسم عن طريق استدعاء الدالة fopen المصرّح عنها على النحو التالي: #include <stdio.h> FILE *fopen(const char *pathname, const char *mode); يمثل الوسيط pathname اسم الملف الذي تريد فتحه، مثل الاسم الذي تعيده الدالة tmpnam أو أي اسم ملف معين آخر. يمكن فتح الملفات باستخدام عدة أنماط modes، مثل نمط القراءة read لقراءة البيانات، ونمط الكتابة write لكتابة البيانات وهكذا. لاحظ أن الدالة fopen ستُنشئ ملفًا إذا أردت كتابة البيانات على ملف، أو أنها ستتخلص من محتويات الملف إذا وُجد ليصبح طوله صفر (أي أنك ستخسر محتويات الملف السابقة). يوضح الجدول التالي جميع الأنماط الموجودة في المعيار، إلا أن التنفيذ قد يسمح بأنماط أخرى بإضافة محارف إضافية في نهاية كل من الأنماط. 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; } النمط نوع الملف القراءة الكتابة إنشاء جديد حذف القيمة السابقة "r" نصي نعم لا لا لا "rb" ثنائي نعم لا لا لا "r+" نصي نعم نعم لا لا "r+b" ثنائي نعم نعم لا لا "rb+" ثنائي نعم نعم لا لا "w" نصي لا نعم نعم نعم "wb" ثنائي لا نعم نعم نعم "w+" نصي نعم نعم نعم نعم "w+b" ثنائي نعم نعم نعم نعم "wb+" ثنائي نعم نعم نعم نعم "a" نصي لا نعم نعم لا "ab" ثنائي لا نعم نعم لا "a+" نصي نعم نعم نعم لا "a+b" ثنائي لا نعم نعم لا "ab+" ثنائي لا نعم نعم لا انتبه من بعض التنفيذات التي تضيف إلى النمط الأخير محارف NULL في حالة الملفات الثنائية، إذ قد يتسبب فتح هذه الملفات بالنمط ab أو +ab أو a+b بوضع مؤشر الملف خارج نطاق آخر البيانات المكتوبة. تُكتب جميع البيانات في نهاية الملف إذا فُتح باستخدام نمط الإضافة append، بغض النظر عن محاولة تغيير موضع المؤشر باستخدام الدالة fseek، ويكون موضع مؤشر الملف المبدئي معرف بحسب التنفيذ. تفشل محاولات فتح الملف بنمط القراءة (النمط 'r')، إذا لم يكن الملف موجودًا أو لم يمكن قراءته. يمكن القراءة من والكتابة إلى الملفات المفتوحة بنمط التحديث update (باستخدام '+' مثل المحرف الثاني أو الثالث ضمن النمط) إلا أنه من غير الممكن إلحاق القراءة بالكتابة مباشرةً أو الكتابة بالقراءة دون استدعاء بينهما لدالة واحدة (أو أكثر) من الدوال: fflush أو fseek أو fsetpos أو rewind، والاستثناء الوحيد هنا هو جواز إلحاق الكتابة مباشرةً بعد القراءة إذا قُرأ المحرف EOF (نهاية الملف). من الممكن أيضًا في بعض التنفيذات أن يُتخلى عن b في أنماط فتح الملفات الثنائية واستخدام الأنماط ذاتها الخاصة بالملفات النصية. تُخزّن المجاري المفتوحة باستخدام fopen تخزينًا مؤقتًا بالكامل إذا لم تكن متصلة إلى جهاز تفاعلي، ويضمن ذلك التعامل مع الأسئلة prompts والطلبات responses على النحو الصحيح. تعيد الدالة fopen مؤشرًا فارغًا null إذا فشلت بفتح الملف، وإلا فتعيد مؤشرًا يشير إلى الكائن الذي يتحكم بالمجرى. كائنات المجاري stdin و stdout و stderr غير قابلة للتعديل بالضرورة ومن الممكن عدم وجود إمكانية استخدام القيمة المُعادة من الدالة fopen لإسنادها إلى واحدة من هذه الكائنات، بدلًا من ذلك نستخدم freopen لهذا الغرض. الدالة freopen تُستخدم الدالة freopne لأخذ مؤشر يشير إلى مجرى وربطه مع اسم ملف آخر، وتصرَّح الدالة على النحو التالي: #include <stdio.h> FILE *freopen(const char *pathname, const char *mode, FILE *stream); وسيط mode مشابه لمثيله في دالة fopen. يُغلق المجرى stream أولًا ويحدث تجاهل أي أخطاء متولدة عن ذلك، ونحصل على قيمة NULL في حالة حدوث خطأ عند تنفيذ الدالة، وإلا فإننا نحصل على القيمة الجديدة للمجرى stream. إغلاق الملفات يمكننا إغلاق ملف مفتوح باستخدام الدالة close والمصرح عنها كما يلي: #include <stdio.h> int fclose(FILE *stream); يُتخلّص من أي بيانات موجودة على الذاكرة المؤقتة لم تُكتب على الملف الخاص بالمجرى stream إضافةً إلى أي بيانات أخرى لم تُقرأ، وتُحرّر الذاكرة المؤقتة المرتبطة بالمجرى إذا رُبطت به تلقائيًا، وأخيرًا يُغلق الملف. نحصل على القيمة صفر للدلالة على نجاح العملية، وإلا فالقيمة EOF للدلالة على الخطأ. الدالتان setbuf و setvbuf تُستخدم الدالتان للتعديل على استراتيجية التخزين المؤقتة لمجرى معين مفتوح، ويُصرّح عن الدالتين كما يلي: #include <stdio.h> int setvbuf(FILE *stream, char *buf, int type, size_t size); void setbuf(FILE *stream, char *buf); يجب استخدام الدالتين قبل قراءة الملف أو الكتابة إليه، ويعرف الوسيط type نوع التخزين المؤقت للمجرى stream، ويوضح الجدول التالي أنواع التخزين المؤقت. القيمة التأثير _IONBF لا تخزّن الدخل والخرج مؤقتًا _IOFBF خزِّن الدخل والخرج مؤقتًا _IOLBF تخزين مؤقت خطي: تخلص من محتويات الذاكرة المؤقتة عندما تمتلئ، أو عند كتابة سطر جديد، أو عند طلب القراءة يمكن للوسيط buf أن يكون مؤشرًا فارغًا، وفي هذه الحالة تُنشأ مصفوفة تلقائيًا لتخزين البيانات مؤقتًا، ويمكن بخلاف ذلك للمستخدم توفير ذاكرة مؤقتة لكن يجب التأكد من استمرارية الذاكرة المؤقتة بقدر مساوٍ (أو أكثر) لاستمرارية التدفق stream. يُعد استخدام مساحة التخزين المحجوزة تلقائيًا ضمن تعليمة مركبة compound statement من الأخطاء الشائعة، إذ أن الحصول على المساحة التخزينية على النحو الصحيح في هذه الحالة يجري عن طريق الدالة malloc عوضًا عن ذلك. يُحدد حجم الذاكرة المؤقتة باستخدام الوسيط size. يشابه استدعاء الدالة setbuf استدعاء الدالة setvbuf إذا استخدمنا _IOFBF قيمةً للوسيط type والقيمة BUFSIZ للوسيط size، وتُستخدم القيمة _IONBF للوسيط type إذا كان buf مؤشرًا فارغًا. لا تُعاد أي قيمة بواسطة الدالة setbuf، بينما تُعيد الدالة setvbuf القيمة صفر للدلالة على نجاح الاستدعاء، وإلا فقيمة غير صفرية إذا كانت قيم type، أو size غير صالحة، أو كان الطلب غير ممكن التنفيذ. دالة fflush يُصرّح عن الدالة fflush كما يلي: #include <stdio.h> int fflush(FILE *stream); إذا أشار المجرى stream إلى ملف مفتوح للخرج أو بنمط التحديث، وكان هناك أي بيانات غير مكتوبة فإنها تُكتب خارجًا، وهذا يعني أنه لا يمكن لدالة داخل بيئة مستضافة hosted environment، أو ضمن لغة سي أن تضمن -على سبيل المثال- أن البيانات تصل مباشرةً إلى سطح قرص يدعم الملف. تُهمَل أي عملية ungetc سابقة إذا كان المجرى مرتبطًا بالملف المفتوح بهدف الخرج أو التحديث. يجب أن تكون آخر عملية على المجرى عملية خرج، وإلا فسنحصل على سلوك غير معرّف. يتخلص استدعاء fflush الذي يحتوي على وسيط قيمته صفر من جميع مجاري الدخل والخرج، ويجب هنا الانتباه إلى المجاري التي لم تكن عمليتها الأخيرة عملية خرج، أي تفادي حصول السلوك غير المعرّف الذي ذكرناه سابقًا. تُعيد الدالة القيمة EOF للدلالة على الخطأ، وإلا فالقيمة صفر للدلالة على النجاح. الدوال عشوائية الوصول Random access functions تعمل جميع دوال دخل وخرج الملفات بصورةٍ مشابهة بين بعضها، إذ أن الملفات ستُقرأ أو يُكتب إليها بصورةٍ متتابعة إلا إذا اتخذ المستخدم خطوات مقصودة لتغيير موضع مؤشر الملف. ستتسبب عملية قراءة متبوعة بكتابة متبوعة بقراءة ببدء عملية القراءة الثانية بعد نهاية عملية كتابة البيانات فورًا، وذلك بفرض أن الملف مفتوح باستخدام نمط يسمح بهذا النوع من العمليات، كما يجب أن تتذكر أن المجرى stdio يُصرّ على إدخال المستخدم لعملية تحرير ذاكرة مؤقتة بين كل عنصر من عناصر دورة قراءة- كتابة- قراءة، وللتحكم بذلك، تسمح دالة الوصول العشوائي random access function بالتحكم بموضع الكتابة والقراءة ضمن الملف، إذ يُحرّك موضع مؤشر الملف دون الحاجة لقراءة أو كتابة ويشير إلى البايت الذي سيخضع لعملية القراءة أو الكتابة التالية. هناك ثلاثة أنواع من الدوال التي تسمح بفحص موضع مؤشر الملف أو تغييره، إليك تصاريح كل منهم: #include <stdio.h> /* إعادة موضع مؤشر الملف */ long ftell(FILE *stream); int fgetpos(FILE *stream, fpos_t *pos); /* ضبط موضع مؤشر الملف إلى الصفر */ void rewind(FILE *stream); /* ضبط موضع مؤشر الملف */ int fseek(FILE *stream, long offset, int ptrname); int fsetpos(FILE *stream, const fpos_t *pos); تُعيد الدالة ftell القيمة الحالية لموضع مؤشر الملف (المُقاسة بعدد المحارف)، إذا كان المجرى stream يشير إلى ملف ثنائي، وإلا فإنها تعيد رقمًا مميزًا في حالة الملف النصي، ويمكن استخدام هذه القيمة فقط عند استدعاءات لاحقة لدالة fseek لإعادة ضبط موضع مؤشر الملف الحالة. نحصل على القيمة -1L في حالة الخطأ ويُضبط errno. تضبط الدالة rewind موضع مؤشر الملف الحالي إلى بداية الملف المُشار إليه بالمجرى stream، ويُعاد ضبط مؤشر خطأ الملف باستدعاء الدالة rewind ولا تُعيد الدالة أي قيمة. تسمح الدالة fseek لموضع مؤشر الملف ضمن المجرى أن يُضبط لقيمة عشوائية (للملفات الثنائية)، أو إلى الموضع الذي نحصل عليه من ftell فقط بالنسبة للملفات النصية، وتتبع الدالة القوانين التالية: يُضبط موضع مؤشر الملف في الحالة الاعتيادية بفارق معين من البايتات (المحارف) عن نقطة الملف المُحددة بالقيمة prtname، وقد يكون الفارق offset سالبًا. قد يأخذ ptrname القيمة SEEK_SET التي تضبط موضع مؤشر الملف نسبيًا إلى بداية الملف، أو القيمة SEEK_CUR التي تضبط موضع مؤشر الملف نسبيًا إلى قيمتها الحالية، أو القيمة SEEK_END التي تضبط موضع مؤشر الملف نسبيًا إلى نهاية الملف، إلا أنه من غير المضمون أن تعمل القيمة الأخيرة بنجاح في المجاري الثنائية. يجب أن تكون قيمة offset في الملفات النصية إما صفر أو قيمة مُعادة بواسطة استدعاء سابق للدالة ftell على المجرى ذاته، ويجب أن تكون قيمة ptrnmae مساويةً إلى SEEK_SET. يُفرغ fseek مؤشر نهاية الملف للمجرى المُحدد ويحذف بيانات أي استدعاء لعملية ungetc، ويعمل ذلك لكلٍّ من الدخل والخرج. تُعاد القيمة صفر للدلالة على النجاح وأي قيمة غير صفرية تدل على طلب ممنوع للدالة. لاحظ أنه يمكن لكلٍ من ftell و fseek ترميز قيمة موضع مؤشر الملف إلى قيمة من نوع long، وقد لا يحدث هذا بنجاح في حالة استخدامه على الملفات الطويلة جدًا؛ لذلك، يقدم المعيار كلًا من fgetpos و fsetpos للتغلُّب على هذه المشكلة. تخزِّن الدالة fgetpos موضع مؤشر الملف الحالي ضمن المجرى للكائن المُشار إليه باستخدام المؤشر pos، والقيمة المخزنة هي قيمة مميزة تُستخدم فقط للعودة إلى الموضع المحدد ضمن المجرى ذاته باستخدام الدالة fsetpos. تعمل الدالة fsetpos كما وضحنا سابقًا، كما أنها تُفرغ مؤشر نهاية الملف للمجرى وتُزيل أي تأثير لعمليات ungetc سابقة. نحصل على القيمة صفر في حالة النجاح لكلا الدالتين، بينما نحصل على قيمة غير صفرية في حالة الخطأ ويُضبط errno. التعامل مع الأخطاء تحافظ دوال الدخل والخرج القياسية على مؤشرين لكل مجرى مفتوح للدلالة على نهاية الملف وحالة الخطأ ضمنه، ويمكن الحصول على قيم هذه المؤشرات وضبطها عن طريق الدوال التالية: #include <stdio.h> void clearerr(FILE *stream); int feof(FILE *stream); int ferror(FILE *stream); void perror(const char *s); تُفرّغ الدالة clearerr كلًا من مؤشري الخطأ ونهاية الملف EOF للمجرى stream. تُعيد الدالة feof قيمةً غير صفرية إذا كان لمؤشر نهاية الملف الخاص بالمجرى stream قيمة، وإلا فإنها تعيد القيمة صفر. تُعيد الدالة ferror قيمة غير صفرية إذا كان لمؤشر الخطأ الخاص بالمجرى stream قيمة، وإلا فإنها تعيد القيمة صفر. تطبع الدالة perror سطرًا واحدًا يحتوي على رسالة خطأ على خرج البرنامج القياسي مسبوقًا بالسلسلة النصية المُشار إليها بواسطة المؤشر s مع إضافة مسافة فارغة ونقطتين ":". تُحدد رسالة الخطأ بحسب قيمة errno وتُعطي شرحًا بسيطًا عن سبب الخطأ، على سبيل المثال يتسبب البرنامج التالي برسالة خطأ: #include <stdio.h> #include <stdlib.h> main(){ fclose(stdout); if(fgetc(stdout) >= 0){ fprintf(stderr, "What - no error!\n"); exit(EXIT_FAILURE); } perror("fgetc"); exit(EXIT_SUCCESS); } /* رسالة الخطأ */ fgetc: Bad file number [مثال 2] لم نقُل أن الرسالة التي سنحصل عليها ستكون واضحة. ترجمة -وبتصرف- لقسم من الفصل Libraries من كتاب The C Book. اقرأ أيضًا المقال التالي: التعامل مع الدخل والخرج I/O وتنسيقه في لغة سي C المقال السابق: التعامل مع المكتبات في لغة سي C التعامل مع المحارف والسلاسل النصية في لغة سي C التعامل مع المؤشرات Pointers في لغة سي C التعامل مع المحارف وضبط إعدادات التوطين localization في لغة سي C
-
كانت مشكلتنا باستخدام الصف tuple في المقالة السابقة (الشيفرة 4-5) هو أنه علينا إعادة النوع String إلى القيمة المُستدعية ليتسنى لنا استخدام النوع String حتى بعد استدعاء الدالة calculate_length، وذلك لأن النوع String نُقل إلى calculate_length. بدلًا مما سبق يمكننا استخدام مرجع reference إلى القيمة String؛ والمرجع هو أشبه بالمؤشر pointer، إذ يُمثل عنوانًا يمكنك اتباعه للوصول إلى البيانات المخزنة ضمن العنوان المذكور، وتعود ملكية هذه البيانات إلى متغيرات أخرى مختلفة. من المضمون للمراجع -على عكس المؤشرات- أن تُشير إلى قيمة صالحة لنوع معين طوال دورة حياة المرجع. إليك كيفية تعريف الدالة calculate_length واستخدامها مع احتوائها على مرجع لكائن بمثابة معاملٍ لها بدلًا من أخذ ملكية القيمة: اسم الملف: src/main.rs fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() } لاحظ أولًا أننا أزلنا الشيفرة البرمجية الخاصة بالصف في تصريح المتغير وقيمة الدالة المُعادة، كما أننا مررنا أيضًا s1& إلى الدالة calculate_length وأخذنا في تعريفها String& بدلًا من String وتُمثّل هذه الإشارة & المرجع، وتسمح لك بالإشارة إلى قيمة دون أخذ ملكيتها، ويوضح الشكل التالي هذا المفهوم. [شكل 5: مخطط يوضح String s& الذي يُشير إلى String s1] ملاحظة: عكس عملية المرجع باستخدام & هي التحصيل dereferencing ويمكن تحقيقها باستخدام عامل التحصيل * وسنرى بعض استخدامات التحصيل بالإضافة لتفاصيل العملية لاحقًا. دعنا نأخذ نظرةً أعمق على استدعاء الدالة هنا: let s1 = String::from("hello"); let len = calculate_length(&s1); تسمح لك الكتابة s1& بإنشاء مرجع يشير إلى القيمة s1 إلا أنه لا ينقل الملكية، وبالتالي -وبما أنه لا يملكها- لن تُسقط القيمة (باستخدام drop) التي يشير إليها عندما يتوقف المرجع عن استخدامها. وبالمثل، تستخدم شارة الدالة الرمز & للإشارة إلى أن نوع المعامل s هو مرجع. دعنا نضيف بعض التعليقات التوضيحية: fn calculate_length(s: &String) -> usize { // يمثل s مرجعًا إلى String s.len() } // يخرج s من النطاق هنا إلا أنه لا يُسقط لأنه لا يمتلك القيمة التي يشير إليها النطاق الذي يحتوي على المتغير s هو مماثل لأي نطاق معامل دالة، إلا أن القيمة المُشار إليها باستخدام المرجع لا تُسقَط عندما نتوقف عن استخدام s وذلك لأن s لا يملك القيمة. لا نحتاج لإعادة القيم عندما تحتوي الدوال على مراجع مثل معاملات بدلًا من القيم الفعلية حتى نستطيع منح الملكية مجددًا، وذلك لأننا لم ننقل الملكية في المقام الأول. نطلق على عملية إنشاء المراجع عملية الاستعارة borrowing، فكما الحال في الحياة الواقعية، إذا امتلك شخصٌ ما غرضًا، فيمكنك استعارته منه، وعند الانتهاء من الغرض عليك أن تُعيده إلى ذلك الشخص لأنك لا تملك الغرض. ما الذي يحصل إذًا عندما نحاول التعديل على شيء مُستعار؟ جرّب تنفيذ الشيفرة 4-6 (تحذير: لن تعمل) اسم الملف: src/main.rs fn main() { let s = String::from("hello"); change(&s); } fn change(some_string: &String) { some_string.push_str(", world"); } [الشيفرة 4-6: محاولة التعديل على قيمة مُستعارة] وسيظهر الخطأ التالي: $ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference --> src/main.rs:8:5 | 7 | fn change(some_string: &String) { | ------- help: consider changing this to be a mutable reference: `&mut String` 8 | some_string.push_str(", world"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable For more information about this error, try `rustc --explain E0596`. error: could not compile `ownership` due to previous error المراجع غير قابلة للتعديل immutable افتراضيًا كما هو الحال مع المتغيرات، لذا لا يُمكنك التعديل على شيء باستخدام المرجع. المراجع القابلة للتعديل يُمكننا تصحيح الشيفرة 4-6 السابقة لكي تسمح لنا بتعديل قيمة مُستعارة باستخدام تعديلات بسيطة، وذلك باستخدامنا مرجعًا قابلًا للتعديل mutable reference: اسم الملف: src/main.rs fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); } علينا أولًا أن نعدل المتغير s ليصبح mut، ثم نُنشئ مرجعًا قابلًا للتعديل باستخدام mut s& عند نقطة استدعاء الدالة chang، ومن ثم تحديث شارة الدالة حتى تقبل مرجعًا قابلًا للتعديل بكتابة some_string: &mut String، إذ يدلنا هذا بكل وضوح على أن الدالة chang ستُعدّل من القيمة التي استعارتها. للمراجع القابلة للتعديل قيدٌ واحدٌ كبير، وهو أنه لا يمكنك الحصول على أكثر من مرجع إلى قيمة إذا كان لتلك القيمة مرجعًا قابلًا للتعديل. نُحاول في الشيفرة البرمجية التالية إنشاء مرجعين قابلَين للتعديل يشيران إلى s: اسم الملف: src/main.rs let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2); إليك الخطأ: $ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0499]: cannot borrow `s` as mutable more than once at a time --> src/main.rs:5:14 | 4 | let r1 = &mut s; | ------ first mutable borrow occurs here 5 | let r2 = &mut s; | ^^^^^^ second mutable borrow occurs here 6 | 7 | println!("{}, {}", r1, r2); | -- first borrow later used here For more information about this error, try `rustc --explain E0499`. error: could not compile `ownership` due to previous error يدل هذا الخطأ على أن الشيفرة البرمجية غير صالحة لأنه لا يُمكننا استعارة القيمة s على أنها قيمة قابلة للتعديل أكثر من مرة واحدة، إذ نستعير القيمة القابلة للتعديل للمرة الأولى في r1 ويجب أن نحافظ على الاستعارة حتى تُستخدم القيمة في !println، إلا أننا حاولنا أيضًا إنشاء مرجع قابل للتعديل آخر في r2 يستعير نفس البيانات الموجودة في r1 وذلك بين إنشاء المرجع القابل للتعديل الأول وبين استخدامه. يسمح القيد الذي يمنع وجود عدة مراجع قابلة للتعديل تشير لنفس البيانات في نفس الوقت بتعديل البيانات ولكن بطريقة مُقيّدة جدًا، وهو شيء يعاني منه معظم متعلمي لغة رست الجدُد وذلك لأن معظم اللغات تسمح لك بتعديل ما تشاء. الميزة من هذا القيد هو أن رست تمنع سباق البيانات data races عند وقت التصريف، وسباق البيانات هو مشابه لحالة السباق race condition ويحدث عند حدوث أحد الحالات الثلاث: محاولة مؤشرين أو أكثر الوصول إلى نفس البيانات في نفس الوقت. استخدام واحد من المؤشرات على الأقل للكتابة إلى البيانات. عدم وجود آلية مُستخدمة لمزامنة الوصول إلى البيانات. تتسبب سباقات البيانات بسلوك غير معرّف undefined behaviour وقد يكون تشخيص المشكلة وتصحيحها صعبًا عندما تحاول تتبع الخطأ عند وقت التشغيل، وتمنع رست هذه المشكلة برفض تصريف الشيفرة البرمجية التي تحتوي على سباقات البيانات. يُمكننا استخدام الأقواس المعقوصة curly brackets لإنشاء نطاق جديد، مما يسمح بوجود مراجع قابلة للتعديل، إلا أن الشرط هو عدم تواجد المراجع القابلة للتعديل ضمن النطاق في ذات الوقت: let mut s = String::from("hello"); { let r1 = &mut s; } // يخرج r1 من النطاق هنا وبالتالي يمكننا إنشاء مرجع جديد دون أي مشاكل let r2 = &mut s; تُجبرنا رست على قاعدة مشابهة لجمع المراجع القابلة وغير القابلة للتعديل. تولّد الشيفرة البرمجية التالية خطأً: let mut s = String::from("hello"); let r1 = &s; // لا توجد مشكلة let r2 = &s; // لا توجد مشكلة let r3 = &mut s; // هناك مشكلة كبيرة println!("{}, {}, and {}", r1, r2, r3); إليك الخطأ: $ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> src/main.rs:6:14 | 4 | let r1 = &s; // no problem | -- immutable borrow occurs here 5 | let r2 = &s; // no problem 6 | let r3 = &mut s; // BIG PROBLEM | ^^^^^^ mutable borrow occurs here 7 | 8 | println!("{}, {}, and {}", r1, r2, r3); | -- immutable borrow later used here For more information about this error, try `rustc --explain E0502`. error: could not compile `ownership` due to previous error كما أنه لا يمكننا إنشاء مرجع قابل للتعديل بينما لدينا مرجع غير قابل للتعديل يشير إلى القيمة ذاتها. لا يتوقع مستخدموا المراجع غير القابلة للتعديل بأن تتغير القيمة من تلقاء نفسها، لكن تسمح المراجع المتعددة غير القابلة للتعديل بذلك لأنه لا يوجد أي شيء يقرأ البيانات ولديه القدرة على التأثير على أي من المراجع التي تقرأ البيانات. لاحظ بأن نطاق المراجع يبدأ من مكان إنشائها ويستمر إلى آخر مكان استُخدم فيه المرجع، فعلى سبيل المثال، يمكن تصريف الشيفرة البرمجية التالية بنجاح لأن استخدام المراجع غير القابلة للتعديل الأخير في !println يحدث قبل إنشاء المرجع القابل للتعديل: let mut s = String::from("hello"); let r1 = &s; // لا يوجد مشكلة let r2 = &s; // لا يوجد مشكلة println!("{} and {}", r1, r2); // لن يُستخدم المتغيرين r1 و r2 بعد هذه النقطة let r3 = &mut s; // لا يوجد مشكلة println!("{}", r3); ينتهي نطاق كل من المرجعَين غير القابلَين للتعديل r1 و r2 بعد !println وهي آخر نقطة لاستخدامهما قبل إنشاء المرجع القابل للتعديل r3. لا تتداخل النطاقات ولذلك تكون الشيفرة البرمجية صالحة، وتدعى قدرة المصرف على معرفة المراجع التي لا تُستخدم بعد الآن في نهاية النطاق باسم دورات الحياة غير المُعجمية Non-Lexical Lifetimes -أو اختصارًا NLL -ويمكنك القراءة عنها من rust-lang.org. على الرغم من كون أخطاء الاستعارة أخطاءً مثيرةً للإحباط في بعض الأحيان، إلا أنه يجب عليك أن تتذكر أن مصرّف رست يُشير إلى خطأ قبل أوانه (عند وقت التصريف بدلًا من وقت التشغيل) ويوضح لك بالتفصيل مكان المشكلة حتى لا تضطر إلى تعقب المشكلة إذا كانت بياناتك تحتوي قيمًا مختلفة لتوقعاتك. المراجع المعلقة من السهل في اللغات التي تدعم المؤشرات أن تُنشئ مؤشرات معلقة dangling pointer بصورةٍ خاطئة؛ وهي مؤشرات تشير إلى موضع في الذاكرة مُعطًى لشخص آخر وذلك بتحرير المساحة مع المحافظة على المؤشر الذي يشير إلى الذاكرة، وهذا غير ممكن الحصول في رست، فعلى النقيض تمامًا يضمن المصرف ألا تكون جميع المراجع مُعلّقة، لأنه سيتأكد من عدم مغادرة البيانات التي يشير إليها المرجع النطاق قبل أن يُغادر المرجع النطاق أولًا. دعنا نجرّب إنشاء مرجع معلّق لملاحظة كيف تمنع رست وجودها بخطأ عند وقت التصريف: اسم الملف: src/main.rs fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s } إليك الخطأ: $ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0106]: missing lifetime specifier --> src/main.rs:5:16 | 5 | fn dangle() -> &String { | ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from help: consider using the `'static` lifetime | 5 | fn dangle() -> &'static String { | ~~~~~~~~ For more information about this error, try `rustc --explain E0106`. error: could not compile `ownership` due to previous error يُشير هذا الخطأ إلى الميزة التي لم نكتشفها بعد، ألا وهي دورات الحياة lifetimes، وسنناقشها بتوسُّع لاحقًا، ولكن إذا تغاضينا عن الجزء الخاص بدورات الحياة، فإن رسالة الخطأ تحتوي سبب المشكلة: this function's return type contains a borrowed value, but there is no value for it to be borrowed from دعنا نأخذ نظرةً أقرب لما يحدث في كل مرحلة من مراحل الدالة dangle: اسم الملف: src/main.rs fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { // تُعيد الدالة dangle مرجعًا إلى النوع String let s = String::from("hello"); // يشكل s السلسلة String الجديدة &s // نُعيد المرجع إلى النوع String واسمه s } // يخرج s من النطاق هنا ويُسقَط وتُحرّر ذاكرته. خطر! ستُحرّر s عند الانتهاء من تنفيذ الشيفرة البرمجية داخل الدالة dangle، لأن s مُنشأة بداخل dangle، إلا أننا حاولنا إعادة مرجع إليها وهذا يعني أن المرجع سيشير إلى قيمة String غير صالحة، وهذا ما يجب تجنُّب حدوثه، بالتالي لن تسمح لنا رست بفعل ذلك. يكمن الحل هنا بإعادة String مباشرةً: fn main() { let string = no_dangle(); } fn no_dangle() -> String { let s = String::from("hello"); s } يعمل هذا الحل دون أي مشاكل، إذ تُنقل الملكية ولا يُحرّر أي حيز من الذاكرة. قوانين المراجع دعنا نلخص أهم النقاط التي ناقشناها بخصوص المراجع: يُمكنك في نقطة من الزمن استخدام مرجع واحد قابل للتعديل أو عدة مراجع غير قابلة للتعديل. يجب أن تكون المراجع صالحة على الدوام. سننظر في الفقرات التالية إلى نوع مختلف من المراجع هو الشرائح slices. نوع الشريحة تسمح لك الشرائح بالإشارة reference إلى سلسلة متتابعة من العناصر ضمن تجميعة collection بدلًا من الإشارة إلى كامل التجميعة، وتشبه الشريحة المرجع ولذا فهو لا يحتوي على ملكية. إليك مشكلةً برمجيةً صغيرة: اكتب دالةً تأخذ سلسلةً نصيةً من كلمات مفصول ما بينها بمسافات وتُعيد الكلمة الأولى التي تجدها ضمن تلك السلسلة النصية، إذا لم تعثر الدالة على مسافة ضمن السلسلة النصية، فهذا يعني أن السلسلة النصية مؤلفةٌ من كلمة واحدة فقط وعليها أن تُعيد كامل السلسلة النصية. لنرى كيف يمكننا كتابة بصمة signature الدالة دون استخدام الشرائح، وذلك حتى نفهم المشكلة التي تحلها الشرائح: fn first_word(s: &String) -> ? للدالة first_word معامل &String، وهذا ما لا بأس فيه لأننا لا نحتاج الملكية عليه، لكن ما الذي يجب أن تُعيده الدالة؟ لا توجد لدينا أي طريقة لتحديد جزء من السلسلة النصية، إلا أننا نستطيع إعادة دليل index نهاية الكلمة بالاستفادة من المسافة، لنجرّب هذه الطريقة كما هو موضح في الشيفرة 4-7. اسم الملف: src/main.rs fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } [الشيفرة 4-7: الدالة first_word تُعيد قيمة دليل بحجم بايت إلى معامل String] علينا أن نحوّل String إلى مصفوفة من البايتات لأننا بحاجة للمرور بعناصر String عنصرًا تلو الآخر للبحث عن مسافات، وذلك باستخدام التابع as_bytes: let bytes = s.as_bytes(); من ثم نُنشئ مُكرّرًا iterator على مصفوفة البايتات باستخدام تابع iter: for (i, &item) in bytes.iter().enumerate() { سنُناقش المُكرّرات بالتفصيل لاحقًا، أما الآن فكل ما عليك معرفته هو أن iter تابع يُعيد كل عنصر في تجميعة، وأن enumerate يُغلّف wraps نتيجة iter ويُعيد كل عنصر على أنه جزءٌ من الصف tuple بدلًا من ذلك، إذ يمثّل العنصر الأول من الصف المُعاد من التابع enumerate دليلًا، بينما يمثل العنصر الثاني مرجعًا إلى العنصر في التجميعة، وهذه الطريقة أسهل من حساب أدلة العناصر بأنفسنا. يُمكننا استخدام الأنماط patterns لتفكيك الصف وذلك بالنظر إلى أن التابع enumerate يُعيد صفًا، وسنناقش الأنماط بالتفصيل لاحقًا. نُحدد في حلقة for النمط الذي يحتوي على i، والذي يمثل الدليل الموجود في الصف و &item للبايت الوحيد الموجود في الصف، ونستخدم & في النمط لأننا نحصل على مرجع للعنصر من iter().enumerate().. نبحث عن البايت الذي يمثل المسافة داخل حلقة for باستخدام البايت المجرّد، وإن وجدنا مسافةً نُعيد مكانها، وإلا فنعيد طول السلسلة النصية باستخدامs.len(): if item == b' ' { return i; } } s.len() لدينا طريقةٌ الآن لمعرفة دليل نهاية الكلمة الأولى في السلسلة النصية، إلا أن هناك مشكلةُ في هذه الطريقة؛ إذ أننا نُعيد النوع usize بصورةٍ منفردة إلا أنه ذو معنى فقط في سياق &String، بكلمات أخرى، لا يوجد أي ضمان أن القيمة ستكون صالحة في المستقبل نظرًا لأنها قيمة منفصلة عن String. ألقِ نظرةً على الشيفرة 4-8 التي تستخدم الدالة first_word من الشيفرة 4-7. اسم الملف: src/main.rs fn main() { let mut s = String::from("hello world"); let word = first_word(&s); // ستُخزّن القيمة 5 في word s.clear(); // تُفرغ هذه التعليمة السلسلة النصية وتجعلها مساوية إلى القيمة “” // للمتغير word القيمة 5 هنا إلا أنه لا يوجد أي سلسلة نصية تجعل من هذه القيمة ذات معنى، وبالتالي القيمة عديمة الفائدة! } [الشيفرة 4-8: تخزين القيمة من استدعاء الدالة first_word ومن ثمّ تغيير محتويات String] يُصرَّف البرنامج السابق دون أي أخطاء وسيعمل دون مشاكل إذا استخدمنا word بعد استدعاء s.clear()، وذلك لأن word ليست مرتبطة بحالة sإطلاقًا، إذ ما زالت تحتوي word على القيمة 5 حتى بعد استدعاء s.clear()، ويمكننا استخدام القيمة 5 مع المتغير s حتى نحاول استخراج الكلمة الأولى إلا أن ذلك سيتسبب بخطأ لأن محتويات s تغيرت منذ أن خزّننا 5 في word. تُعد مراقبة الدليل الموجود في word خشيةً من فقدان صلاحيته بالنسبة للمتغير s عمليةً رتيبةً ومعرضةً للخطأ، وستصبح أسوأ إذا كتبنا دالة second_word تكون بصمتها على النحو التالي: fn second_word(s: &String) -> (usize, usize) { الآن وبما أننا نتتبع دليل البداية والنهاية، فهذا يعني أنه لدينا قيم أكثر لحسابها من البيانات في حالة معينة، إلا أن هذه القيم غير مرتبطة بحالة ما إطلاقًا، أصبح لدينا الآن إذًا ثلاثة متغيرات غير مرتبطة مع بعضها بعضًا ويجب أن نربطها. لدى رست لحسن الحظ الحل لهذه المشكلة، وهي سلسلة الشرائح النصية string slices. شرائح السلاسل النصية شرائح السلاسل النصية هي مرجعٌ لجزء من النوع String وتُكتب بالشكل التالي: fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; } نستخدم المرجع hello الذي يمثّل مرجعًا لجزء من النوع String بدلًا من استخدام مرجع لكامل النوع، والجزء مُحدّد باستخدام [5..0] بت. نُنشئ هنا شرائحًا باستخدام مجال باستخدام الأقواس وذلك بتحديد دليل البداية ودليل النهاية بالشكل التالي: [starting_index..ending_index]؛ إذ يُمثل starting_index موضع بداية الشريحة؛ بينما يمثل ending_index الموضع الذي يلي موضع نهاية الشريحة. يُخزّن هيكل بيانات الشريحة داخليًا كلًا من موضع البداية وطول الشريحة، الذي تحصل عليه من طرح ending_index من starting_index، لذا في حالة let world = &s[6..11] نحصل على شريحة باسم world تحتوي على مؤشر يشير إلى البايت الموجود في الدليل 6 للسلسلة s بقيمة طول مساوية إلى 5. يوضح الشكل 6 العملية السابقة. [شكل 6: شريحة سلسلة نصية تُشير إلى جزء من String] يُمكنك إهمال دليل البدء قبل النقطتين .. في المجال إذا أردت البدء من الدليل صفر، أي أن الشريحتين التاليتن متماثلتان: let s = String::from("hello"); let slice = &s[0..2]; let slice = &s[..2]; كما يمكنك إهمال دليل النهاية إذا أردت أن تُضمّن السلسلة النصية إلى النهاية، أي أن الشريحتين التاليتين متماثلتان: let s = String::from("hello"); let len = s.len(); let slice = &s[3..len]; let slice = &s[3..]; أخيرًا، يمكنك إهمال كل من دليل البداية ودليل النهاية ضمن المجال إذا أردت الحصول على كامل السلسلة النصية، والشريحتان التاليتان متماثلتان: let s = String::from("hello"); let len = s.len(); let slice = &s[0..len]; let slice = &s[..]; ملاحظة: يجب أن تمثّل أدلّة شرائح السلاسل النصية محرفًا صالحًا من ترميز UTF-8، وإلا سيتوقف البرنامج ويعرض خطأً إذا حاولت إنشاء شريحة سلسلة نصية منتصف محرف متعدد البايتات multibyte character، ونفرض هنا في هذا القسم أن المحارف بترميز آسكي ASCII فقط بهدف البساطة، وسنناقش مفصّلًا التعامل مع محارف بترميز UTF-8 لاحقًا. بعد تعرّفنا لما سبق، دعنا نكتب دالة تُعيد شريحة نسميها first_word، والنوع الذي يمثّل شريحة السلسلة النصية هو str&: اسم الملف: src/main.rs fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() {} نحصل في البرنامج السابق على دليل نهاية الكلمة بصورةٍ مشابهة لما فعلناه في الشيفرة 4-7 وذلك بالبحث عن أوّل ظهور لمسافة، وعندما نجد هذه المسافة نُعيد شريحة سلسلة نصية باستخدام بداية السلسلة النصية مثل دليل بداية ودليل المسافة مثل دليل نهاية. نحصل على قيمة واحدة متعلقة بالبيانات التي لدينا بعد استدعاء الدالة first_word، وتتألف القيمة من مرجع إلى نقطة البداية لشريحة السلسلة النصية وعدد العناصر في تلك الشريحة. يمكننا أن نجعل الدالة second_word تُعيد شريحة أيضًا: fn second_word(s: &String) -> &str { أصبح لدينا الآن واجهة برمجية API واضحة صعب العبث فيها، إذ سيتأكد المصرّف من أن مراجع النوع String هي مراجع صالحة. أتتذكر الخطأ الذي واجهناه في البرنامج الموجود في الشيفرة 4-8 عندما حصلنا على دليل نهاية الكلمة الأولى ومن ثمّ مسحنا السلسلة النصية مما جعل الدليل غير صالح؟ كانت الشيفرة تلك غير صحيحة منطقيًا ولكننا لم نحصل على أي أخطاء مباشرة واضحة، وستظهر المشكلة لاحقًا إذا حاولت استخدام دليل السلسلة النصية الفارغة، إلا أن شرائح السلاسل النصية تجعل من هذا الخطأ مستحيلًا وستعلمنا بحدوث خطأ في شيفرتنا البرمجية في وقت مبكّر، وعلى سبيل المثال، نحصل على خطأ وقت التصريف إذا استخدمنا إصدار شريحة السلسلة النصية من الدالة first_word. اسم الملف: src/main.rs fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let mut s = String::from("hello world"); let word = first_word(&s); s.clear(); // خطأ! println!("the first word is: {}", word); } إليك خطأ التصريف: $ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> src/main.rs:18:5 | 16 | let word = first_word(&s); | -- immutable borrow occurs here 17 | 18 | s.clear(); // error! | ^^^^^^^^^ mutable borrow occurs here 19 | 20 | println!("the first word is: {}", word); | ---- immutable borrow later used here For more information about this error, try `rustc --explain E0502`. error: could not compile `ownership` due to previous error تذكّر من قواعد الاستعارة borrowing rules أنه لا يمكننا أخذ مرجع قابل للتعديل من شيء إذا كان لدينا مرجع غير قابل للتعديل لهذا الشيء مسبقًا، ولأن clear بحاجة لحذف محتويات String، فهي بحاجة للحصول على مرجع قابل للتعديل، يستخدم println! بعد الاستدعاء للدالة clear المرجع في word، لذلك لا بدّ للمرجع غير القابل للتعديل أن يكون صالحًا بحلول تلك النقطة ولذلك تمنع رست وجود مرجع قابل للتعديل في clear ومرجع غير قابل للتعديل في word في الوقت ذاته مما يتسبب بفشل عملية التصريف. لم تكتفي رست بجعل الواجهة البرمجية أسهل للتعامل بل أقصَت صنفًا كاملًا من الأخطاء ممكنة الحدوث عند وقت التصريف. السلاسل النصية المجردة هي شرائح تذكر أننا تحدثنا عن السلاسل النصية المجردة بكونها تُخزّن بداخل الملف التنفيذي الثنائي، ويمكننا الآن فهم السلاسل النصية المجردة بوضوح بما أننا نعرف الشرائح: let s = "Hello, world!"; نوع s هنا هو &str وهي شريحة تُشير إلى جزء محدد من الملف الثنائي، وهذا السبب في كون السلاسل النصية غير قابلة للتعديل، وبالتالي يكون المرجع &str مرجعًا غير قابل للتعديل. شرائح السلاسل النصية مثل معاملات تدلنا معرفة أنه يُمكننا أخذ شرائح من السلاسل النصية المجردة والقيم من النوع String على أنه نستطيع إجراء تحسين واحد إضافي على first_word وهو بصمة الدالة: fn first_word(s: &String) -> &str { قد يكتب مبرمج لغة رست خبير بصمة الدالة السابقة الموضحة في الشيفرة 4-9 بدلًا من ذلك والسبب في هذا هو أن البصمة السابقة تسمح لنا باستخدام الدالة ذاتها على قيمتَي &String و &str. fn first_word(s: &str) -> &str { [الشيفرة 4-9: تحسين دالة first_word باستخدام شريحة سلسلة نصية لنوع المُعامل s] يُمكننا تمرير شريحة السلسلة النصية مباشرةً في هذه الحالة، فعلى سبيل المثال يمكننا تمرير شريحة من النوع String أو مرجع إليه إذا كان لدينا النوع String في معاملات الدالة، وهذا الأمر ممكن بفضل ميزة التحصيل القسري deref corecions التي سنتكلم عنها بالتفصيل لاحقًا. يجعل تعريف الدالة لتأخذ شريحة سلسلة نصية بدلًا من مرجع إلى النوع String من واجهتنا البرمجية شاملة الاستخدام أكثر ومفيدةً دون خسارة أي من وظائفها. اسم الملف: src/main.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[..] } fn main() { let my_string = String::from("hello world"); // تعمل الدالة first_word على شرائح النوع String سواءً كانت شريحة جزئية أو شريحة تشكل كامل السلسلة let word = first_word(&my_string[0..6]); let word = first_word(&my_string[..]); // تعمل الدالة first_word على مراجع النوع String والمساوية إلى كامل شرائح String let word = first_word(&my_string); let my_string_literal = "hello world"; // تعمل الدالة first_word على شرائح سلاسل نصية مجردة سواءً كانت مجردة أو جزئية let word = first_word(&my_string_literal[0..6]); let word = first_word(&my_string_literal[..]); // بما أن السلاسل النصية المجردة هي شرائح سلاسل نصية بالأصل، فالتعليمة التالية تعمل أيضًا دون طريقة كتابة الشريحة let word = first_word(my_string_literal); } الشرائح الأخرى شرائح السلاسل النصية هي شرائح خاصة بالسلاسل النصية كما قد تتوقع، إلا أن هناك أنواع شرائح عامة أكثر، ألقِ نظرةً على المصفوفة التالية: let a = [1, 2, 3, 4, 5]; قد نحتاج لاستخدام مرجع يشير إلى جزء من مصفوفة بطريقة مماثلة للسلسلة النصية، ويمكننا تحقيق ذلك الأمر وفق ما يلي: let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]); للشريحة النوع &[i32] وتعمل بطريقة مماثلة لشريحة السلسلة النصية وذلك بتخزين مرجع للعنصر الأول وطول الشريحة، وستستخدم هذا النوع من الشرائح لكافة أنواع التجميعات الأخرى، وسنناقش هذه التجميعات بالتفصيل عندما نتحدث عن الأشعة لاحقًا. ترجمة -وبتصرف- لقسم من فصل Understanding Ownership من كتاب The Rust Programming Language اقرأ أيضًا المقال التالي: استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust المقال السابق: الملكية في لغة رست كيفية كتابة الدوال Functions والتعليقات Comments في لغة راست Rust أنواع البيانات Data Types في لغة رست Rust
-
نتحدث في هذا المقال عن مجموعة من الخصائص غير المرتبطة مع بعضها بعضًا مباشرةً ولكنها تصب في موضوع المكتبات والتعامل معها في لغة سي، وهي القفزات اللا محلية والتعامل مع الإشارات والدوال ذات العدد المتغير من الوسطاء، ونستعرض الدوال والأنواع والماكرو الموجودة بداخل ملفات الترويسة الموافقة لكل منها. القفزات اللا محلية تقدم القفزات اللا محلية non-local jumps طريقةً مشابهة لطريقة goto بالانتقال من دالة إلى أخرى. نلجأ إلى استخدام الماكرو setjmp والدالة longjmp لأن الأمر غير ممكن الحدوث باستخدام goto والعناوين labels إذ أن للعناوين نطاق scope داخل الدالة فقط، وتُعرف هذه الطريقة باسم goto اللا محلية أو القفزة اللا محلية. يصرح ملف الترويسة <setjmp.h> شيئًا يدعى jmp_buf، وهو اسم مستخدم في الماكرو والدالة لتخزين المعلومات الضرورية لإجراء القفزة، وتُكتب التصاريح على النحو التالي: #include <setjmp.h> int setjmp(jmp_buf env); void longjmp(jmp_buf env, int val); يُستخدم الماكرو setjmp لتهيئة قيمة jmp_buf ويُعيد القيمة صفر عند استدعائه الأولي، إلا أن الأمر غير الاعتيادي هنا، هو أنه يُعيد مجددًا قيمة غير صفرية لاحقًا عند استدعاء الدالة longjmp، وتكون القيمة غير الصفرية هذه مساويةً للقيمة المُمرّرة للدالة longjmp. لعل الأمر سيتضح لك بوضوح بعد المثال التالي: #include <stdio.h> #include <stdlib.h> #include <setjmp.h> void func(void); jmp_buf place; main(){ int retval; /* يُعيد الاستدعاء الأول القيمة 0، ويعيد استدعاء آخر للدالة longjmp قيمة غير صفرية */ if(setjmp(place) != 0){ printf("Returned using longjmp\n"); exit(EXIT_SUCCESS); } /* لن يُعيد الاستدعاء التالي أي قيمة لأنه يقفز مجددًا إلى الأعلى */ func(); printf("What! func returned!\n"); } void func(void){ /* العودة إلى main، ويبدو أن الاستدعاء الثاني لدالة setjmp يعيد القيمة 4 */ longjmp(place, 4); printf("What! longjmp returned!\n"); } [مثال 1] يمثل وسيط الدالة longjmp المسمى val القيمة المُعادة من الاستدعاء الثاني اللاحق لتعليمة الإعادة return ضمن الدالة setjmp، ويجب أن تكون هذه القيمة قيمة غير صفرية عادةً، وستغيّر القيمة إلى 1 إذا حاولت إعادة القيمة صفر باستخدام longjmp، وبذلك يمكننا معرفة فيما إذا كان استدعاء الدالة setjmp مباشرةً أو عن طريق استدعاء الدالة longjmp. تأثير الدالة setjmp غير محدد إذا لم يكن هناك أي استدعاء لها قبل استدعاء longjmp، وسيتسبب ذلك غالبًا بتوقف البرنامج. لا يُتوقّع من الدالة longjmp أن تُعيد قيمة بعد استدعائها مباشرةً. يكون لجميع الكائنات الممكن الوصول إليها من تعليمة الإعادة return داخل الدالة setjmp القيم السابقة المخزنة عند استدعاء longjmp عدا الكائنات ذات صنف التخزين التلقائي automatic storage class التي لا تحتوي على نوع "volatile"، وتكون قيمها غير محددة إذا تغيرت هذه الكائنات بين استدعاء setjmp واستدعاء longjmp. تُنفّذ الدالة longjmpعلى نحوٍ صحيح بخصوص المقاطعات interrupts والإشارات وأي دوال أخرى مرتبطة، ونحصل على سلوك غير معرف إذا حصل استدعاء longjmp باستخدام دالة نتج استدعائها عن إشارة وصلت بينما تُعالج إشارة أخرى. يُعدّ القفز إلى دالة غير فعالة باستخدام longjmp خطئًا فادحًا (ويُقصد بدالة غير فعالة أنها أعادت قيمة للتو، أو أن استدعاء longjmp آخر تحوّل إلى setjmp ضمن مجموعة من الاستدعاءات المترابطة nested calls). يصرّ المعيار على أن setjmp يجب أن تستخدم فقط مثل تعبير للتحكم في تعليمات if و switch و do و while و for (إضافةً إلى كونها التعليمة الوحيدة الموجودة في تعليمة تعبير)، وامتدادًا لهذه القاعدة، يمكن لاستدعاء setjmp (طالما يشكّل تعبير التحكم بأكمله كما ذكرنا سابقًا) أن يخضع للعامل !، أو أن يُقارن مباشرةً مع تعبير ثابت ذي قيمة عدد صحيح باستخدام إحدى العوامل العلاقيّة أو عوامل المساواة، ولا يجب استخدام أي تعابير معقدة أكثر من ذلك. إليك الأمثلة التالية: setjmp(place); /* تعليمة تعبير */ if(setjmp(place)) ... /* تعبير تحكم كامل */ if(!setjmp(place)) ... /* تعبير تحكم كامل */ if(setjmp(place) < 4) ... /* تعبير تحكم كامل */ if(setjmp(place)<;4 && 1!=2) ... /* ممنوع */ التعامل مع الإشارة تقدّم لنا دالتان إمكانية التعامل مع الأحداث غير المتزامنة؛ وتُعرف الإشارة signal بأنها شرط قد يحدث خلال تنفيذ البرنامج ويمكن تجاهله أو التعامل معه بصورةٍ خاصة أو استخدامه لإنهاء البرنامج كما هي الحالة الاعتيادية. تُرسل إحدى الدوال الإشارة بينما تُستخدم الأخرى لتحديد كيفية معالجة الإشارة، وقد تولّد الكثير من الإشارات من العتاد الصلب أو نظام التشغيل إضافةً إلى دوال إرسال الإشارات raise. الإشارات المُعرّفة في ملف الترويسة <signal.h>، هي: الإشارة SIGABRT: إنهاء غير اعتيادي للبرنامج، مثل الإنهاء الحاصل باستخدام الدالة abort (إبطال). الإشارة SIGFPE: عملية حسابية خاطئة، مثل التقسيم على الصفر أو الطفحان overflow (استثناء الفاصلة والأرقام العشرية Floating point exception). الإشارة SIGILL: العثور على "كائن برنامج غير صالح"، وهذا يعني غالبًا أن هناك تعليمات غير صالحة في البرنامج. (تعليمة غير صالحة Illegal instruction). الإشارة SIGINT: إشارة تفاعلية للفت الانتباه، وتولد هذه الإشارة على الأنظمة التفاعلية عادةً بكتابة مفتاح الهروب break-in في الطرفية terminal (مقاطعة Interrupt). الإشارة SIGSEGV: محاولة غير صالحة للوصول إلى مساحة تخزين، وتُسبب غالبًا بمحاولة تخزين قيمة في كائن مُشار إليه بمؤشر خاطئ. (انتهاك جزء segment violation). الإشارة SIGTERM: طلب إنهاء للبرنامج. (إنهاء Terminate). قد تمتلك بعض التنفيذات implementations بعض الإشارات الإضافية الزائدة عن الإشارات السابقة المعرفة في المعيار، وستبدأ أسماء الإشارات بالأحرف SIG وستمتلك قيمًا مميزة مختلفة عن القيم السابقة. تسمح لك الدالة signal بتحديد الفعل الذي تريد اتخاذه عند تلقي إشارة، وتُصطحب بحالة إشارة من الإشارات المذكورة سابقًا ومؤشر يشير إلى دالة تُنفّذ للتعامل مع الإشارة، وذلك بتغيير المؤشر وإعادة القيمة الأصلية، إذًا، نعرف الدالة كما يلي: #include <signal.h> void (*signal (int sig, void (*func)(int)))(int); يدل ما سبق على أن الدالة signal تُعيد مؤشرًا يشير إلى دالة أخرى وتأخذ الدالة الثانية وسيطًا واحدًا من نوع عدد صحيح وتُعيد void؛ بينما يكون الوسيط الثاني للدالة signal مؤشرًا يشير إلى دالة تعيد void بصورةٍ مشابهة، وتأخذ int وسيطًا لها. يمكن استخدام قيمتين مميزتين لوسيط لدالة func (دالة التعامل مع الإشارة)، ألا وهما SIG_DFL وهي معالج الإشارة الافتراضي الأولي و SIG_IGN الذي يُستخدم لتجاهل الإشارة، ويضبط التنفيذ حالة جميع الإشارات إلى واحدة من هذه القيمتين في بداية البرنامج. تُعاد قيمة func السابقة للإشارة إذ استدعيت signal بنجاح، وإلا فتُعاد SIG_ERR ويُضبط errno إلى قيمة. عند حصول حدث إشارة غير مُتجاهل، يُنفّذ أول signal(sig, SIG_DFL) يطابق الحالة وذلك إذا كانت الدالة func المترافقة تمثّل مؤشرًا يشير إلى دالة، وتتسبب تلك العملية بإعادة تشغيل معالج الإشارة إلى الإجراء الافتراضي ألا وهو إنهاء البرنامج، وإذا كانت الإشارة هي SIGILL فسيكون إعادة التشغيل معرفًا حسب التنفيذ، إذ قد تختار بعض التنفيذات حجب أي حالات أخرى من الإشارة عوضًا عن إعادة التشغيل. بعد ذلك، يُجرى استدعاء لدالة معالجة الإشارة، وسيعاود البرنامج عمله من نقطة حصول الحدث في معظم الحالات وذلك إذا أعادت الدالة قيمة بنجاح، إلا أننا نحصل على سلوك غير معرف إذا كانت قيمة sig مساويةً إلى SIGFPE (استثناء الفاصلة العائمة) أو أي استثناء حسابي معرف بحسب التنفيذ، والحل الأكثر استخدامًا لمعالج SIGFPE هو استدعاء إحدى الدوال: abort، أو exit، أو longjmp. يستعرض الجزء التالي استخدام الإشارة لتحقيق خروج أنيق من البرنامج عند تلقي مقاطعة أو إشارة "الانتباه التفاعلي interactive attention". #include <stdio.h> #include <stdlib.h> #include <signal.h> FILE *temp_file; void leave(int sig); main() { (void) signal(SIGINT,leave); temp_file = fopen("tmp","w"); for(;;) { /* افعل بعض الأشياء هنا */ printf("Ready...\n"); (void)getchar(); } /* لا يمكننا الوصول إلى هذه النقطة */ exit(EXIT_SUCCESS); } /* أغلق الملف tmp عند الحصول على SIGINT، لكن انتبه لأن استدعاء دوال المكتبات من معالج الإشارة غير مضمون العمل في جميع التنفيذات وهذا ليس ببرنامج متجاوب مع جميع التنفيذات بالضرورة */ void leave(int sig) { fprintf(temp_file,"\nInterrupted..\n"); fclose(temp_file); exit(sig); } [مثال 2] من الممكن للبرنامج أن يرسل إشارات إلى نفسه باستخدام دالة raise وهذا معرّف على النحو التالي: include <signal.h> int raise (int sig); تُرسل الإشارة sig في هذه الحالة إلى البرنامج. تُعيد التعليمة raise القيمة صفر في حال النجاح، وقيمة غير صفرية عدا ذلك، تُنفّذ دالة abort على النحو التالي: #include <signal.h> void abort(void) { raise(SIGABRT); } إذا حصلنا على إشارة لأي سبب كان -باستثناء استدعاء abort أو raise- فمن الممكن للدالة أن تستدعي فقط الإشارة أو أن تُسند قيمةً إلى كائن ساكن static متطاير volatile (مؤهل باستخدام volatile) من النوع sig_atomic_t، وهذا النوع مصرّحٌ في ملف الترويسة <signal.h>، وهو النوع الوحيد من الكائنات الممكن تعديله بأمان مثل كيان ذري atomic entity حتى مع وجود المقاطعات اللا متزامنة، وهذا قيدٌ مرهق مفروض من المعيار، الذي على سبيل المثال، يُبطل الدالة leave في مثالنا أعلاه، وعلى الرغم من أن الدالة ستعمل بصورةٍ صحيحة في بعض البيئات إلى أنها لا تتبع القوانين الصارمة الخاصة بالمعيار. أعداد متغيرة من الوسطاء غالبًا ما يكون تنفيذ دالة تأخذ عددًا غير معروفًا أو غير ثابت من الوسطاء محبّذًا عند كتابة الدالة، نذكر دالة printf على سبيل المثال التي سنتكلم عنها لاحقًا. يوضح المثال التالي تصريح دالة مشابهة. int f(int, ... ); int f(int, ... ) { . . . } int g() { f(1,2,3); } [مثال 3] علينا تضمين الدوال المصرح عنها ضمن ملف الترويسة <stdarg.h> لكي نستطيع الوصول إلى الوسطاء الموجودة بداخل الدالة المُستدعاة، ونحصل نتيجةً لذلك على نوع جديد يدعى va_list وثلاثة دوال تتعامل مع كائنات من هذا النوع وتدعى va_start و va_arg و va_end. علينا استدعاء va_start قبل محاولة الوصول إلى لائحة الوسطاء المتغيرة، وهي معرفة على النحو التالي: #include <stdarg.h> void va_start(va_list ap, parmN); يُهيّئ الماكرو va_start الوسيط ap بهدف الاستخدام اللاحق من قبل الدالتين va_arg و va_end، بينما يكون الوسيط الثاني للدالة va_start المسمّى parmN المعرّف identifier الذي يسمّي المعامل الذي يقع أقصى اليمين في لائحة المعاملات المتغيرة (أي المعامل الذي يقع قبل "…,")، ولا يجب التصريح عن المعرف parmN باستخدام صنف التخزين storage class من النوع register أو على أنه دالة أو نوع مصفوفة. يمكن الوصول إلى الوسطاء على نحوٍ تتابعي بعد التهيئة وذلك باستخدام الماكرو va_arg، وهذا غير مألوف لأن النوع المُعاد يحدّد باستخدام وسيط للماكرو. لاحظ أن ذلك مستحيل التنفيذ في دالة فعلية، ويمكن تنفيذه فقط باستخدام الماكرو، وهو معرّف على النحو التالي: #include <stdarg.h> type va_arg(va_list ap, type); سيتسبب كل استدعاء للماكرو السابق بالحصول على الوسيط التالي من لائحة الوسطاء بقيمة من النوع المُحدّد، ويجب للوسيط va_list أن يُهيّأ باستخدام va_start، ونحصل على سلوك غير معرّف إذا لم يكن الوسيط التالي من النوع المُحدّد. احذر من المشاكل التي قد تنتج من التحويلات الحسابية وتفاداها، إذ أن استخدام النوع char أو عدد صغير short وسيطًا ثانيًا للدالة va_arg خطأٌ واضح؛ لأن هذه الأنواع تُرقّى دائمًا إلى signed int أو unsigned int ويُحوّل float إلى double. لاحظ أن ترقية الكائنات المصرّحة عنها من الأنواع char و unsigned char و unsigned short وحقول البت عديمة الإشارة unsigned bitfields إلى النوع unsigned int الذي سيعقّد أكثر استخدام الدالة va_arg هو معرّف بحسب التنفيذ، وقد يكون ذلك هو السبب في الحصول على بعض المشاكل الخفية غير المتوقعة. نحصل على سلوك غير معرف أيضًا إذا استُدعيت الدالة va_arg ولم يكن هناك مزيدًا من الوسطاء. يجب أن يكون الوسيط type -في تعريفنا السابق لدالة va_arg- ممثلًا لاسم نوع يمكن تحويله إلى مؤشر يشير إلى كائن بإضافة المحرف "*" ببساطة (حتى يعمل الماكرو)، وذلك محقق للأنواع البسيطة مثل char (لأن char * يمثّل نوع مؤشر يشير إلى محرف)، لكن لن تعمل مصفوفة المحارف (لا يتحول النوع char [] إلى مؤشر يشير إلى مصفوفة محارف بإضافة "*" إليه). يمكن لحسن الحظ معالجة المصفوفات إذا ما تذكرنا أن اسم المصفوفة الذي يُستخدم وسيطًا فعليًا لاستدعاء الدالة يُحوّل إلى مؤشر، وبذلك فإن النوع الصحيح لوسيط من النوع "مصفوفة من المحارف" هو char *. تُستدعى الدالة va_end بعد معالجة جميع الوسطاء، وهذا سيمنع اللائحة va_list من أن تُستخدم بعد ذلك، ونحصل على سلوك غير معرف إذا لم تُستخدم الدالة va_end. يمكن إعادة قراءة كامل لائحة الوسطاء باستدعاء الدالة va_start مجددًا بعد استدعاء va_end، وتُصرّح الدالة va_end كما يلي: #include <stdarg.h> void va_end(va list ap); يوضح المثال التالي كيفية استخدام كل من va_start و va_arg و va_end ضمن دالة تُعيد أكبر قيم وسطائها التي تكون من نوع عدد صحيح. #include <stdlib.h> #include <stdarg.h> #include <stdio.h> int maxof(int, ...) ; void f(void); main(){ f(); exit(EXIT_SUCCESS); } int maxof(int n_args, ...){ register int i; int max, a; va_list ap; va_start(ap, n_args); max = va_arg(ap, int); for(i = 2; i <= n_args; i++) { if((a = va_arg(ap, int)) > max) max = a; } va_end(ap); return max; } void f(void) { int i = 5; int j[256]; j[42] = 24; printf("%d\n",maxof(3, i, j[42], 0)); } [مثال 4] ترجمة -وبتصرف- لقسم من الفصل Libraries من كتاب The C Book. اقرأ أيضًا المقال التالي: مقدمة عن التعامل مع الدخل والخرج I/O في لغة سي C المقال السابق: القيم الحدية والدوال الرياضية في لغة سي C التعامل مع المحارف والسلاسل النصية في لغة سي C التعامل مع المؤشرات Pointers في لغة سي C التعامل مع المحارف وضبط إعدادات التوطين localization في لغة سي C
-
المُلكية ownership هي مجموعة من القوانين التي تحدد كيف يُدير برنامج رست استخدام الذاكرة، ويتوجب على جميع البرامج أن تُدير الطريقة التي تستخدم فيها ذاكرة الحاسوب عند تشغيلها. تلجأ بعض لغات البرمجة إلى كانس المهملات garbage collector الذي يتفقد باستمرار الذاكرة غير المُستخدمة بعد الآن أثناء عمل البرنامج، بينما تعطي لغات البرمجة الأخرى مسؤولية تحديد الذاكرة وتحريرها للمبرمج مباشرةً، إلا أن رست تسلك طريقًا آخر ثالثًا؛ ألا وهو أن الذاكرة تُدار عبر نظام ملكية يحتوي على مجموعة من القوانين التي يتفقدها المصرّف، إذ لا يُصرّف البرنامج إذا حدث خرق لأحد هذه القوانين، ولن تبطئ أي من مزايا الملكية برنامجك عند تشغيله. سيتطلب مفهوم الملكية بعض الوقت للاعتياد عليه بالنظر إلى أنه مفهوم جديد للعديد من المبرمجين، إلا أنك ستجد قوانين نظام الملكية أكثر سهولة بالممارسة وستجدها من البديهيات التي تسمح لك بكتابة شيفرة برمجية آمنة وفعّالة، لذا لا تستسلم. ستحصل على أساس قوي في فهم المزايا التي تجعل من رست لغة فريدة فور فهمك للملكية، وستتعلم في هذا المقال مفهوم الملكية بكتابة بعض الأمثلة التي تركز على هيكل بيانات data structure شائع جدًا هو السلاسل النصية strings. المكدس والكومة لا تطلب معظم لغات البرمجة منك بالتفكير بالمكدس stack والكومة heap بصورةٍ متكررة عادةً، إلا أن هذا الأمر مهم في لغات برمجة النظم مثل رست، إذ يؤثر وجود القيمة في المكدس أو الكومة على سلوك اللغة واتخاذها لبعض القرارات المعينة، وسنصف أجزاءً من نظام الملكية بما يتعلق بالكومة والمكدس لاحقًا، وسنستعرض هنا مفهومي -الكومة والمكدس- ونشرحهما للتحضير لذلك. يمثل كلًا من المكدس والكومة أجزاءً متاحةً من الذاكرة لشيفرتك البرمجية حتى تستخدمها عند وقت التشغيل runtime، إلا أنهما مُهيكلان بطريقة مختلفة؛ إذ يُخزن المكدس القيم بالترتيب الذي وردت فيه ويُزيل القيم بالترتيب المعاكس، ويُشار إلى ذلك بمصطلح الداخل آخرًا، يخرج أولًا last in, first out. يمكنك التفكير بالمكدس وكأنه كومة من الأطباق، فعندما تضيف المزيد من الأطباق، فإنك تضيفها على قمة الكومة وعندما تريد إزالة طبق فعليك إزالة واحد من القمة، ولا يُمكنك إزالة الأطباق من المنتصف أو من القاع. تُسمى عملية إضافة البيانات بالدفع إلى المكدس pushing onto the stack بينما تُدعى عملية إزالة البيانات بالإزالة من المكدس popping off the stack، ويجب على جميع البيانات المخزنة في المكدس أن تكون من حجم معروف وثابت، بينما تُخزّن البيانات ذات الحجم غير المعروف عند وقت التصريف أو ذات الحجم الممكن أن يتغير في الكومة بدلًا من ذلك. الكومة هي الأقل تنظيمًا إذ يمكنك طلب مقدار معين من المساحة عند تخزين البيانات إليها، وعندها يجد محدد المساحة memory allocator حيزًا فارغًا في الكومة يتسع للحجم المطلوب، يعلّمه على أنه حيّز قيد الاستعمال، ثم يُعيد مؤشرًا pointer يشير إلى عنوان المساحة المحجوزة، وتدعى هذه العملية بحجز مساحة الكومة allocating on the heap وتُدعى في بعض الأحيان بحجز المساحة فقط (لا تُعد عملية إضافة البيانات إلى المكدس عملية حجز مساحة). بما أن المؤشر الذي يشير إلى الكومة من حجم معروف وثابت، يمكنك تخزينه في المكدس، وعندما تريد البيانات الفعلية من الكومة يجب عليك تتبع المؤشر. فكر بالأمر وكأنه أشبه بالجلوس في مطعم، إذ تصرّح عن عدد الأشخاص في مجموعتك عند دخولك إلى المطعم، ثم يبحث كادر المطعم عن طاولة تتسع للجميع ويقودك إليها، وإذا تأخر شخص ما عن المجموعة يمكنه سؤال كادر المطعم مجددًا ليقوده إلى الطاولة. الدفع إلى المكدس أسرع من حجز الذاكرة في الكومة، لأن محدد المساحة لا يبحث عن مكان للبيانات الجديدة وموقع البيانات المخزنة، فهو دائمًا على قمة المكدس، بالمثل، يتطلب حجز المساحة في الكومة مزيدًا من العمل، إذ يجب على محدد المساحة البحث عن حيز كبير بما فيه الكفاية ليتسع البيانات ومن ثم حجزها للتحضير لعملية حجز المساحة التالية. الوصول إلى البيانات من الكومة أبطأ من الوصول إلى البيانات من المكدس وذلك لأنه عليك أن تتبع المؤشر لتصل إلى حيز الذاكرة، وتؤدي المعالجات المعاصرة عملها بصورةٍ أسرع إذا انتقلت من مكان إلى آخر ضمن الذاكرة بتواتر أقل. لنبقي على تقليد التشابيه، فكر بالأمر وكأن النادل في المطعم يأخذ الطلبات من العديد من الطاولات، وفي هذه الحالة فمن الأفضل أن يحصل النادل على جميع الطلبات في الطاولة الواحدة قبل أن ينتقل إلى الطاولة التي تليها، فأخذ الطلب من الطاولة (أ) ومن ثم أخذ الطلب من الطاولة (ب) ومن ثم العودة إلى الطاولة (أ) والطاولة (ب) مجددًا عملية أبطأ بكثير، وبالمثل فإن المعالج يستطيع إنجاز عمله بصورةٍ أفضل إذا تعامل مع البيانات القريبة من البيانات الأخرى (كما هو الحال في المكدس) بدلًا من العمل على بيانات بعيدة عن بعضها (مثل الكومة). عندما تستدعي شيفرتك البرمجية دالةً ما، يُدفع بالقيم المُمررة إليها إلى المكدس (بما فيها المؤشرات إلى البيانات الموجودة في الكومة) إضافةً إلى متغيرات الدالة المحلية، وعندما ينتهي تنفيذ الدالة، تُزال هذه القيم من المكدس. يتكفل نظام الملكية بتتبع الأجزاء التي تستخدم البيانات من الكومة ضمن شيفرتك البرمجية، وتقليل كمية البيانات المُكررة ضمن الكومة، وإزالة أي بيانات غير مُستخدمة منها حتى لا تنفد من المساحة. عندما تفهم نظام الملكية لن تحتاج للتفكير بالمكدس والكومة كثيرًا، فكل ما عليك معرفته هو أن الهدف من نظام الملكية هو إدارة بيانات الكومة، وسيساعدك هذا الأمر في فهم طريقة عمل هذا النظام. قوانين الملكية دعنا نبدأ أولًا بالنظر إلى قوانين الملكية، أبقِ هذه القوانين في ذهنك بينما تقرأ بقية المقال الذي يستعرض أمثلةً توضح هذه القوانين: لكل قيمة في رست مالك owner. يجب أن يكون لكل قيمة مالك واحد في نقطة معينة من الوقت. تُسقط القيمة عندما يخرج المالك من النطاق scope. نطاق المتغير الآن وبعد تعرفنا إلى مبادئ رست في المقالات السابقة، لن نُضمّن الشيفرة fn main() { في الأمثلة، لذا إذا كنت تتبع الأمثلة تأكد من أنك تكتب الشيفرة البرمجية داخل دالة main يدويًا، ونتيجةً لذلك ستكون أمثلتنا أقصر بعض الشيء مما سيسمح لنا بالتركيز على التفاصيل المهمة بدلًا من الشيفرة البرمجية النمطية المتكررة. سننظر إلى نطاق المتغيرات في أول مثال من أمثلة الملكية، والنطاق هو مجال ضمن البرنامج يكون فيه العنصر صالحًا. ألقِ نظرةً على المتغير التالي: let s = "hello"; يُشير المتغير s إلى السلسلة النصية المجردة، إذ أن قيمة السلسلة النصية مكتوبة بصورةٍ صريحة على أنها نص في برنامجنا، والمتغير هذا صالح من نقطة التصريح عنه إلى نهاية النطاق الحالي. توضح الشيفرة 4-1 البرنامج مع تعليقات توضح مكان صلاحية المتغير s. { // المتغير غير صالح هنا إذ لم يُصرّح عنه بعد let s = "hello"; // المتغير صالح من هذه النقطة فصاعدًا // يمكننا استخدام s في العمليات هنا } // انتهى النطاق بحلول هذه النقطة ولا يمكننا استخدام s [الشيفرة 4-1: متغير والنطاق الذي يكون فيه صالحًا] بكلمات أخرى، هناك نقطتان مهمتان حاليًا: عندما يصبح المتغير s ضمن النطاق، يصبح صالحًا. يبقى المتغير صالحًا حتى مغادرته النطاق. لحد اللحظة، العلاقة بين النطاقات والمتغيرات هي علاقة مشابهة للعلاقة التي تجدها في لغات البرمجة الأخرى، وسنبني على أساس هذا الفهم النوع String. النوع String نحتاج نوعًا أكثر تعقيدًا من الأنواع التي غطيناها سابقًا وذلك لتوضيح قوانين الملكية، إذ كانت الأنواع السابقة جميعها من أحجام معروفة ويمكن تخزينها في المكدس وإزالتها عند انتهاء نطاقها، كما أنه من الممكن نسخها بكل سهولة إلى متغير آخر جديد يمثّل نسخةً مستقلةً وذلك إذا احتاج حزءٌ ما من الشيفرة البرمجية استخدام المتغير ذاته ضمن نطاق آخر، إلا أننا بحاجة إلى النظر لأنواع البيانات المخزنة في الكومة حتى نكون قادرين على معرفة ما تفعله رست لتنظيف البيانات هذه ويمثل النوع String مثالًا رائعًا لهذا الاستخدام. سنركز على أجزاء النوع String التي ترتبط مباشرةً بالملكية، وتنطبق هذه الجوانب أيضًا على أنواع البيانات المعقدة الأخرى سواءٌ كانت هذه الأنواع موجودةً في المكتبة القياسية أو كانت مبنيةً من قبلك، وسنناقش النوع String بتعمّق أكبر لاحقًا. رأينا مسبقًا السلاسل النصية المجردة (وهي السلاسل النصية المكتوبة بين علامتي تنصيص " " بصورةٍ صريحة)، إذ تُكتب قيمة السلسلة النصية يدويًا إلى البرنامج. السلاسل النصية المجردة مفيدةٌ إلا أنها غير مناسبة لكل الحالات، مثل تلك التي نريد فيها استخدام النص ويعود السبب في ذلك إلى أنها غير قابلة للتعديل immutable، والسبب الآخر هو أنه لا يمكننا معرفة قيمة كل سلسلة نصية عندما نكتب شيفرتنا البرمجية، فعلى سبيل المثال، ماذا لو أردنا أخذ الدخل من المستخدم وتخزينه؟ تملك رست لمثل هذه الحالات نوع سلسلة نصية آخر يدعى String، ويدير هذا النوع البيانات باستخدام الكومة، وبالتالي يمكنه تخزين كمية غير معروفة من النص عند وقت التصريف. يُمكنك إنشاء String من سلسلة نصية مجردة باستخدام دالة from كما يلي: let s = String::from("hello"); يسمح لنا عامل النقطتين المزدوجتين :: بتسمية الدالة الجزئية from ضمن فضاء الأسماء namespace وأن تندرج تحت النوع String بدلًا من استخدام اسم مشابه مثل string_from، وسنناقش هذه الطريقة في الكتابة أكثر لاحقًا، بالإضافة للتكلم عن فضاءات الأسماء وإنشائها. يُمكن تعديل mutate هذا النوع من السلاسل النصية: let mut s = String::from("hello"); s.push_str(", world!"); // يُضيف ()push_str سلسلةً نصية مجردة إلى النوع String println!("{}", s); // سيطبع هذا السطر `!hello, world` إذًا، ما الفارق هنا؟ كيف يمكننا تعديل النوع String بينما لا يمكننا تعديل السلاسل النصية المجردة؟ الفارق هنا هو بكيفية تعامل كل من النوعين مع الذاكرة. الذاكرة وحجزها نعرف محتويات السلسلة النصية في حال كانت السلسلة النصية مجرّدة عند وقت التصريف، وذلك لأن النص مكتوب في الشيفرة البرمجية بصورةٍ صريحة في الملف النهائي التنفيذي، وهذا السبب في كون السلاسل النصية المجردة سريعة وفعالة، إلا أن هذه الخصائص تأتي من حقيقة أن السلاسل النصية المجردة غير قابلة للتعديل immutable، ولا يمكننا لسوء الحظ أن نضع جزءًا من الذاكرة في الملف التنفيذي الثنائي لكل قطعة من النص، وذلك إذا كان النص ذو حجم غير معلوم عند وقت التصريف كما أن حجمه قد يتغير خلال عمل البرنامج. نحتاج إلى تحديد مساحة من الذاكرة ضمن الكومة عند استخدام نوع String وذلك لدعم إمكانية تعديله وجعله سلسلةً نصيةً قابلة للزيادة والنقصان، بحيث تكون هذه المساحة التي تخزن البيانات غير معلومة الحجم عند وقت التصريف، وهذا يعني: يجب أن تُطلب الذاكرة من مُحدد الذاكرة عند وقت التشغيل. نحتاج طريقة لإعادة الذاكرة إلى محدد الذاكرة عندما ننتهي من استخدام String الخاص بنا. يُنجز المتطلّب الأول عن طريق استدعاء String::from، إذ تُطلب الذاكرة التي يحتاجها ضمنيًا، وهذا الأمر موجود في معظم لغات البرمجة. أما المتطلب الثاني فهو مختلفٌ بعض الشيء، إذ يراقب كانس المهملات -أو اختصارًا GC- الذاكرة غير المُستخدمة بعد الآن ويحررها، ولا حاجة للمبرمج بالتفكير بهذا الأمر، بينما تكون مسؤوليتنا في اللغات التي لا تحتوي على كانس المهملات هي العثور على المساحة غير المُستخدمة بعد الآن وأن نستدعي الشيفرة البرمجية بصورةٍ صريحة لتحرير تلك المساحة، كما هو الحال عندما استدعينا شيفرة برمجية لحجزها، ولطالما كانت هذه المهمة صعبة على المبرمجين، فإذا نسينا تحرير الذاكرة فنحن نهدر الذاكرة وإذا حررنا الذاكرة مبكرًا فهذا يعني أن قيمة المتغير أصبحت غير صالحة للاستخدام، بينما نحصل على خطأ إذا حررنا الذاكرة نفسها لأكثر من مرة، إذ علينا استخدام تعليمة allocate واحدة فقط مصحوبةً مع تعليمة free واحدة لكل حيز ذاكرة نستخدمه. تسلك لغة رست سلوكًا مختلفًا، إذ تُحرر الذاكرة أوتوماتيكيًا عندما يغادر المتغير الذي يملك تلك الذاكرة النطاق. إليك إصدارًا من الشيفرة 4-1 نستخدم فيه النوع String بدلًا من السلسلة النصية المجردة: { let s = String::from("hello"); // المتغير s صالح من هذه النقطة فصاعدًا // يمكننا إنجاز العمليات باستخدام المتغير s هنا } // انتهى النطاق ولم يعد المتغير s صالحًا نستعيد الذاكرة التي يستخدمها String من محدد الذاكرة عندما يخرج المتحول s من النطاق، إذ تستدعي رست دالةً مميزةً بالنيابة عنا عند خروج متحول ما من النطاق وهذه الدالة هي drop، وتُستدعى تلقائيًا عند الوصول إلى قوس الإغلاق المعقوص {. ملاحظة: يُدعى نمط تحرير الموارد في نهاية دورة حياة العنصر في لغة C++ أحيانًا "اكتساب الموارد هو تهيئتها Resource Acquisition Is Initialization" -أو اختصارًا RAII- ودالة drop في رست هي مشابهة لأنماط RAII التي قد استخدمتها سابقًا. لهذا النمط تأثير كبير في طريقة كتابة شيفرة رست البرمجية، وقد يبدو بسيطًا للوقت الحالي إلا أن سلوك الشيفرة البرمجية قد يكون غير متوقعًا في الحالات الأكثر تعقيدًا عندما يوجد عدة متغيرات تستخدم البيانات المحجوزة على الكومة، دعنا ننظر إلى بعض من هذه الحالات الآن. طرق التفاعل مع البيانات والمتغيرات: النقل يُمكن لعدة متغيرات أن تتفاعل مع نفس البيانات بطرق مختلفة في رست، دعنا ننظر إلى الشيفرة 4-2 على أنها مثال يستخدم عددًا صحيحًا. let x = 5; let y = x; [الشيفرة 4-2: إسناد قيمة العدد الصحيح إلى المتغيرين x و y] يمكنك غالبًا تخمين ما الذي تؤديه الشيفرة البرمجية السابقة: إسناد القيمة 5 إلى x ومن ثم نسخ القيمة x وإسنادها إلى القيمة y، وبالتالي لدينا متغيرين x و y وقيمة كل منهما تساوي إلى 5، وهذا ما يحدث فعلًا، لأن الأعداد الصحيحة هي قيم بسيطة بحجم معروف وثابت وبالتالي يُمكن إضافة القيمتين 5 إلى المكدس. لننظر الآن إلى إصدار String من الشيفرة السابقة: let s1 = String::from("hello"); let s2 = s1; تبدو الشيفرة البرمجية هذه شبيهة بسابقتها، وقد نفترض هنا أنها تعمل بالطريقة ذاتها، ألا وهي: ينسخ السطر الثاني القيمة المخزنة في المتغير s1 ويُسندها إلى s2 إلا أن هذا الأمر غير صحيح. انظر إلى الشكل 1 لرؤية ما الذي يحصل بدقة للنوع String، إذ يتكون هذا النوع من ثلاثة أجزاء موضحة ضمن الجدول اليساري وهي المؤشر ptr الذي يشير إلى الذاكرة التي تُخزن السلسلة النصية وطول السلسلة النصية len وسعتها capacity، وتُخزّن مجموعة المعلومات هذه في المكدس، بينما يمثّل الجدول اليميني الذاكرة في الكومة التي تخزن محتوى السلسلة النصية. [شكل 1: مخطط توضيحي لما تبدو عليه الذاكرة عند استخدام String يخزن القيمة "hello" المُسندة إلى s1] يدل الطول على كمية الذاكرة المُستهلكة بالبايت وهي الحيز الذي يشغله محتوى String، بينما تدل السعة على كمية الذاكرة المستهلكة بالكامل التي تلقّاها String من مُحدد الذاكرة، والفرق بين الطول والسعة مهم، إلا أننا سنهمل السعة لأنها غير مهمة في السياق الحالي. تُنسخ بيانات String عندما نُسند s1 إلى s2، وهذا يعني أننا ننسخ المؤشر والطول والسعة الموجودين في المكدس ولا ننسخ البيانات الموجودة في الكومة التي يشير إليها المؤشر، بكلمات أخرى، يبدو تمثيل الذاكرة بعد النسخ كما هو موضح في الشكل 2. [شكل 2: تمثيل الذاكرة للمتغير s2 الذي يحتوي على نسخة من مؤشر وطول وسعة s1] تمثيل الذاكرة غير مطابق للشكل 3 وقد ينطبق هذا التمثيل إذا نسخت رست محتويات بيانات الكومة أيضًا، وإذا فعلت رست ذلك، فستكون عملية الإسناد s2 = s1 عمليةً مكلفةً وستؤثر سلبًا على أداء وقت التشغيل إذا كانت البيانات الموجودة في الكومة كبيرة. [شكل 3: احتمال آخر لما قد تبدو عليه الذاكرة بعد عملية الإسناد s2 = s1 وذلك إذا نسخت رست محتويات الكومة أيضًا] قلنا سابقًا أن رست تستدعي الدالة drop تلقائيًا عندما يغادر متغيرٌ ما النطاق، وتحرّر الذاكرة الموجودة في الكومة لذلك المتغير، إلا أن الشكل 2 يوضح أن كلا المؤشرين يشيران إلى الموقع ذاته، ويمثّل هذا مشكلةً واضحة، إذ عندما يغادر كلًا من s1 وs2 النطاق، فهذا يعني أن الذاكرة في الكومة ستُحرّر مرتين، وهذا خطأ تحرير ذاكرة مزدوج double free error شائع، وهو خطأ من أخطاء أمان الذاكرة الذي ذكرناه سابقًا، إذ يؤدي تحرير الذاكرة نفسها مرتين إلى فساد في الذاكرة مما قد يسبب ثغرات أمنية. تنظر رست إلى s1 بكونه غير صالح بعد السطر let s2 = s1 وذلك لضمان أمان الذاكرة، وبالتالي لا يتوجب على رست تحرير أي شيء عندما يغادر المتحول s1 النطاق. انظر ما الذي يحدث عندما نحاول استخدام s1 بعد إنشاء s2 (لن تعمل الشيفرة البرمجية): let s1 = String::from("hello"); let s2 = s1; println!("{}, world!", s1); سنحصل على خطأ شبيه بالخطأ التالي لأن رست يمنعك من استخدام المرجع غير الصالح بعد الآن: $ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0382]: borrow of moved value: `s1` --> src/main.rs:5:28 | 2 | let s1 = String::from("hello"); | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait 3 | let s2 = s1; | -- value moved here 4 | 5 | println!("{}, world!", s1); | ^^ value borrowed here after move | = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0382`. error: could not compile `ownership` due to previous error لعلك سمعت بمصطلح النسخ السطحي shallow copy والنسخ العميق deep copy خلال عملك على لغة برمجة أخرى؛ إذ يُشير مصطلح النسخ السطحي إلى عملية نسخ مؤشر وطول وسعة السلسلة النصية دون البيانات الموجودة في الكومة، إلا أن رست تسمّي هذه العملية بالنقل move لأنها تُزيل صلاحية المتغير الأول. في هذا المثال، نقول أن s1 نُقِلَ إلى s2، والنتيجة الحاصلة موضحة في الشكل 4. [شكل 4: تمثيل الذاكرة بعد إزالة صلاحية المتغير s1] يحلّ هذا الأمر مشكلتنا، وذلك بجعل المتغير s2 صالحًا فقط، وعند مغادرته للنطاق فإن المساحة تُحرر بناءً عليه فقط. إضافةً لما سبق، هناك خيارٌ تصميمي ملمّح إليه بواسطة هذا الحل، ألا وهو أن رست لن تُنشئ نُسَخًا عميقة من بياناتك تلقائيًا، وبالتالي لن يكون أي نسخ تلقائي مكلفًا بالنسبة لأداء وقت التشغيل. طرق التفاعل مع البيانات والمتغيرات: الاستنساخ يُمكننا استخدام تابع شائع يدعى clone إذا أردنا نسخ البيانات الموجودة في الكومة التي تعود للنوع String نسخًا عميقًا إضافةً لبيانات المكدس، وسنناقش طريقة كتابة التوابع لاحقًا، إلا أنك غالبًا ما رأيت استخدامًا للتوابع مسبقًا بالنظر إلى أنها شائعة في العديد من لغات البرمجة. إليك مثالًا عمليًا عن تابع clone: let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); تعمل الشيفرة البرمجية بنجاح، وتولد النتيجة الموضحة في الشكل 3، إذ تُنسخ محتويات الكومة. عند رؤيتك لاستدعاء التابع clone، عليك أن تتوقع تنفيذ شيفرة برمجية إضافية وأن الشيفرة البرمجية ستكون مكلفة التنفيذ، وأن استخدام التابع هو دلالة بصرية أيضًا على حدوث شيء مختلف. نسخ بيانات المكدس فقط هناك تفصيلٌ آخر لم نتكلم بخصوصه بعد. تستخدم الشيفرة البرمجية التالية (جزء من الشيفرة 4-2) أعدادًا صحيحة، وهي شيفرة برمجية صالحة: let x = 5; let y = x; println!("x = {}, y = {}", x, y); إلا أن الشيفرة البرمجية تبدو مناقضة لما تعلمنا مسبقًا، إذ أن x صالح ولم يُنقل إلى المتغير y على الرغم من عدم استدعائنا للتابع clone. السبب في هذا هو أن الأنواع المشابهة للأعداد الصحيحة المؤلفة من حجم معروف عند وقت التصريف تُخزن كاملًا في المكدس، ولذلك يمكننا نسخها فعليًا بصورةٍ أسرع، ولا فائدة في منع x من أن يكون صالحًا في هذه الحالة بعد إنشاء المتغير y، وبكلمات أخرى ليس هناك أي فرق بين النسخ السطحي والعميق هنا، لذا لن يُغيّر استدعاء التابع clone أي شيء مقارنةً بالنسخ السطحي الاعتيادي، ولذلك يمكننا الاستغناء عنه. لدى لغة رست طريقةً مميزةً تدعى سمة trait النسخ Copy، التي تمكّننا من وضعها على الأنواع المخزنة في المكدس كما هو الحال في الأعداد الصحيحة (سنتكلم بالتفصيل عن السمات لاحقًا). إذا استخدم نوعٌ ما السمة Copy، فهذا يعني أن جميع المتغيرات التي تستخدم هذا النوع لن تُنقل وستُنسخ بدلًا من ذلك مما يجعل منها صالحة حتى بعد إسنادها إلى متغير آخر. لن تسمح لنا رست بتطبيق السمة Copy إذا كان النوع -أو أي من أجزاء النوع- يحتوي على السمة Drop، إذ أننا سنحصل على خطأ وقت التصريف إذا كان النوع بحاجة لشيء مميز للحدوث عند خروج القيمة من النطاق وأضفنا السمة Copy إلى ذلك النوع، إن أردت تعلم المزيد عن إضافة السمة Copy إلى النوع لتطبيقها، ألقِ نظرةً على الملحق (ت) قسم السمات المُشتقة derivable traits. إذًا، ما هي الأنواع التي تقبل تطبيق السمة Copy؟ يمكنك النظر إلى توثيق النوع للتأكد من ذلك، لكن تنص القاعدة العامة على أن أي مجموعةٍ من القيم البسيطة المُفردة تقبل السمة Copy، إضافةً إلى أي شيء لا يتطلب تحديد الذاكرة، أو ليس أي نوع من أنواع الموارد. إليك بعض الأنواع التي تقبل تطبيق Copy: كل أنواع الأعداد الصحيحة مثل u32. الأنواع البوليانية bool التي تحمل القيمتين true و false. جميع أنواع أعداد الفاصلة العشرية مثل f64. نوع المحرف char. الصفوف tuples إذا احتوى الصف فقط على الأنواع التي يمكن تطبيق Copy عليها، على سبيل المثال يُمكن تطبيق Copy على (i32, i32)، بينما لا يمكن تطبيق Copy على (i32, String). الملكية والدوال تشبه طريقة تمرير قيمة إلى دالة إسناد assign قيمة إلى متغير، إذ أن تمرير القيمة إلى المتغير سينقلها أو ينسخها كما هو الحال عند إسناد القيمة، توضح الشيفرة 4-3 مثالًا عن بعض الطرق التي توضح أين يخرج المتغير من النطاق. اسم الملف: src/main.rs fn main() { let s = String::from("hello"); // يدخل المتغير s إلى النطاق takes_ownership(s); // تُنقل قيمة s إلى الدالة ولا تعود صالحة للاستخدام هنا let x = 5; // يدخل المتغير x إلى النطاق makes_copy(x); // يُنقل المتغير x إلى الدالة إلا أن i32 تملك السمة Copy لذا من الممكن استخدام المتغير x بعد هذه النقطة } // يغادر المتحول x خارج النطاق ولا شيء مميز يحدث لأن قيمة s نُقِلَت fn takes_ownership(some_string: String) { // يدخل المتغير some_string إلى النطاق println!("{}", some_string); } // يُغادر المتغير some_string النطاق هنا وتُستدعى drop، وتُحرر الذاكرة الخاصة بالمتغير fn makes_copy(some_integer: i32) { // يدخل المتغير some_integer إلى النطاق println!("{}", some_integer); } // يغادر المتغير some_integer النطاق ولا يحصل أي شيء مثير للاهتمام [الشيفرة 4-3: استخدام الدوال مع الملكية والنطاق] ستعرض لنا رست خطأً عند وقت التصريف إذا حاولنا استخدام a بعد استدعاء takes_ownership، ويحمينا هذا التفقد الساكن static من بعض الأخطاء. حاول إضافة شيفرة برمجية تستخدم s و x إلى الدالة main ولاحظ أين يمكنك استخدامهما وأين تمنعك قوانين الملكية من استخدامهما. القيم المعادة والنطاق يُمكن أن تحول عملية إعادة القيمة ملكيتها أيضًا، توضح الشيفرة 4-4 مثالًا عن دالة تُعيد قيمة بصورةٍ مشابهة للشيفرة 4-3. اسم الملف: src/main.rs fn main() { let s1 = gives_ownership(); // تنقل الدالة gives_ownership قيمتها المُعادة إلى s1 let s2 = String::from("hello"); // يدخل المتغير s2 إلى النطاق let s3 = takes_and_gives_back(s2); // يُنقل المتغير s2 إلى takes_and_gives_back الذي ينقل قيمته المعادة بدوره إلى المتغير s3 } // يُغادر s3 النطاق من هنا ويُحرر من الذاكرة باستخدام drop، ولا يحصل أي شيء للمتغير s2 لأنه نُقل، بينما يغادر s1 النطاق أيضًا ويُحرر من الذاكرة باستخدام drop fn gives_ownership() -> String { // تنقل الدالة gives_ownership قيمتها المعادة إلى الدالة التي استدعتها let some_string = String::from("yours"); // يدخل المتغير some_string إلى النطاق some_string // يُعاد some_string ويُنقل إلى الدالة المُستدعاة } // تأخذ هذه الدالة سلسلة نصية وتُعيد سلسلة نصية أخرى fn takes_and_gives_back(a_string: String) -> String { // يدخل a_string إلى النطاق a_string // يُعاد a_string ويُنقل إلى الدالة المُستدعاة } [الشيفرة 4-4: تحويل ملكية القيمة المُعادة] تتبع ملكية المتغير نفس النمط في كل مرة، وهو: "إسناد قيمة إلى متغير آخر ينقلها"، وعندما يخرج متغير يتضمن على بيانات ضمن الكومة من النطاق، تُحرر قيمته باستخدام drop إلا إذا نُقلت ملكية البيانات إلى متغير آخر. على الرغم من نجاح هذه العملية، إلا أن عملية أخذ الملكية ومن ثم إعادتها عند كل دالة عملية رتيبة بعض الشيء. ماذا لو أردنا أن نسمح لدالة ما باستخدام القيمة دون الحصول على ملكيتها؟ إنه أمر مزعج جدًا أن أي شيء نمرره سيحتاج أيضًا لإعادة تمريره مجددًا إذا أردنا استخدامه من جديد، بالإضافة إلى أي بيانات نحصل عليها ضمن متن الدالة التي قد نحتاج أن نُعيدها أيضًا. تسمح لنا رست بإعادة عدّة قيم باستخدام مجموعة كما هو موضح في الشيفرة 4-5. اسم الملف: src/main.rs fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{}' is {}.", s2, len); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // يُعيد ()len طول السلسلة النصية (s, length) } [الشيفرة 4-5: إعادة الملكية إلى المعاملات] هذه العملية طويلة قليلًا وتتطلب كثيرًا من الجهد لشيء يُشاع استخدامه، ولحسن حظنا لدى رست ميزة لاستخدام القيمة دون تحويل ملكيتها وتدعى المراجع references، وسنستعرضها في المقال التالي. ترجمة -وبتصرف- لقسم من فصل Understanding Ownership من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست المقال السابق: التحكم بسير تنفيذ برامج راست Rust كيفية كتابة الدوال Functions والتعليقات Comments في لغة راست Rust أنواع البيانات Data Types في لغة رست Rust المتغيرات والتعديل عليها في لغة رست
-
نتطرق في هذا المقال إلى كل من ملفات الترويسة الخاصة بالقيم الحدّية Limits والدوال الرياضية في لغة C، كما نقدّم شرحًا موجزًا عن الأسماء المعرفة بداخل كل من الملفات، ويمكنك الاحتفاظ بهذا القسم كمرجع سريع بخصوص هذا الأمر. القيم الحدية يعرّف ملفا الترويسة <float.h> و <limits.h> عدة قيم حدية معرفة حسب التطبيق. ملف الترويسة <limits.h> يوضح الجدول 1 الأسماء المُصرح عنها في هذا الملف وقيمها المسموحة، إضافةً إلى وصف موجز عن وظيفتها، إذ يوضح وصف SHRT_MIN مثلًا أن قيمة الاسم في بعض التطبيقات يجب أن تكون أقل من أو تساوي القيمة -32767، وهذا يعني أن البرنامج لا يستطيع الاعتماد على متغيرات صغيرة short لتخزين قيم سالبة تتعدى 32767- إذا أردنا قابلية نقل أكبر للبرنامج. قد يدعم التطبيق في بعض الأحيان القيم السالبة الأكبر إلا أن الحد الأدنى الذي يجب أن يدعمه التطبيق هو 32767-. 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; } الاسم القيم المسموحة الوصف CHAR_BIT (≥8) بتات في قيمة من نوع char CHAR_MAX اقرأ الملاحظة القيمة العظمى لنوع char CHAR_MIN اقرأ الملاحظة القيمة الدنيا لنوع char INT_MAX (≥+32767) القيمة العظمى لنوع int INT_MIN (≤−32767) القيمة الدنيا لنوع int LONG_MAX (≥+2147483647) القيمة العظمى لنوع long LONG_MIN (≤−2147483647) القيمة الدنيا لنوع long MB_LEN_MAX (≥1) عدد البتات الأعظمي في محرف متعدد البتات multibyte character SCHAR_MAX (≥+127) القيمة العظمى لنوع signed char SCHAR_MIN (≤−127) القيمة الدنيا لنوع signed char SHRT_MAX (≥+32767) القيمة العظمى لنوع short SHRT_MIN (≤−32767) القيمة الدنيا لنوع short UCHAR_MAX (≥255U) القيمة العظمى لنوع unsigned char UINT_MAX (≥65535U) القيمة الدنيا لنوع unsigned int ULONG_MAX (≥4294967295U) القيمة العظمى لنوع unsigned long USHRT_MAX (≥65535U) القيمة الدنيا لنوع unsigned short [جدول 1 أسماء ملف الترويسة <limits.h>] ملاحظة: إذا كان التطبيق يعامل char على أنه من نوع ذو إشارة فقيمة CHAR_MAX وCHAR_MIN مماثلة لقيمة SCHAR الموافق لها، وإلا فقيمة CHAR_MIN هي صفر وقيمة CHAR_MAX هي مساوية لقيمة UCHAR_MAX. ملف الترويسة <float.h> يتضمن ملف الترويسة <float.h> قيمًا دنيا للأرقام ذات الفاصلة العائمة floating point بصورةٍ مشابهة لما سبق، ويمكن الافتراض عند عدم وجود قيمة دنيا لنوع ما أن هذا النوع لا يمتلك قيمة دنيا أو أن القيمة مرتبطة بقيمة أخرى. الاسم القيم المسموحة الوصف FLT_RADIX (≥2) تمثيل أساس الأس DBL_DIG (≥10) عدد خانات الدقة في نوع double DBL_EPSILON (≤1E−9) العدد الموجب الأدنى الذي يحقق 1.0 + x ≠ 1.0 DBL_MANT_DIG (—) عدد خانات أساس FLT_RADIX في الجزء العشري من النوع double DBL_MAX (≥1E+37) القيمة العظمى لنوع double DBL_MAX_10_EXP (≥+37) القيمة العظمى لأس (أساسه 10) من نوع double DBL_MAX_EXP (—) القيمة العظمى لأس (أساسه FLT_RADIX) من نوع double DBL_MIN (≤1E−37) القيمة الدنيا للنوع double DBL_MIN_10_EXP (≤37) القيمة الدنيا لأس (أساسه 10) من نوع double DBL_MIN_EXP (—) القيمة الدنيا لأس (أساسه FLT_RADIX) من نوع double FLT_DIG (≥6) عدد خانات الدقة في نوع float FLT_EPSILON (≤1E−5) العدد الموجب الأدنى الذي يحقق 1.0 + x ≠ 1.0 FLT_MANT_DIG (—) عدد خانات أساس FLT_RADIX في الجزء العشري من النوع float FLT_MAX (≥1E+37) القيمة العظمى للنوع float FLT_MAX_10_EXP (≥+37) القيمة العظمى لأس (أساسه 10) من نوع float FLT_MAX_EXP (—) القيمة العظمة لأس (أساسه FLT_RADIX) من نوع float FLT_MIN (≤1E−37) القيمة الدنيا للنوع float FLT_MIN_10_EXP (≤−37) القيمة الدنيا لأس (أساسه 10) من نوع float FLT_MIN_EXP (—) القيمة الدنيا لأس (أساسه FLT_RADIX) من نوع float FLT_ROUNDS (0) يحدد التقريب للفاصلة العائمة، غير مُحدّد لقيمة 1-، تقريب باتجاه الصفر لقيمة 0، تقريب للقيمة الأقرب لقيمة 1، تقريب إلى اللا نهاية الموجبة لقيمة 2، تقريب إلى اللا نهاية السالبة لقيمة 3. أي قيمة أخرى تكون محددة بحسب التطبيق LDBL_DIG (≥10) عدد خانات الدقة في نوع long double LDBL_EPSILON (≤1E−9) العدد الموجب الأدنى الذي يحقق 1.0 + x ≠ 1.0 LDBL_MANT_DIG (—) عدد خانات أساس FLT_RADIX في الجزء العشري من النوع long double LDBL_MAX (≥1E+37) القيمة العظمى للنوع long double LDBL_MAX_10_EXP (≥+37) القيمة العظمى لأس (أساسه 10) من نوع long double LDBL_MAX_EXP (—) القيمة العظمى لأس (أساسه FLT_RADIX) من نوع long double LDBL_MIN (≤1E−37) القيمة الدنيا للنوع long double LDBL_MIN_10_EXP (≤−37) القيمة الدنيا لأس (أساسه 10) من نوع long double LDBL_MIN_EXP (—) القيمة الدنيا لأس (أساسه FLT_RADIX) من نوع long double [جدول 2 أسماء ملف الترويسة <float.h>] الدوال الرياضية إذا كنت تكتب برامجًا رياضيّة تجري عمليات على الفاصلة العائمة وما شابه، فهذا يعني أنك تحتاج الوصول إلى مكتبات الدوال الرياضية دون أدنى شك، ويأخذ هذا النوع من الدوال وسطاءً من النوع double ويعيد نتيجةً من النوع ذاته أيضًا. تُعرَّف الدوال والماكرو المرتبطة بها في ملف الترويسة <math.h>. يُستبدل الماكرو HUGE_VAL المُعرف إلى تعبير ذي قيمة موجبة من نوع عدد عشري مضاعف الدقة "double"، ولا يمكن تمثيله بالضرورة باستخدام النوع float. نحصل على خطأ نطاق domain error في جميع الدوال إذا كانت قيمة الوسيط المُدخل خارج النطاق المُعرّف للدالة، مثل محاولة الحصول على جذر تربيعي لعدد سالب، وإذا حصل هذا الخطأ يُضبط errno إلى الثابت EDOM، وتُعيد الدالة قيمة معرّفة بحسب التطبيق. نحصل على خطأ مجال range error إذا لم يكن من الممكن تمثيل نتيجة الدالة بقيمة عدد عشري مضاعف الدقة، تُعيد الدالة القيمة ±HUGE_VAL إذا كانت قيمة النتيجة كبيرة جدًا (الإشارة موافقة للقيمة) وتُضبط errno إلى ERANGE إذا كانت القيمة صغيرة جدًا وتُعاد القيمة 0.0 وتُعتمد قيمة errno على تعريف التطبيق. تصف اللائحة التالية كلًا من الدوال المتاحة باختصار: الدالة double acos(double x);: تُعيد القيمة الرئيسة Principal value لقوس جيب التمام Arc cosine للوسيط x في النطاق من 0 إلى π راديان، ونحصل على الخطأ EDOM إذا كان x خارج النطاق -1 إلى 1. الدالة double asin(double x);: تُعيد القيمة الرئيسة لقوس الجيب Arc sin للوسيط x في النطاق من -π/2 إلى +π/2 راديان، ونحصل على الخطأ EDOM إذا كان xخارج النطاق -1 إلى 1. الدالة double atan(double x);: تُعيد القيمة الرئيسة لقوس الظل Arc tan للوسيط x في النطاق من -π/2 إلى +π/2 راديان. الدالة double atan2(double y, double x);: تُعيد القيمة الرئيسة لقوس الظل للقيمة y/x في النطاق من -π إلى +π راديان، وتستخدم إشارتي الوسيطين x و y لتحديد الربع الذي تقع فيه قيمة الإجابة، ونحصل على الخطأ EDOM في حال كان x و y مساويين إلى الصفر. الدالة double cos(double x);: تُعيد جيب تمام قيمة الوسيط x (تُقاس x بالراديان). الدالة double sin(double x);: تُعيد جيب قيمة الوسيط x (تُقاس x بالراديان). الدالة double tan(double x);: تُعيد ظل قيمة الوسيط x (تُقاس x بالراديان)، وتكون إشارة HUGE_VAL غير مضمونة الصحّة إذا حدث خطأ مجال. الدالة double cosh(double x);: تُعيد جيب التمام القطعي Hyperbolic للقيمة x، ونحصل على الخطأ ERANGE إذا كان مقدار x كبيرًا جدًا. الدالة double sinh(double x);: تُعيد الجيب القطعي للقيمة x، ونحصل على الخطأ ERANGE إذا كان مقدار x كبيرًا للغاية. الدالة double tanh(double x);: تُعيد الظل القطعي للقيمة x. الدالة double exp(double x);: دالة أسية للقيمة x، ونحصل على الخطأ ERANGE إذا كان مقدار xكبيرًا جدًا. الدالة double frexp(double value, int *exp);: تجزئة عدد ذو فاصلة عائمة إلى كسر طبيعي وأُس عدد صحيح من الأساس 2، ويخزن هذا العدد الصحيح في الغرض المُشار إليه بواسطة المؤشر exp. الدالة double ldexp(double x, int exp);: ضرب x بمقدار 2 إلى الأُس exp، وقد نحصل على الخطأ ERANGE. الدالة double log(double x);: اللوغاريتم الطبيعي للقيمة x، وقد نحصل على الخطأ EDOM إذا كانت القيمة x سالبة، و ERANGE إذا كانت x تساوي إلى الصفر. الدالة double log10(double x);: اللوغاريتم ذو الأساس 10 للقيمة x، ونحصل على الخطأ EDOM إذا كانت x سالبة، و ERANGE إذا كانت x تساوي إلى الصفر. الدالة double modf(double value, double *iptr);: تجزئة قيمة الوسيط value إلى جزء عدد صحيح وجزء كسري، ويحمل كل جزء إشارة الوسيط ذاتها، وتُخزن قيمة العدد الصحيح على أنها قيمة من نوع double في الكائن المُشار إليه بواسطة المؤشر iptr وتُعيد الدالة الجزء الكسري. الدالة double pow(double x, double y);: تحسب x إلى الأس y، ونحصل على الخطأ EDOM إذا كانت القيمة x سالبة و y عدد غير صحيح، أو ERANGE إذا لم يكن من الممكن تمثيل النتيجة في حال كانت x تساوي إلى الصفر و y موجبة أو تساوي الصفر. الدالة double sqrt(double x);: تحسب مربع القيمة x، ونحصل على الخطأ EDOM إذا كانت x سالبة. الدالة double ceil(double x);: أصغر عدد صحيح لا يكون أصغر من x. الدالة double fabs(double x);: القيمة المطلقة للوسيط x. الدالة double floor(double x);: أكبر عدد صحيح لا يكون أكبر من x. الدالة double fmod(double x, double y);: الباقي العشري من عملية القسمة x/y، ويعتمد الأمر على تعريف التطبيق فيما إذا كانت fmod تُعيد صفرًا أو خطأ نطاق في حال كانت y تساوي إلى الصفر. ترجمة -وبتصرف- لقسم من الفصل Libraries من كتاب The C Book. اقرأ أيضًا المقال التالي: تعلم لغة سي التعامل مع المكتبات في لغة سي C المقال السابق: التعامل مع المحارف وضبط إعدادات التوطين localization في لغة سي C الدوال في لغة C مفهوم التعاود Recursion وتمرير الوسطاء إلى الدوال في لغة سي C مفهوم النطاق Scope والربط Linkage على مستوى الدوال في لغة C
-
تُعد القدرة على تشغيل جزء من الشيفرة البرمجية إذا تحقق شرط ما، أو تشغيل جزء ما باستمرار بينما الشرط محقق من الكتل الأساسية في بناء أي لغة برمجة، كما تُعد تعابير if والحلقات التكرارية أكثر اللبنات التي تسمح لك بالتحكم بسير تنفيذ البرنامج flow control في البرامج المكتوبة بلغة راست. تعابير if الشرطية يسمح لك تعبير if بتفرعة branch شيفرتك البرمجية بحسب الشروط، ويُمكنك كتابة الشرط بحيث "إذا تحقق هذا الشرط فنفذ هذا الجزء من الشيفرة البرمجية، وإلا فلا تنفّذه". أنشئ مشروعًا جديدًا باسم "branches" في المجلد "projects"، إذ سنستخدم هذا المشروع للتعرف على تعابير if. عدّل الملف "src/main.rs" ليحتوي على الشيفرة التالية: اسم الملف: src/main.rs fn main() { let number = 3; if number < 5 { println!("condition was true"); } else { println!("condition was false"); } } تبدأ جميع تعابير if بالكلمة المفتاحية if متبوعةً بالشرط، ويتحقق الشرط في مثالنا السابق فيما إذا كانت قيمة المتغير number أصغر من 5، ونضع شيفرة برمجية مباشرةً بعد الشرط داخل أقواس معقوصة تُنفّذ إذا كان الشرط المذكور محققًا، تُدعى الشيفرة البرمجية المُرتبطة بالشرط في تعابير if بالأذرع arms في بعض الأحيان، وهي تشبه أذرع تعابير match التي تكلمنا عنها سابقًا. يُمكننا أيضًا تضمين تعبير else اختياريًا وهو ما فعلناه في هذا المثال، وذلك لإعطاء البرنامج كتلة بديلة للتنفيذ إذا كان الشرط في تعليمة if السابقة غير مُحقّق. يتخطى البرنامج كتلة تعليمة if ببساطة وينفذ بقية البرنامج في حال عدم وجود تعبير else. نحصل على الخرج التالي في حال تجربتنا لتنفيذ الشيفرة البرمجية: $ cargo run Compiling branches v0.1.0 (file:///projects/branches) Finished dev [unoptimized + debuginfo] target(s) in 0.31s Running `target/debug/branches` condition was true دعنا نُغيّر قيمة number إلى قيمة أخرى تجعل قيمة الشرط false ونرى ما الذي سيحدث: let number = 7; نفّذ البرنامج مجددًا، وانظر إلى الخرج: $ cargo run Compiling branches v0.1.0 (file:///projects/branches) Finished dev [unoptimized + debuginfo] target(s) in 0.31s Running `target/debug/branches` condition was false من الجدير بالذكر أيضًا أن الشرط في هذه الشيفرة البرمجية يجب أن يكون من النوع bool وإذا لم يكن كذلك فسنحصل على خطأ، جرّب تنفيذ الشيفرة البرمجية التالية على سبيل المثال: اسم الملف: src/main.rs fn main() { let number = 3; if number { println!("number was three"); } } يُقيَّم شرط if إلى القيمة 3 هذه المرة، ويعرض لنا راست الخطأ التالي: $ cargo run Compiling branches v0.1.0 (file:///projects/branches) error[E0308]: mismatched types --> src/main.rs:4:8 | 4 | if number { | ^^^^^^ expected `bool`, found integer For more information about this error, try `rustc --explain E0308`. error: could not compile `branches` due to previous error يُشير الخطأ إلى أن راست توقّع تلقي قيمة من نوع bool إلا أنه حصل على قيمة عدد صحيح integer. لا تُحوّل راست الأنواع غير البوليانية إلى أنواع بوليانية بعكس لغات البرمجة الأخرى، مثل روبي وجافا سكريبت، إذ عليك أن تكون دقيقًا بكتابة شرط تعليمة if ليكون تعبيرًا يُقيَّم إلى قيمة بوليانية، على سبيل المثال إن أردنا لكتلة تعليمة if أن تعمل فقط في حالة كان الرقم لا يساوي الصفر فيمكننا تغيير التعبير كما يلي: اسم الملف: src/main.rs fn main() { let number = 3; if number != 0 { println!("number was something other than zero"); } } سيطبع تشغيل الشيفرة البرمجية السابقة "number was something other than zero". التعامل مع عدة شروط باستخدام else if يُمكنك استخدام عدة شروط باستخدام if و else في تعابير else if، على سبيل المثال: اسم الملف: src/main.rs fn main() { let number = 6; if number % 4 == 0 { println!("number is divisible by 4"); } else if number % 3 == 0 { println!("number is divisible by 3"); } else if number % 2 == 0 { println!("number is divisible by 2"); } else { println!("number is not divisible by 4, 3, or 2"); } } لهذا البرنامج أربعة مسارات مختلفة ممكنة التنفيذ، ومن المُفترض أن تحصل على الخرج التالي بعد تشغيله: $ cargo run Compiling branches v0.1.0 (file:///projects/branches) Finished dev [unoptimized + debuginfo] target(s) in 0.31s Running `target/debug/branches` number is divisible by 3 يتحقَّق البرنامج عند تنفيذه من كل تعبير if فيما إذا كان مُحقًًقًا ويُنفّذ أول متن شرط يُحقّق، لاحظ أنه على الرغم من قابلية قسمة 6 على 2 إلا أننا لم نرى الخرج number is divisible by 2، أو الخرج number is not divisible by 4, 3, or 2 من كتلة التعليمة else، وذلك لأن راست تُنفّذ الكتلة الأولى التي تحقق الشرط فقط وحالما تجد هذه الكتلة، فإنها لا تتفقّد تحقق الشروط الأخرى التي تلي تلك الكتلة. قد يسبب استخدام الكثير من تعابير else if الفوضى في شيفرتك البرمجية، لذا إذا كان لديك أكثر من تعبير واحد، تأكد من إعادة النظر إلى شيفرتك البرمجية ومحاولة تحسينها، وسنناقش لاحقًا هيكل تفرعي branching construct في راست يُدعى match وقد صُمّم لهذه الحالات خصيصًا. استخدام if في تعليمة let يُمكننا استخدام if في الجانب الأيمن من تعليمة let بالنظر إلى أنها تعبير وإسناد النتيجة إلى متغير كما توضح الشيفرة 3-2. اسم الملف: src/main.rs fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {number}"); } [الشيفرة 3-2: إسناد نتيجة تعبير if إلى متغير] يُسند المتغير number إلى قيمة بناءً على نتيجة تعبير if، نفّذ الشيفرة البرمجية السابقة ولاحظ النتيجة: $ cargo run Compiling branches v0.1.0 (file:///projects/branches) Finished dev [unoptimized + debuginfo] target(s) in 0.30s Running `target/debug/branches` The value of number is: 5 تذكر أن كُتل الشيفرات البرمجية تُقيّم إلى قيمة آخر تعبير موجود داخلها والأرقام بحد ذاتها هي تعابير أيضًا، في هذه الحالة تعتمد قيمة تعبير if بالكامل على أي كتلة برمجية تُنفَّذ، وهذا يعني أنه يجب أن تكون القيم المُحتمل أن تكون نتيجة تعبير if من النوع ذاته. نجد في الشيفرة 2 نتيجة كل من ذراع if و else، إذ تُمثّل القيمة النوع الصحيح i32. نحصل على خطأ إذا كانت الأنواع غير متوافقة كما هو الحال في المثال التالي: اسم الملف: src/main.rs fn main() { let condition = true; let number = if condition { 5 } else { "six" }; println!("The value of number is: {number}"); } عندما نحاول تصريف الشيفرة البرمجية السابقة سنحصل على خطأ، إذ يوجد لذراعي if و else قيمتين من أنواع غير متوافقة، ويدلّنا راست على مكان المشكلة في البرنامج بالضبط عن طريق الرسالة: $ cargo run Compiling branches v0.1.0 (file:///projects/branches) error[E0308]: `if` and `else` have incompatible types --> src/main.rs:4:44 | 4 | let number = if condition { 5 } else { "six" }; | - ^^^^^ expected integer, found `&str` | | | expected because of this For more information about this error, try `rustc --explain E0308`. error: could not compile `branches` due to previous error يُقيَّم التعبير الموجود في كتلة if إلى عدد صحيح، بينما يُقيَّم التعبير الموجود في كتلة else إلى سلسلة نصية، وذلك لن يعمل لأنه يجب على المتغيرات أن تكون من النوع ذاته وذلك حتى تعرف راست نوع المتغير number وقت التصريف بصورةٍ نهائية ومؤكدة، إذ تسمح معرفة نوع number للمصرف بالتحقق من أن النوع المُستخدم صالح الاستخدام في كل مكان نستخدم فيه المتغير number، ولن تكون راست قادرةً على التحقق من هذا الأمر إذا كان نوع المتغير number يُحدّد عند وقت التشغيل runtime فقط، إذ سيُصبح المصرف مُشوَّشًا ولن يُقدم الضمانات ذاتها في الشيفرة البرمجية إذا كان عليه تتبع عدة أنواع افتراضية لأي متغير. التكرار باستخدام الحلقات نحتاج غالبًا لتنفيذ جزء محدد من الشيفرة البرمجية أكثر من مرة واحدة، ولتحقيق ذلك تزوّدنا راست بالحلقات التي تُنفّذ الشيفرة البرمجية داخل متن الحلقة من البداية إلى النهاية ومن ثم إلى البداية مجددًا، وللتعرّف إلى الحلقات دعنا نُنشئ مشروعًا جديدًا باسم "loops". لراست ثلاثة أنواع من الحلقات، هي: loop و while و for، دعنا نجرّب كل منها. تكرار الشيفرة البرمجية باستخدام loop تُعلِم الكلمة المفتاحية loop راست بوجوب تنفيذ جزء من الشيفرة البرمجية على نحوٍ متكرر إلى الأبد أو لحين تحديد التوقف بصورةٍ صريحة. على سبيل المثال، عدّل محتويات الملف "src/main.rs" في مجلد مشروعنا الجديد "loops" ليحتوي على الشيفرة البرمجية التالية: اسم الملف: src/main.rs fn main() { loop { println!("again!"); } } عندما نُشغّل البرنامج السابق سنجد النص "again!" مطبوعًا مرةً بعد الأخرى باستمرار إلى أن نوقف البرنامج يدويًا، ونستطيع إيقافه باستخدام اختصار لوحة المفاتيح "ctrl-c"، إذ تدعم معظم الطرفيات هذا الاختصار لإيقاف البرنامج في حال تكرار حلقة للأبد. جرّب الأمر: $ cargo run Compiling loops v0.1.0 (file:///projects/loops) Finished dev [unoptimized + debuginfo] target(s) in 0.29s Running `target/debug/loops` again! again! again! again! ^Cagain! يُمثل الرمز ^C الموضع الذي ضغطت فيه على الاختصار "ctrl-c"، وقد تجد الكلمة again! مطبوعةً بعد ^C أو قد لا تجدها بحسب مكان التنفيذ ضمن الشيفرة البرمجية عند ضغطك على إشارة المقاطعة interrupt signal. تُزوّدنا راست أيضًا لحسن الحظ بطريقة أخرى للخروج قسريًا من حلقة تكرارية باستخدام شيفرة برمجية، إذ يمكننا استخدام الكلمة المفتاحية break داخل الحلقة التكرارية لإخبار البرنامج بأننا نريد إيقاف تنفيذ الحلقة. تذكر أننا فعلنا ذلك عند كتابتنا شيفرة برنامج لعبة التخمين سابقًا وذلك للخروج من البرنامج عندما يفوز اللاعب بتخمين الرقم الصحيح. كما أننا استخدمنا أيضًا الكلمة المفتاحية continue في لعبة التخمين، وهي كلمة تُخبر البرنامج بتخطي أي شيفرة برمجية متبقية داخل الحلقة في التكرار الحالي والذهاب إلى التكرار اللاحق. إعادة قيم من الحلقات واحدة من استخدامات loop هي إعادة تنفيذ عملية قد تفشل، مثل التحقق إذا أنهى خيط thread ما العمل، وقد تحتاج أيضًا إلى تمرير نتيجة هذه العملية خارج الحلقة إلى باقي الشيفرة البرمجية؛ ولتحقيق ذلك يمكنك إضافة القيمة التي تُريد إعادتها بعد تعبير break، إذ سيتوقف عندها تنفيذ الحلقة وستُعاد القيمة خارج الحلقة حتى يتسنى لك استخدامها كما هو موضح: fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("The result is {result}"); } نُصرّح عن متغير باسم counter ونُهيّئه بالقيمة "0" قبل الحلقة التكرارية، ثم نصرح عن متغير باسم result لتخزين القيمة المُعادة من الحلقة. نُضيف 1 إلى المتغير counter عند كل تكرار للحلقة ومن ثم نتحقق فيما إذا كان المتغير counter مساويًا إلى القيمة 10، وعندما يتحقق هذا الشرط نستخدم الكلمة المفتاحية break مع القيمة counter * 2، ونستخدم بعد الحلقة فاصلة منقوطة لإنهاء التعليمة التي تُسند القيمة إلى result، وأخيرًا نطبع القيمة result التي تكون في هذه الحالة مساويةً إلى 20. تسمية الحلقات للتفريق بين عدة حلقات تُطبّق break و continue في حال وجود حلقة داخل حلقة على الحلقة الداخلية الموجود بها الكلمة المفتاحية، ويمكنك تحديد تسمية الحلقة loop label اختياريًا عند إنشاء حلقة حتى يُمكنك استخدام break أو continue مع تحديد تسمية الحلقة بدلًا من تنفيذ عملها على الحلقة الداخلية. يجب أن تبدأ تسمية الحلقة بعلامة تنصيص واحدة، ويوضح المثال التالي استخدام حلقتين متداخلتين: fn main() { let mut count = 0; 'counting_up: loop { println!("count = {count}"); let mut remaining = 10; loop { println!("remaining = {remaining}"); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; } count += 1; } println!("End count = {count}"); } للحلقة الخارجية التسمية 'counting_up وستعدّ من 0 إلى 2، بينما تعدّ الحلقة الداخلية عديمة التسمية من 10 إلى 9. لا تُحدد break الأولى أي تسمية لذلك ستغادر الحلقة الداخلية فقط، بينما ستغادر تعليمة break 'counting_up الحلقة الخارجية، وتطبع الشيفرة البرمجية السابقة ما يلي: $ cargo run Compiling loops v0.1.0 (file:///projects/loops) Finished dev [unoptimized + debuginfo] target(s) in 0.58s Running `target/debug/loops` count = 0 remaining = 10 remaining = 9 count = 1 remaining = 10 remaining = 9 count = 2 remaining = 10 End count = 2 الحلقات الشرطية باستخدام while سيحتاج البرنامج غالبًا إلى تقييم شرط داخل حلقة، بحيث يستمر تنفيذ الحلقة إذا كان الشرط محققًا وإلا فسيتوقف تنفيذها عن طريق استدعاء break وإيقاف الحلقة ومن الممكن تطبيق شيء مماثل باستخدام مزيج من loop و if و else و break، ويمكنك تجربة الأمر الآن داخل برنامج إذا أردت ذلك. يُعد هذا النمط شائعًا جدًا وهذا هو السبب وراء وجود بنية مُضمَّنة في راست لهذا الاستخدام تُدعى حلقة while. نستخدم في الشيفرة 3-3 التالية الحلقة while لتكرار الحلقة ثلاث مرات بالعدّ تنازليًا في كل مرة وعند الخروج من الحلقة نطبع رسالة ونُنهي البرنامج. اسم الملف: src/main.rs fn main() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("LIFTOFF!!!"); } [الشيفرة 3-3: استخدام حلقة while لتنفيذ شيفرة برمجية عند تحقق شرط ما] يُغنينا استخدام هذه البنية عناء استخدام الكثير من التداخلات بواسطة loop و if و else و break كما أنه أكثر وضوحًا، إذ طالما يكون الشرط محققًا ستُنفّذ الحلقة وإلا فسيغادر الحلقة. استخدام for مع تجميعة Collection يمكنك اختيار البنية while للانتقال بين عناصر التجميعة مثل المصفوفات. توضح الشيفرة 3-4 ذلك الاستخدام بطباعة كل عنصر في المصفوفة a. اسم الملف: src/main.rs fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { println!("the value is: {}", a[index]); index += 1; } } [الشيفرة 4: الانتقال بين عناصر التجميعة باستخدام حلقة while] إليك الشيفرة البرمجية التي تنتقل بين عناصر المصفوفة، إذ تبدأ من الدليل "0" وتنتقل إلى ما يليه لحد الوصول إلى الدليل الأخير في المصفوفة (أي عندما يكون index < 5 غير محقق). سيطبع تنفيذ الشيفرة السابقة عناصر المصفوفة كما يلي: $ cargo run Compiling loops v0.1.0 (file:///projects/loops) Finished dev [unoptimized + debuginfo] target(s) in 0.32s Running `target/debug/loops` the value is: 10 the value is: 20 the value is: 30 the value is: 40 the value is: 50 تظهر جميع قيم عناصر المصفوفة الخمسة ضمن الطرفية كما هو متوقع. على الرغم من أن index سيصل إلى القيمة 5 في مرحلة ما إلا أن تنفيذ الحلقة يتوقف قبل محاولة طباعة العنصر السادس من المصفوفة. سلوك البرنامج معرض للخطأ، فقد يهلع البرنامج إذا كانت قيمة الدليل أو الشرط الذي يُفحص خاطئة، على سبيل المثال إذا استبدلنا تعريف المصفوفة a ليكون لها أربعة عناصر ولكننا نسينا تحديث الشرط إلى while index < 4، ستهلع الشيفرة البرمجية، كما أن هذا السلوك بطيء لأن المصرف يُضيف شيفرة برمجية عند وقت التشغيل لإنجاز التحقق من الشرط فيما إذا كان الدليل خارج حدود المصفوفة عند كل تكرار ضمن الحلقة. بدلًا من ذلك، يمكننا استخدام حلقة for وتنفيذ شيفرة برمجية لكل عنصر في التجميعة، وتبدو الحلقة بالشكل الموضح في الشيفرة 3-5. اسم الملف: src/main.rs fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } } [الشيفرة 3-5: الانتقال بين عناصر التجميعة باستخدام حلقة for] ستجد الخرج ذاته للشيفرة 3-4 عند تنفيذ الشيفرة السابقة، والأهم هنا أننا زدنا من أمان شيفرتنا البرمجية وأزلنا أي فرص للأخطاء الناجمة عن الذهاب إلى ما بعد حدود المصفوفة، أو عدم الذهاب إلى نهايتها وبالتالي عدم طباعة جميع العناصر. لست مضطرًا لتغيير أي شيفرة برمجية باستخدام حلقة for إذا عدلت رقم العناصر في المصفوفة، الأمر الذي ستضطر لفعله في حال استخدامك للشيفرة 3-4. تُستخدم حلقات for كثيرًا نظرًا للأمان والإيجاز التي تقدمه مقارنةً ببُنى الحلقات الأخرى الموجودة في راست، حتى أن معظم مبرمجين لغة راست يفضلون استخدام الحلقة for عند تنفيذ شيفرة برمجية يُفترض تنفيذها عدد معين من المرات كما هو الحال في مثال العد التنازلي الذي أنجزناه باستخدام حلقة while في الشيفرة 3-3، ويُنجز ذلك الأمر باستخدام Range المُضمَّن في المكتبة القياسية، والذي يولّد بدوره جميع الأرقام في السلسلة بدءًا من رقم معين وانتهاءً برقم آخر. إليك ما سيبدو عليه برنامج العد التنازلي باستخدام حلقة for وتابع آخر لم نتكلم عنه بعد وهو rev، المُستخدم في عكس المجال: اسم الملف: src/main.rs fn main() { for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!"); } تبدو هذه الشيفرة البرمجية أفضل، أليس كذلك؟ ترجمة -وبتصرف- للقسم Control Flow من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: الملكية Ownership في لغة رست المقال السابق: كيفية كتابة الدوال Functions والتعليقات Comments في لغة راست Rust أنواع البيانات Data Types في لغة رست Rust المتغيرات والتعديل عليها في لغة رست
-
تنتشر الدوال في معظم شيفرات راست البرمجية، وقد رأيت سابقًا واحدةً من أهم الدوال في اللغة ألا وهي دالة main وهي نقطة البداية للكثير من البرامج، كما أنك رأيت أيضًا الكلمة المفتاحية fn التي تسمح لك بالتصريح عن دالةٍ جديدة. تستخدم شيفرة راست البرمجية نمط الثعبان snake case نمطًا اصطلاحيًا لأسماء الدوال والمتغيرات، إذ تكون الأحرف في هذا النمط جميعها أحرف صغيرة ويفصل ما بين الكلمة والأخرى شرطة سفلية. إليك برنامجًا يحتوي على مثال لتعريف دالة: اسم الملف: src/main.rs fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); } نعرّف الدالة في راست بإدخال الكلمة fn متبوعةً باسم الدالة يليها قوسين هلاليّين parentheses ()، بينما تُخبر الأقواس المعقوفة curly brackets {} المصرف بموضع بداية وانتهاء متن الدالة. يُمكننا استدعاء أي دالة عرفناها سابقًا بإدخال اسمها متبوعًا بقوسين هلاليّين، وبما أن الدالة another_function مُعرفةٌ في البرنامج، يمكننا استدعائها من داخل الدالة main. لاحظ أننا عرفنا another_function بعد دالة main في الشيفرة البرمجية إلا أنه يمكننا تعريفها قبلها أيضًا، إذ لا تُبالي راست بموضع تعريف الدوال طالما يوجد التعريف داخل النطاق scope الذي استدعيت الدالة منه. دعنا نبدأ مشروعًا ثنائيًا binary project جديدًا باسم "functions" للنظر إلى الدوال بتعمّق أكبر، وضع مثال "another_function" السابق في ملف "src/main.rs" ونفّذه. يجب أن يظهر لك الخرج التالي: $ cargo run Compiling functions v0.1.0 (file:///projects/functions) Finished dev [unoptimized + debuginfo] target(s) in 0.28s Running `target/debug/functions` Hello, world! Another function. تُنفّذ هذه السطور البرمجية بالترتيب التي ظهرت فيه في الدالة main، أي تُطبع الرسالة "Hello, world!" أولًا، ثم تُستدعى الدالة another_function وتُطبع رسالتها. المعاملات يُمكننا تعريف الدوال بحيث تحتوي على معاملات parameters، وهي متغيرات خاصة تنتمي إلى بصمة الدالة function's signature، ويُمكنك استخدام قيم فعلية لهذه الدالة عند احتوائها على معاملات، وتُدعى هذه القيم بالوسطاء arguments إلا أنه غالبًا ما يُستخدم المصطلحان معامل ووسيط بصورةٍ تبادلية interchangeably لأي من المتغيرات في تعريف الدالة أو القيم الفعلية المُمرّرة للدالة عند استدعائها. نُضيف معاملًا في هذا الإصدار من الدالة another_function: اسم الملف: src/main.rs fn main() { another_function(5); } fn another_function(x: i32) { println!("The value of x is: {x}"); } يجب أن تحصل على الخرج التالي عند تجربتك لتشغيل البرنامج: $ cargo run Compiling functions v0.1.0 (file:///projects/functions) Finished dev [unoptimized + debuginfo] target(s) in 1.21s Running `target/debug/functions` The value of x is: 5 يحتوي تصريح الدالة another_function على معامل واحد باسم x وهو من النوع i32، بالتالي يضع الماكرو !println القيمة 5 عند تمريرها إلى الدالة مثل قيمة للمعامل x في تنسيق السلسلة النصية. يجب التصريح عن نوع كل معامل في بصمة الدالة، وهذا أمر متعمد في تصميم لغة راست؛ إذ يعني تحديد أنواع المعاملات في تعريف الدالة أن المُصرّف لن يحتاج منك استخدامها في مكان آخر ضمن الشيفرة البرمجية لمعرفة النوع الذي قصدته، وبالتالي يستطيع المصرف إعطاء رسائل خطأ ذات معنًى ومضمون مُساعد أكثر إذا كان يعلم نوع المعاملات التي تأخذها الدالة. يجب فصل المعاملات بالفاصلة عند تعريف أكثر من معامل واحد كما هو موضح: اسم الملف: src/main.rs fn main() { print_labeled_measurement(5, 'h'); } fn print_labeled_measurement(value: i32, unit_label: char) { println!("The measurement is: {value}{unit_label}"); } يُنشئ هذا المثال دالةً باسم print_labeled_measurment بمعاملَين، إذ يسمى المعامل الأول value وهو من النوع i32، بينما يسمى النوع الثاني unit_label وهو من النوع char، وتطبع الدالة نصًّا يحتوي على كل من value و unit_label. دعنا نجرّب تشغيل الشيفرة البرمجية السابقة، وذلك باستبدال البرنامج الموجود حاليًا في ملف "src/main.rs" لمشروع "function" بالشيفرة البرمجية السابقة، وتشغيل البرنامج باستخدام cargo run: $ cargo run Compiling functions v0.1.0 (file:///projects/functions) Finished dev [unoptimized + debuginfo] target(s) in 0.31s Running `target/debug/functions` The measurement is: 5h نحصل على الخرج السابق طالما استُدعيت الدالة بالقيمة "5" للمعامل value والقيمة 'h' للمعامل unit_label. التعابير والتعليمات يتألف متن الدالة من مجموعة من التعليمات التي تنتهي -اختياريًا- بتعبير expression، والدوال التي غطيناها حتى الآن لم تتضمن تعبيرًا في نهاية التعليمة، إلا أننا قد رأينا تعبيرًا بمثابة جزء من تعليمة. من المهم أن نميّز بين المصطلحين، وذلك لأن راست لغة مبنية على التعابير وذلك الأمر لا ينطبق على بقية اللغات، لذا دعنا ننظر إلى ماهية التعليمات والتعابير وما هو الفرق فيما بينهما وكيف يؤثر كل منهما على متن الدالة. التعليمات هي توجيهات تُجري عمليات ما ولا تُعيد قيمةً، بينما تُقيّم التعابير إلى قيمة ناتجة. دعنا ننظر إلى بعض الأمثلة. استخدمنا في الحقيقة سابقًا كلًا من التعابير والتعليمات، وذلك بإنشاء متغير وإسناد قيمة إليه باستخدام الكلمة المفتاحية let، نجد في الشيفرة 3-1 التعليمة let y = 6;. اسم الملف: src/main.rs fn main() { let y = 6; } [الشيفرة 3-1: تعريف دالة main يحتوي على تعليمة واحدة] تُعدّ تعاريف الدوال تعليمات أيضًا، فالمثال السابق هو تعليمة واحدة بذات نفسه. لا تُعيد التعليمات أي قيمة، لذلك لا يُمكنك إسناد تعليمة let إلى متغير آخر كما نحاول في المثال التالي، إذ ستحصل على خطأ: اسم الملف: src/main.rs fn main() { let x = (let y = 6); } ستحصل على الخطأ التالي عند محاولتك لتشغيل البرنامج السابق: $ cargo run Compiling functions v0.1.0 (file:///projects/functions) error: expected expression, found statement (`let`) --> src/main.rs:2:14 | 2 | let x = (let y = 6); | ^^^^^^^^^ | = note: variable declaration using `let` is a statement error[E0658]: `let` expressions in this position are unstable --> src/main.rs:2:14 | 2 | let x = (let y = 6); | ^^^^^^^^^ | = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information warning: unnecessary parentheses around assigned value --> src/main.rs:2:13 | 2 | let x = (let y = 6); | ^ ^ | = note: `#[warn(unused_parens)]` on by default help: remove these parentheses | 2 - let x = (let y = 6); 2 + let x = let y = 6; | For more information about this error, try `rustc --explain E0658`. warning: `functions` (bin "functions") generated 1 warning error: could not compile `functions` due to 2 previous errors; 1 warning emitted لا تُعيد التعليمة let y = 6 أي قيمة، لذا لا يوجد هناك أي قيمة لإسنادها إلى x، وهذا الأمر مختلفٌ عن باقي لغات البرمجة مثل سي C وروبي Ruby إذ تُعيد عملية الإسناد في هذه اللغات قيمة الإسناد، وبالتالي يمكنك كتابة التعليمة x = y = 6 بحيث تُسند القيمة 6 إلى كل من x و y إلا أن هذا الأمر لا ينطبق في راست. تُقيِّم وتركّب التعابير معظم الشيفرة البرمجية التي ستكتبها في راست، خُذ على سبيل المثال تعبير العملية الحسابية 5 + 6، التي تُقيّم إلى القيمة 11. يمكن أن تكون التعابير جزءًا من التعليمات، ففي الشيفرة 3-1 تُمثّل 6 في التعليمة let y = 6; تعبيرًا يُقيّم إلى القيمة 6. يُعد كل من استدعاء الدالة واستدعاء الماكرو وإنشاء نطاق جديد باستخدام الأقواس المعقوفة تعبيرًا، على سبيل المثال: اسم الملف: src/main.rs fn main() { let y = { let x = 3; x + 1 }; println!("The value of y is: {y}"); } التعبير التالي هو جزءٌ يُقيم إلى القيمة 4: { let x = 3; x + 1 } تُسند القيمة فيما بعد إلى y كجزء من تعليمة let، لاحظ أن السطر x + 1 لا يحتوي على فاصلة منقوطة في نهايته مثل معظم الأسطر التي كتبناها لحد اللحظة، وذلك لأن التعابير لا تحتوي على فاصلة منقوطة في النهاية، وإذا أضفت الفاصلة المنقوطة فسيتحول التعبير إلى تعليمة ولن يكون هناك أي قيمة مُعادة حينها. تذكّر ما سبق بينما نتكلم عن القيم المُعادة من الدوال والتعابير لاحقًا. الدوال التي تعيد قيمة يُمكن للدوال أن تُعيد قيمًا إلى الشيفرة البرمجية التي استدعتها، ولا نُسمّي القيم المُعادة هذه إلا أنه يجب التصريح عن نوعها باستخدام السهم <-. القيمة المُعادة من الدالة في راست هي مرادف لقيمة التعبير الأخير في متن الدالة، ويُمكنك إعادة قيمة مبكرًا من الدالة باستخدام الكلمة المفتاحية return وتحديد القيمة بعدها، إلا أن معظم الدوال تُعيد قيمة التعبير الأخير ضمنيًا. إليك مثالًا عن دالة تُعيد قيمة: اسم الملف: src/main.rs fn five() -> i32 { 5 } fn main() { let x = five(); println!("The value of x is: {x}"); } لا يوجد في الدالة five أي استدعاءات، أو ماكرو، أو حتى تعليمة let، بل فقط الرقم 5، وتلك دالة صالحة في لغة راست. لاحظ أن نوع القيمة المُعادة من الدالة مُحدّد أيضًا بكتابة -> i32. يجب أن تحصل على الخرج التالي إذا جرّبت تشغيل الشيفرة البرمجية: $ cargo run Compiling functions v0.1.0 (file:///projects/functions) Finished dev [unoptimized + debuginfo] target(s) in 0.30s Running `target/debug/functions` The value of x is: 5 تُمثّل القيمة 5 في الدالة five القيمة المُعادة من الدالة، وهذا السبب في تحديدنا لنوع القيمة المعادة بالنوع i32، لكن دعنا ننظر إلى الدالة بتعمُّق أكبر، إذ يوجد جزآن مُهمّان، هما: أولًا، يوضح السطر let x = five(); أننا نستخدم القيمة المُعادة من الدالة لإسنادها مثل قيمة أولية للمتغير وبما أن الدالة تُعيد القيمة 5، فهذا الأمر موافق لكتابة السطر البرمجي التالي تمامًا: let x = 5; ثانيًا، لا تحتوي الدالة five أي معاملات وتُعرِّف نوع القيمة المعادة، إلا أن متن الدالة يحتوي على القيمة 5 بصورةٍ منفردة دون فاصلة منقوطة وذلك لأنه تعبير نُريد قيمته على أنها قيمة الدالة المُعادة. دعنا ننظر إلى مثال آخر: اسم الملف: src/main.rs fn main() { let x = plus_one(5); println!("The value of x is: {x}"); } fn plus_one(x: i32) -> i32 { x + 1 } سيطبع تنفيذ الشيفرة البرمجية السابقة The value of x is: 6، إلا أننا سنحصل على خطأ إذا استبدلنا الفاصلة المنقوطة في نهاية السطر x + 1 مما يُغيّر السطر من تعبير إلى تعليمة. اسم الملف: src/main.rs fn main() { let x = plus_one(5); println!("The value of x is: {x}"); } fn plus_one(x: i32) -> i32 { x + 1; } تصريف الشيفرة البرمجية السابقة سيتسبب بخطأ كما هو موضح: $ cargo run Compiling functions v0.1.0 (file:///projects/functions) error[E0308]: mismatched types --> src/main.rs:7:24 | 7 | fn plus_one(x: i32) -> i32 { | -------- ^^^ expected `i32`, found `()` | | | implicitly returns `()` as its body has no tail or `return` expression 8 | x + 1; | - help: remove this semicolon For more information about this error, try `rustc --explain E0308`. error: could not compile `functions` due to previous error تُشير الرسالة الأساسية إلى أن سبب الخطأ هو بسبب "أنواع غير متوافقة mismatched types". يدل تعريف الدالة plus_one على أنها تُعيد قيمةً من النوع i32 إلا أن التعبير لا يُقيَّم إلى قيمة، وهو الشيء المُعبّر بالقوسين () نوع الوحدة unit type، وبالتالي لا يوجد هناك أي قيمة لإعادتها مما يتناقض مع تعريف الدالة ويتسبب بخطأ. توفّر راست في رسالة الخطأ رسالةً لمساعدتك في حل هذه المشكلة إذ تقترح إزالة الفاصلة المنقوطة مما سيحلّ المشكلة بدوره. التعليقات يسعى جميع المبرمجين لجعل شيفرتهم البرمجية سهلة الفهم، إلا أن الشرح الإضافي في بعض الأحيان لازم، وهنا تأتي أهمية التعليقات في الشيفرة المصدرية التي يتجاهلها المُصرّف إلا أنها مفيدة بحقّ للناس الذين يقرؤون شيفرتك المصدرية. إليك تعليقًا بسيطًا: // hello, world يبدأ التعليق في لغة راست بشرطتين مائلتين ويستمر التعليق إلى نهاية السطر، وإذا أردت استخدام التعليق ليشمل عدّة أسطر فعليك استخدام // في كل سطر كما يلي: // So we’re doing something complicated here, long enough that we need // multiple lines of comments to do it! Whew! Hopefully, this comment will // explain what’s going on. يُمكن إضافة التعليقات في نهاية الأسطر البرمجية: اسم الملف: src/main.rs fn main() { let lucky_number = 7; // I’m feeling lucky today } إلا أنك غالبًا ما سترى التعليقات بالتنسيق التالي على سطر منفصل عن بقية الشيفرة البرمجية التي تشرحها: اسم الملف: src/main.rs fn main() { // I’m feeling lucky today let lucky_number = 7; } يوجد طريقة أخرى لكتابة التعليقات ألا وهي التعليقات التوثيقية documentation comments التي سنناقشها لاحقًا. ترجمة -وبتصرف- للقسم Functions والقسم Comments من كتاب The Rust Programming Language. اقرأ أيضًا المقال التالي: التحكم بسير تنفيذ برامج رست Rust المقال السابق: أنواع البيانات Data Types في لغة رست Rust تعلم لغة رست Rust: البدايات برمجة لعبة تخمين الأرقام بلغة رست Rust
-
نستعرض في هذا المقال كيفية التعامل مع المحارف في لغة سي باستخدام دوال المكتبات القياسية، إضافةً إلى التوطين وإعدادات اللغة المحلية locale. التعامل مع المحارف هناك مجموعةٌ متنوعةٌ من الدوال تهدف لفحص وربط mapping المحارف، إذ تسمح لك دوال الفحص test functions -التي سنناقشها أولًا- بفحص فيما إذا كان المحرف من نوع معين، مثل حرف أبجدي، أو حرف صغير أم كبير، أو محرف رقمي، أو محرف تحكّم control character، أو إشارة ترقيم، أو محرف قابل للطباعة أو لا، وهكذا. تُعيد دوال فحص المحرف قيمة عدد صحيح integer تساوي الصفر إذا لم يكن المحرف المُحدّد منتميًا إلى التصنيف المذكور، أو قيمة غير صفرية عدا ذلك، ويأخذ هذا النوع من الدوال وسيطًا ذا قيمة عدد صحيح تُمثّل قيمته من نوع "unsigned char"، أو عدد صحيح ثابت قيمته "EOF" مثل تلك القيمة المُعادة من دوال مشابهة، مثل getchar()، ونحصل على سلوك غير معرّف خارج هذه الحالات. تعتمد هذه الدوال على إعدادات البرنامج المحلية: محرف الطباعة printing character هو عضو من مجموعة المحارف المعرّفة بحسب التطبيق، ويشغل كل محرف طباعة موقع طباعة واحد، ومحرف التحكم control character هو عضو من مجموعة المحارف المعرفة بحسب التطبيق أيضًا إلا أن كل محرف منها ليس بمحرف طباعة. إذا استخدمنا مجموعة محارف معيار ASCII 7-bit، ستكون محارف الطباعة بين الفراغ (0x20) وتيلدا tilde (0x7e)، بينما تكون محارف التحكم بين NUL (0x0) و US (0x1f) والمحرف DEL (0x7f). تجد أدناه ملخصًا يحتوي على جميع دوال فحص المحرف، ويجب تضمين ملف الترويسة <ctype.h> قبل استخدام أيّ منها. دالة isalnum(int c): تُعيد القيمة "True" إذا كان c محرفًا أبجديًا أو رقمًا؛ أي (isalpha(c)||isdigit(c)). دالة isalpha(int c): تُعيد القيمة "True" إذا كان هذا الشرط (isupper(c)||islower(c)) محققًا، كما أنها تُعيد القيمة True لمجموعة المحارف المُعرفة بحسب التطبيق التي لا تعيد القيمة True عند تمريرها على الدالة iscntrl أو isdigit أو ispunct أو isspace وتكون مجموعة المحارف الإضافية هذه فارغة في لغة سي المحلية. دالة iscntrl(int c): تُعيد القيمة True إذا كان المحرف محرف تحكم. دالة isdigit(int c): تُعيدالقيمة True إذا كان المحرف رقمًا عشريًا decimal. دالة isgraph(int c): تُعيد القيمة True إذا كان المحرف هو محرف طباعة عدا محرف المسافة الفارغة. دالة islower(int c): تُعيد القيمة True إذا كان المحرف محرفًا أبجديًا صغيرًا lower case، كما أنها محققة لمجموعة محارف معرفة حسب التطبيق لا تُعيد القيمة True لأي من الدالة iscntrl أو isdigit أو ispunct أو isspace، وتكون مجموعة المحارف الإضافية هذه فارغة في C المحلية. دالة isprint(int c): تُعيد القيمة True إذا كان المحرف محرف طباعة (متضمّنًا محرف المسافة الفارغة). دالة ispunct(int c): تُعيد القيمة True إذا كان المحرف محرف طباعة عدا محرف المسافة الفارغة أو المحارف التي تُعيد القيمة True في دالة isalnum. دالة isspace(int c): تُعيد القيمة True إذا كان المحرف محرف مسافة بيضاء (المحرف ' ' أو \f أو \n أو \r أو \t أو \v) دالة isupper(int c): تُعيد القيمة True إذا كان المحرف محرف أبجديًا كبيرًا upper case، كما أنها محققة لمجموعة محارف معرفة حسب التطبيق لا تُعيد القيمة True لأي من الدالة iscntrl أو isdigit أو ispunct أو isspace، وتكون مجموعة المحارف الإضافية هذه فارغة في لغة سي المحلية. دالة isxdigit(int c): تُعيد القيمة True إذا كان المحرف رقم ستّ عشري صالح. هناك دالتان إضافيتان تربطان المحارف من مجموعةٍ إلى أخرى، إذ تُعيد الدالة tolower محرفًا صغيرًا موافقًا لمحرف كبير مُرِّر لها، على سبيل المثال: tolower('A') == 'a' تُعيد الدالة tolower المحرف ذاته، إذا تلقّت أي محرف مُغاير للمحارف الأبجدية الكبيرة. تربط الدالة toupper المعاكسة للدالة السابقة في عملها المحرف المُمرّر لها إلى مكافئه الكبير. تُجرى عملية الربط في الدالتين السابقتين فقط في حال وجود محرف موافق للمحرف المُمرّر لها، إذ لا تمتلك بعض اللغات محرفًا كبيرًا موافق لمحرف صغير والعكس صحيح. التوطين Localization نستطيع التحكم بالإعدادات المحليّة للبرنامج من هنا، ويصرح ملف الترويسة <locale.h> دوال setlocale و localeconv وعددًا من الماكرو: LC_ALL LC_COLLATE LC_CTYPE LC_MONETARY LC_NUMERIC LC_TIME تُستبدل جميع الماكرو بتعبير ثابت ذي قيمة عدد صحيح وتُستخدم القيمة الناتجة عن التعبير مكان الوسيط category في الدالة setlocale (يمكن تعريف أسماء أخرى أيضًا، ويجب أن يبدأ كل منها بـ LC_X، إذ يمثّل X المحرف الأبجدي الكبير)، ويُستخدم النوع struct lconv لتخزين المعلومات المتعلقة بتنسيق القيم الرقمية، ويُستخدَم CHAR_MAX للأعضاء من النوع char للدلالة على أن القيمة غير متوافرة في الإعدادات المحلية الحالية. يحتوي lconv على عضو واحد على الأقل من الأعضاء التالية: 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; } العضو الاستخدام تمثيله في إصدارات سي المحلية ملاحظات إضافية char *decimal_point يُستخدم المحرف للفاصلة العشرية في القيم المنسقة غير المالية. "." --- char *thousands_sep يُستخدم المحرف لفصل مجموعات من الخانات الواقعة على يسار الفاصلة العشرية في القيم المنسقة غير المالية. "" --- char *grouping يعرّف عدد الخانات في كل مجموعة في القيم المنسقة غير المالية، وتحدد القيمة CHAR_MAX أنه لا يوجد أي تجميع إضافي مطلوب، بينما تحدد القيمة 0 أنه يجب تكرار العنصر السابق للخانات الرقمية المتبقية، وإذا استُخدمت أي قيمة أخرى فهي تمثل قيمة العدد الصحيح المُمثل لعدد الخانات التي تتألف منها المجموعة الحالية (المحرف اللاحق في السلسلة النصية يُفسَّر قبل التجميع). "" يحدد "\3" أن الخانات يجب أن تجمع كل ثلاثة في مجموعة ويشير محرف الإنهاء الفارغ terminating null في السلسلة النصية إلى تكرار \3. char *int_curr_symbol تُستخدم المحارف الأولى الثلاث لتخزين رمز العملة العالمي الأبجدي لإصدار سي المحلي، بينما يُستخدم المحرف الرابع للفصل بين رمز العملة العالمي والكمية النقدية. "" --- char *currency_symbol يمثل رمز العملة للإصدار المحلي الحالي. "" --- char *mon_decimal_point المحرف المُستخدم مثل فاصلة عشرية عند تنسيق القيم النقدية. "" --- char *mon_thousands_sep يمثل فاصل مجموعات خانات الأرقام ذات القيم المنسقة بتنسيق نقدي. "" --- char *mon_grouping يعرف عدد الخانات في كل مجموعة عند تنسيق قيم نقدية، وتُفسّر عناصره على أنها جزء من التجميع "" --- char *positive_sign السلسلة النصية المُستخدمة للدلالة على قيمة نقدية غير سالبة. "" --- char *negative_sign السلسلة النصية المُستخدمة للدلالة على قيمة نقدية سالبة. "" --- char int_frac_digits عدد الخانات التي تُعرض بعد الفاصلة العشرية في قيمة نقدية منسقة عالميًا. CHAR_MAX --- char frac_digits عدد الخانات التي تُعرض بعد الفاصلة العشرية في قيمة نقدية غير منسقة عالميًا. CHAR_MAX --- char p_cs_precedes قيمة 1 تدل على وجوب إتباع currency_symbol بالقيمة عند تنسيق قيمة غير سالبة نقدية، بينما تدل القيمة 0 على إسباق currency_symbol بالقيمة. CHAR_MAX --- char p_sep_by_space قيمة 1 تدل على تفريق رمز العملة من القيمة بمسافة فارغة عند تنسيق قيمة غير سالبة نقدية، بينما تدل قيمة 0 على عدم وجود أي مسافة فارغة. CHAR_MAX --- char n_cs_precedes تشابه p_cs_precedes ولكن للقيم النقدية السالبة. CHAR_MAX --- char n_sep_by_space تشابه p_sep_by_space ولكن للقيم النقدية السالبة. CHAR_MAX --- char n_sign_posn يشابه p_sign_posn ولكن للقيم النقدية السالبة. CHAR_MAX --- char p_sign_posn يمثل موقع positive_sign للقيم النقدية المنسقة غير السالبة. CHAR_MAX يتبع الشروط التالية: تُحيط الأقواس القيمة النقدية وcurrency_symbol. تسبق السلسلة النصية كل من القيمة النقدية و currency_symbol. تتبع السلسلة النصية القيمة النقدية و currency_symbol. تسبق السلسلة النصية القيمة currency_symbol. تتبع السلسلة النصية القيمة currency_symbol دالة setlocale لضبط الإعدادات المحلية يكون تعريف دالة setlocale على النحو التالي: #include <locale.h> char *setlocale(int category, const char *locale); تسمح هذه الدالة بضبط إعدادات البرنامج المحلية، ويمكن ضبط جميع أجزاء الإصدار المحلي باختيار القيم المناسبة لوسيط التصنيف category كما يلي: القيمة LC_ALL: تضبط كامل الإصدار المحلي. القيمة LC_COLLATE: تعديل سلوك strcoll و strxfrm. القيمة LC_CTYPE: تعديل سلوك دوال التعامل مع المحارف character-handling. القيمة LC_MONETARY: تعديل تنسيق القيم النقدية المُعادة من دالة localeconv. القيمة LC_NUMERIC: تعديل محرف الفاصلة العشرية لتنسيق الدخل والخرج وبرامج تحويل السلاسل النصية. القيمة LC_TIME: تعديل سلوك strftime. يمكن ضبط قيم الإعدادات المحلية إلى: "C" تحديد البيئة ذات المتطلبات الدنيا لترجمة سي C "" تحديد البيئة الأصيلة المعرفة حسب التطبيق قيمة معرفة بحسب التنفيذ تحديد البيئة الموافقة لهذه القيمة البيئة الافتراضية عند بداية البرنامج موافقة للبيئة التي نحصل عليها عند تنفيذ التعليمة التالية: setlocale(LC_ALL, "C"); يمكن فحص السلسلة النصية الحالية المترافقة مع تصنيف ما بتمرير مؤشر فارغ null قيمةً للوسيط locale؛ نحصل على السلسلة النصية المترافقة مع التصنيف category المحدد للتوطين الجديد إذا كان من الممكن حصول التصنيف المحدد، وتُستخدم هذه السلسلة النصية في استدعاء لاحق للدالة setlocale مع تصنيفها المترافق لاستعادة الجزء الموافق من إعدادات البرنامج المحلية، وإذا كان التحديد غير ممكن الحصول نحصل على مؤشر فراغ دون تغيير الإعدادات المحلية. دالة localeconv يكون تصريح الدالة على النحو التالي: #include <locale.h> struct lconv *localeconv(void); تُعيد هذه الدالة مؤشرًا يشير إلى هيكل من النوع struct lconv، ويُضبط هذا المؤشر طبقًا للإعدادات المحلية الحالية ويمكن تغييره باستدعاء لاحق للدالة localconv أو setlocale، ويجب ألّا يكون الهيكل قابلًا للتعديل بأي طريقة أخرى. على سبيل المثال، إذا كانت إعدادات القيم النقدية المحلية الحالية مُمثّلةً حسب الإعدادات التالية: IR£1,234.56 تنسيق القيم الموجبة (IR£1,234.56) تنسيق القيم السالبة IRP 1,234.56 التنسيق العالمي يجب أن تحمل الأعضاء التي تمثّل القيم النقدية في lconv القيم التالية: int_curr_symbol "IRP " currency_symbol "IR£" mon_decimal_point "." mon_thousands_sep "," mon_grouping "\3" postive_sign "" negative_sign "" int_frac_digits 2 frac_digits 2 p_cs_precedes 1 p_sep_by_space 0 n_cs_precedes 1` n_sep_by_space 0 p_sign_posn CHAR_MAX n_sign_posn 0 ترجمة -وبتصرف- لقسم من الفصل Libraries من كتاب The C Book. اقرأ أيضًا المقال التالي: القيم الحدية والدوال الرياضية في لغة سي C المقال السابق: مقدمة إلى مكتبات لغة سي C التعامل مع المحارف والسلاسل النصية في لغة سي C مدخل إلى المصفوفات في لغة سي C
-
شاع في السنوات الماضية مع تطوّر التقنية في حياتنا ودخولها لكل جوانبها مصطلح الأمن السيبراني Cybersecurity، كما أن الطلب تزايد عليه بالنظر إلى أنّ أي مؤسسة تستخدم تقنيات الحاسوب بحاجة لحماية بنيتها التحتية ضدّ الهجمات الخبيثة والمخترقين. تُشير بعض الإحصاءات التي أجرتها الجمعية الدولية للضمان الاجتماعي ISSA عام 2021 آنذاك إلى أن 57 بالمئة من المؤسسات تعاني من نقص بخصوص مختصّي الأمن السيبراني، وإن دلّت هذه الأرقام على شيء فهي تدلّ على نموّ هذا المجال وتزايد الطلب عليه. فما هو اختصاص الأمن السيبراني؟ وما هي مهام مختص الأمن السيبراني؟ وكيف يمكنك البدء بتعلم هذا المجال؟ هذا ما سنناقشه ضمن هذا المقال. تاريخ الأمن السيبراني كانت الحاجة للأمن السيبراني واضحة منذ ظهور شبكة الإنترنت وانتشارها في ثمانينيات القرن الماضي، إذ شهدنا ظهور مصطلحات متعلقة بهذا المجال مثل الفايروس Virus ومضاد الفيروسات Anti-virus وغيرها، إلا أن التهديدات السيبرانية لم تكن بالتعقيد والصعوبة التي هي عليه الآن. أولى ظهور لبرمجية خبيثة malware كان في عام 1971 باسم كريبر Creeper إذ كان برنامجًا تجريبيًا كُتب بواسطة بوب توماس في شركة BBN، تلاه بعد ذلك ظهور أول مضاد فيروسات في عام 1972 باسم ريبر Reaper الذي أنشئ بواسطة راي توملنسون بهدف القضاء على البرمجية الخبيثة كريبر. مع وصول شبكة الإنترنت إلى كافة بقاع العالم ومع التحول الرقمي الحاصل في كافة قطاعات الحياة اليوم أهمها قطاعات البنى التحتية المدنية من أنظمة تحكم وأنظمة الطاقة والاتصالات والمياه وقطاع الصحة والقطاعات المالية والمصرفية وقطاع النقل والطيران وغيرها، بدأت الهجمات السيبرانية تأخذ بعدًا آخر، فتخيل ماذا سيحصل إن حصل هجوم سيبراني على أحد تلك الخدمات المهمة المفصلية أو تخيل حصول هجوم سيبراني على أحد أنظمة إدارة السدود أو الطاقة في بلد ما، هذا لا يقاس مع حصول هجمات على أفراد التي تكون دائرة الضرر فيها صغيرة جدًا، إذ ممكن أن تؤدي إلى شلل في الحياة وهنالك الكثير من أمثلة تلك الهجمات لا يسع المقال لذكرها هنا. أذكر مرة أنه حصل خلل في التراسل الشبكي في أنظمة إحدى المباني الخدمية مما أوقف العمل بشكل كامل في ذلك المبنى لتتراكم أعداد الناس وتشكل طوابير طويلة منتظرين عودة النظام للعمل أو ستتوقف معاملاتهم بالكامل، وأذكر مرة حصل خلل في نظام شبكة محطات الوقود لتخرج مجموعة كبيرة من المحطات عن العمل في المدينة وتصطف طوابير من السيارات منتظرة عودة الخدمة للعمل، وهذان مثالان عن عطل غير مقصود فما بالك لو كان مقصودًا وناجمًا عن هجوم سيبراني منظم؟ تخيل ماذا سيحصل، لذا كانت أهمية الأمن السيبراني بأهمية الحاجة إليه والضرر الحاصل دونه. ما هو الأمن السيبراني؟ يُعرف الأمن السيبراني Cybersecurity بأنه عملية تأمين وحماية الأنظمة الرقمية والشبكات الإلكترونية وكل ما يتعلق بالأجهزة الرقمية وتكنولوجيا المعلومات الرقمية ضدّ أي هجمات رقمية أو تسمى هجمات سيبرانية Cyber Attacks. تستهدف الهجمات السيبرانية تخريب أو تعطيل أو سرقة شبكة أو نظام حاسوبي، أو دخولًا غير مصرّح به إليهما، وقد يكون الهدف من هذه الهجمات الوصول إلى بيانات ما لتعديلها أو تخريبها، أو ابتزاز الفرد أو المؤسسة مقابل هذه البيانات، أو أن تكون ذات هدف تخريبي تهدف لإيقاف عمل المؤسسة. تأتي هنا مهمّة الأمن السيبراني ألا وهي وقف هذه الهجمات السيبرانية عن طريق الكشف عن الفيروسات الرقمية وتعطيلها وتأمين وسائل اتصال مشفّرة وآمنة تضمن تبادل البيانات الحساسة دون خطر تسريبها أو قدرة الوصول إليها من المخترقين. يستخدم مختصّ الأمن السيبراني لتحقيق ذلك مجموعة من الأدوات والتقنيات مثل جدران الحماية (النارية) Firewalls وأنظمة كشف التسلل Intrusion Detection Systems أو اختصارًا IDS، بالإضافة إلى إجراء اختبارات الأمان وتعريف حدود واضحة (صلاحيات المستخدم، أماكن تخزين البيانات الحساسة، …إلخ). فوائد الأمن السيبراني يستند الأمن السيبراني على ثلاث مبادئ وهي الخصوصية Confidentiality والسلامة Integrity والتوفر Availability، ويرمز إلى هذه المبادئ بشكل كامل بالاختصار CIA: الخصوصية: ضمان الوصول إلى الأنظمة والبيانات فقط للأشخاص المُصرَّح بهم، بإجراء عمليات التعديل البيانات واستعادتها والاطّلاع عليها. السلامة: ويُقصد بها سلامة البيانات وهي ضمان أن البيانات يمكن الاعتماد عليها بشكل دقيق وصحيح، دون أن تُغيَّر من قبل أطراف أخرى غير مصرّح لها بالتغيير (وهو ما يضمنه المبدأ السابق). التوفر: ضمان أن الأطراف المُصرّح لها بالوصول إلى البيانات تستطيع الوصول إليها بأي وقت دون مشاكل وبشكل مستمرّ، وهذا يتطلب المحافظة على سلامة التجهيزات (العتاد الصلب) وسلامة البنية التحتية والنظام الذي يحتوي على البيانات ويعرضها. التطبيق الصحيح للمبادئ الثلاث السابقة يضمن لنا حماية بياناتنا الشخصية ويوفّر بيئة عمل مريحة وآمنة في المؤسسات التي تعتمد على التقنيات والأنظمة الحاسوبية، مما ينعكس بالإيجاب على سمعة المؤسسة وأدائها. أضِف إلى ذلك تفادي الخسارات في حال وقوع هجوم سيبراني على المؤسسة، سواءً أكانت ماديّة (تعطّل بنى تحتية أو سرقة بيانات أو ابتزاز) أو معنويّة (خسارة المؤسسة سمعتها وثقة جمهورها). إذ تُشير إحصاءات أجرتها مجلة Cybercrime المختصة بمجال الأمن السيبراني في عام 2021 إلى أن الجرائم السيبرانية تسببت بخسائر قيمتها 6 تريليون دولار أمريكي، دعونا نقارن هذا الرقم لفهم ضخامته، إذ أنه سيأتي ثالثًا إذا أردنا وضعه ضمن الناتج المحلي الإجمالي لدول العالم بعد الولايات المتحدة الأمريكية والصين! ومن المقدّر أن تزداد الخسائر باستمرار بحلول عام 2025 إلى 10.5 تريليون دولار أمريكي. دعنا لا ننسى أيضًا مهمة مميزة للأمن السيبراني ألا وهي استرداد البيانات واسترجاعها إن حصلت عملية اختراق أو تخريب، إذ تُعدّ مهمة استعادة البيانات بسرعة من أهم مهام مختص الأمن السيبراني. وجود فريق أمن سيبراني مختص لصدّ الهجمات والتعرّف عليها أمر لا غنى عنه، خصوصًا في القطاعات الحساسة مثل المؤسسات الحكومية والبنوك وشركات الطيران. لكن هل تحتاج جميع المؤسسات إلى الأمن السيبراني بقدر متساوٍ من الأهمية؟ في الحقيقة لا، فأهمية الأمن السيبراني بالنسبة لمدوّنة أو موقع شخصي ليست بقدرٍ مساوٍ لشركة طيران أو مؤسسة مصرفيّة. ألا أن هنالك قواعد عامّة يجب اتّباعها بغض النظر عن غرض المؤسسة وطبيعة نشاطها كإدارة البيانات الحساسة وحمايتها مثل كلمات المرور والمحافظة على آخر إصدار مستقر من العتاد البرمجي المُستخدم وأخذ نُسخ احتياطية من البيانات بشكل دوري. ما هو الفرق بين أمن المعلومات والأمن السيبراني؟ قد تتساءل ما الفرق بين أمن المعلومات والأمن السيبراني؟ إذ كثيرًا ما يُطرح هذا السؤال وهو ما سنجيب عليه في هذه الفقرة. يهتمّ الأمن السيبراني كما ذكرنا سابقًا بحماية الأنظمة والأجهزة الحاسوبيّة، ويتضمن ذلك أمن الشبكات والتطبيقات والسحابة cloud والبنية التحتية، وذلك بمنع الوصول غير المصرّح له لهذه الأنظمة بهدف التحكم بها أو الحصول على البيانات التي تحتويها. يركّز مجال أمن المعلومات على المعلومات بذات نفسها وكيفية حمايتها بغض النظر عن الوسيط الذي يحتويها بخلاف الأمن السيبراني (النظام أو العتاد الصلب)، ويُعدّ مجال أمن المعلومات خط الدفاع الثاني في حال التعرض لهجوم سيبراني واختراقه بحيث لا يستفيد المخترق من البيانات حتى وإن كانت بحوزته (إذ أن خط الدفاع الأول هنا هو الأمن السيبراني). مصطلحات شائعة في مجال الأمن السيبراني نذكر هنا بعض أكثر المصطلحات شيوعًا في مجال الأمن السيبراني ومعناها بشرح مقتضب: الجريمة السيبرانية Cybercrime: هي أي هجوم يقوم بها شخص أو مجموعة من الأشخاص ويكون الهدف فيها نظام حاسوبي بهدف التحكم به أو الحصول على بيانات بشكل غير مُصرَّح به، إما بهدف التخريب أو الابتزاز. شبكة روبوتات Robot Network: تُعرف اختصارًا باسم Botnet وهي شبكة تتكون من آلاف أو ملايين الأجهزة المتصلة مع بعضها البعض والمصابة ببرمجية خبيثة malware، ويستخدم المخترقون هذه الشبكة لتنفيذ هجماتهم مثل هجمات الحرمان من الخدمة الموزّع DDoS attacks أو إرسال الرسائل المزعجة Spam. ** خرق بيانات Data Breach**: هي الحادثة التي يحصل بها المخترق على بيانات ما بعد نجاح هجمة سيبرانية، وعادةً ما تكون هذه البيانات بيانات شخصية حساسة مثل كلمات المرور أو بيانات مصرفية وغيرها. المخترق ذو القبعة البيضاء White hat وذو القبعة السوداء Black hat: يُقصد بهذين المصطلحين نيّة كل مخترق من عملية الاختراق إذ أن للمخترق ذو القبعة السوداء نيّة سيئة باستخدام البيانات التي يحصل عليها أو الثغرات الأمنية بالابتزاز المالي أو التخريب بينما يكون هدف المخترق ذو القبعة البيضاء الكشف عن هذه الثغرات وسدّها لحماية النظام بشكل أكبر وغالبًا ما تُدعى هذه الفئة من المخترقين بالمخترقين الأخلاقيين Ethical Hackers. أنواع تهديدات الأمن السيبراني على الرغم من تطوّر الهجمات السيبرانية مع مرور الوقت وزيادة تعقيدها إلا أن هناك العديد من أنواع التهديدات الشائعة التي يجب أن يكون مختصّ الأمن السيبراني ملمًّا بها وبطريقة تحصين النظام ضدها. نذكر من أشهر أنواع تهديدات الأمن السيبراني ما يلي: التصيّد الاحتيالي Phishing: هي محاولة المخترق لخداع الضحية الهدف باتخاذ إجراءات غير آمنة وخاطئة، مثل إرسال رابط لصفحة دخول إلى موقع معيّن ومطالبتهم بتسجيل الدخول بحساباتهم في هذا الموقع، إلا أن الصفحة مُستنسخة وعملية التسجيل مزيّفة مما يسمح للمخترق بالحصول على بيانات حساب الضحية. هجمات الحرمان من الخدمة الموزّعة Distributed Denial of Service: تُعرَف اختصارًا بهجمات DDoS، وهي هجمات تتمّ عن طريق إغراق خادم النظام بسيل من المعلومات غير اللازمة باستخدام مجموعة من الأجهزة Botnet وذلك بهدف تعطيل الخدمة أو إبطائها، مما يتسبب بحرمان استخدام الخدمة للمستخدم أو حجبها. البرمجيات الخبيثة malware: هي برمجية يزرعها المهاجم في النظام الضحية بحيث تُنفّذ مجموعة من الأوامر والمهام غير المصرّح بها على جهاز الضحية، نذكر من هذه البرمجيات برمجيات الفدية ransomware التي تشفّر بيانات جهاز الضحية وتمنعه من الدخول إليها بهدف الحصول على مبلغ مالي، وبرمجيات التجسس spyware التي تراقب نشاط المستخدم على حاسبه بشكل سرّي وتسرق بيانات حساسة مثل كلمات السر. هجمات كلمة المرور password attacks: هجوم يحاول فيه المخترق تخمين كلمة المرور لملفات أو نظام، وتتم عملية التخمين عادةً بمساعدة أدوات مخصصة وليس يدويًا. لذا يُنصح على الدوام بالابتعاد عن كلمات السر القصيرة والمُستخدمة بكثرة مثل password123 أو qwerty، والكلمات الموجودة في القاموس دون إضافة رموز وأرقام بينها (لأن هذه الأدوات تجرّب الكلمات الموجودة في القواميس). هجوم الوسيط man-in-the-middle attack: هجوم يستطيع المخترق عن طريقه الوصول إلى البيانات المُرسلة بين المُرسل والمُستقبل عن طريق التنصّت إلى الاتصال ما بينهما، وتتضمّن معظم بروتوكولات نقل البيانات نوعًا من التشفير والمصادقة للحماية من هذا النوع من الهجمات مثل شهادة SSL في بروتوكول HTTPS. أنواع الأمن السيبراني ينقسم مجال الأمن السيبراني إلى عدّة مجالات فرعية أخرى، إذ يختص كل مجال بعيّن بجانب من الفضاء السيبراني. نذكر من هذه المجالات ما يلي: أمن الشبكات Network Security: يُعنى هذا المجال بحماية شبكات الحاسوب ويُحقَّق ذلك عن طريق بعض التقنيات مثل منع فقدان البيانات Data Loss Prevention اختصارًا DLP وإدارة الوصول إلى الهوية Identitiy Access Managment اختصارًا IAM وغيرها من التقنيات التي تجعل من الشبكات فضاءً آمنًا لمشاركة البيانات. أمن الهواتف المحمولة Mobile Security: عادةً ما يكون لهواتف موظّفي المؤسسة أو طاقمها وصول كامل لنظام المؤسسة وبياناتها، وبالنظر إلى أن الهواتف المحمولة هي الجهاز الأكثر استخدامًا عادةً لكل فرد فهذا يُضيف أهميّة زائدة على هذا المجال، إذ يجب تأمين هذه الأجهزة ضد الهجمات الخبيثة باستخدام مختلف التطبيقات (مثل تطبيقات المراسلة وغيرها). أمن السحابة Cloud Security: بدأت كافة المؤسسات بتبني تقنيات السحابة مؤخرًا، مما جعل أمن السحابة مجالًا مهمًا. تتضمن عملية حماية السحابة شروط التحكم بها والوصول إليها وكيفية توزيع بياناتها وهيكلة البنية التحتية ويندرج كل ذلك فيما يدعى باستراتيجية أمن السحابة cloud security strategy. أمن إنترنت الأشياء IoT Security: يُقصد بإنترنت الأشياء مجموعة الأجهزة التي تتواصل مع بعضها البعض وتراقب بيئتها المحيطة باستخدام الحساسات بالإضافة إلى إمكانية التحكم بها عبر الإنترنت. يحمي أمن إنترنت الأشياء هذه الأجهزة من استغلالها من طرف المخترقين عن طريق استخدام ثغرات في الأجهزة بذات نفسها أو وسيط الاتصال فيما بينها. أمن التطبيقات Application Security: التطبيقات التي تستخدم اتصال الإنترنت معرّضة لخطر الاختراق كأي نظام آخر يستخدم شبكات الإنترنت. يعمل أمن التطبيقات على حمايتها عن طريق منع التفاعلات الخبيثة مع التطبيقات الأخرى أو الواجهات البرمجية API. مجالات الأمن السيبراني يحتوي مجال الأمن السيبراني على عدّة مسميات وظيفية فرعيّة مخصصة عنه، ولكل من هذه المسميات مهامها المحدّدة ومتطلباتها، نذكر منها أهم مجالات الأمن السيبراني مع شرح بسيط لكل منها. كبير موظفي أمن المعلومات كبير موظفي أمن المعلومات Chief Information Security Officer اختصارًا CISO هو موظف ذو خبرة كبيرة مسؤول عن أمان المعلومات ضمن المؤسسة بشكل كامل، وتضمن مهامه تطوير طرق حماية البيانات وصيانتها وإدارة برامج المخاطرة، وعادةً ما يشغل هذا المنصب شخصٌ له باعٌ طويل في مجال أمن المعلومات وعمل في واحدة أو أكثر من وظائف أمن المعلومات بحيث يمتلك على خبرة كافية تمكّنه من قيادة فريق الأمن السيبراني في المؤسسة. مهندس الأمن تتضمّن مهام مهندس الأمن Security Architect تصميم نظم الأمان المُستخدمة في الدفاع عن المؤسسة من هجمات البرمجيات الخبيثة، إذ يُجري مهندس الأمن اختبارات لكشف الثغرات ونقاط الضعف في النظام بالإضافة لتزويد المعلومات المهمة إلى أعضاء الفريق الأمني الآخرين. يتطلّب هذا العمل خبرة في مجال هندسة المعلومات والشبكات وإدارة المخاطر بالإضافة إلى بروتوكولات الأمن وتشفير المعلومات. مهندس الأمن السيبراني يعمل مهندس الأمن السيبراني Cybersecurity Engineer على الإجراءات اللازمة التي تمنع نجاح هجوم سيبراني على أنظمة المؤسسة من شبكات وأجهزة، إذ يعمل على تطوير أنظمة دفاع سيبرانية ويعمل بشكل وثيق مع باقي أقسام المؤسسة للحفاظ على أمنها العام. يتطلّب هذا المنصب فهمًا جيدًا لكيفية عمل الشبكات وإدارة نظم التشغيل وهيكلتها بالإضافة إلى إتقان لغة البرمجة C (لأن لغة C تتعامل مع الحاسوب بمستوى منخفض مما يمنحك أريحية التعامل مع نظام التشغيل ومكوناته مقارنةً بلغات البرمجة عالية المستوى الأخرى مثل جافاسكربت وبايثون). محلل البرمجيات الخبيثة يعمل محلل البرمجيات الخبيثة Malware Analyst على فحص وتحليل التهديدات السيبرانية مثل الفيروسات وأحصنة طروادة Trojan horses والروبوتات bots لفهم طبيعتها وتطوير أدوات حماية للمدافعة ضدها، بالإضافة إلى توثيق طرق الحماية ضد البرمجيات الخبيثة وتجنبها. يتطلب هذا المنصب فهمًا لكل من نظام ويندوز ولينكس بالإضافة إلى معرفة بلغة البرمجة C/C++، واستخدام بعض الأدوات مثل IDA Pro وRegShot وTCP View. مختبر الاختراق أو المخترق الأخلاقي يُعرف مختبر الاختراق Penetration Tester بالمخترق الأخلاقي Ethical Hacker أيضًا، وهو مستشار أمني تتمثل مهامه باستغلال الثغرات الأمنية ونقاط الضعف في النظام بطريقة مماثلة لما سيفعله المخترق ذو النية السيئة التخريبية، إلا أن مختبر الاختراق يُطلِع فريق الأمان السيبراني في المؤسسة على الثغرات لتصحيحها، كما أنه يصمّم أدوات الاختراق ويوثّق نتائج الاختبار. المحلل الجنائي الرقمي يعمل المحلل الجنائي الرقمي Computer Forensics Analyst بعد حدوث هجوم سيبراني، إذ يجمع الأدلة الرقمية ويحاول استعادة البيانات المحذوفة أو المُعدَّل عليها أو المسروقة. يتطلّب هذا العمل معرفة بالشبكات والأمن السيبراني وفهم لقوانين الجرائم السيبرانية بالإضافة إلى مهارات تحليلية والانتباه للتفاصيل الدقيقة. كيف أبدأ بتعلم تخصص الأمن السيبراني؟ إن أردت البدء بتعلم الأمن السيبراني، فهذا يعني أنه عليك أن تبدأ بتعلم بعض المفاهيم والأدوات الأساسية في هذا المجال ألا وهي: تعلم أساسيات البرمجة، وذلك باختيار لغة برمجة معيّنة (يُفضّل البدء بلغة C أو C++ إن أردت دخول مجال الأمن السيبراني لأن اللغتين تتعامل مع الحاسوب على مستوى منخفض مما يمنحك أريحية التحكم بنظام التشغيل وأجزاء النظام الأخرى). فهم كيفية عمل أنظمة التشغيل وبنيتها، ننصحُك هنا بقراءة كتاب أنظمة التشغيل للمبرمجين كيفية عمل قواعد البيانات التي تخزّن بيانات أي نظام حاسوبي وعمليّة تصميمها، يمكنك الاطّلاع على كتاب تصميم قواعد البيانات للحصول على فهم أوّلي حول هذا الموضوع. إتقان التعامل مع سطر الأوامر command line بمختلف أوامره البسيطة والمتقدمة. فهم كيفية عمل الشبكات وكيف تتواصل الأجهزة مع بعضها البعض وتتبادل البيانات باستخدام بروتوكولات الاتصال المختلفة. التعامل مع أحد توزيعات لينكس الموجهة للأمن السيبراني والاختراقات بتمرّس، ونذكر منها توزيعة ريدهات Redhat لإدارة الخوادم (التي ستمكنك من الحصول على شهادة RHCSA وشهادة RHCE) ولينكس كالي Kali Linux، إذ تحتوي هذه التوزيعات على أدوات أساسية للتعامل مع الشبكات ومهام الأمن السيبراني. ماذا بعد؟ اختر مجالًا لتركّز عليه من المجالات السابقة التي ذكرناها، إذ أنّ مسار التعلم الخاص بك سيختلف بحسب توجهك، وهناك بعض الشهادات التي يجب أن تمتلكها لتزيد من فرصك في الحصول على عمل بحسب المجال الذي تختاره. على سبيل المثال تُعدّ شهادة CEH هامة لمختبر الاختراق -أو المخترق الأخلاقي- وشهادة CHFI هامة للمحلل الجنائي الرقمي. نرشّح لك أيضًا كتاب دليل الأمان الرقمي للاستزادة وتعلّم المزيد بخصوص الأمن السيبراني والاختراق. المصادر: What is Cybersecurity? Everything You Need to Know | TechTarget Top 20 Cybersecurity Terms You Need to Know (simplilearn.com) What Is Cybersecurity | Types and Threats Defined | Cybersecurity | CompTIA How To Learn Cybersecurity on Your Own A Basic Guide On Cyber Security For Beginners 2022 Edition | Simplilearn The Life and Times of Cybersecurity Professionals 2021 - Volume V - ISSA اقرأ أيضًا الهجمات الأمنية Security Attacks في الشبكات الحاسوبية تأمين الشبكات اللاسلكية كيف نخفف من هجمات DDoS ضد موقعنا باستخدام CloudFlare 7 تدابير أمنية لحماية خواديمك
- 1 تعليق
-
- 2