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

Naser Dakhel

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

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

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

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

  1. يعد انتشار الذكاء الاصطناعي والروبوتات أمراً مثيراً للفضول لمعظم الأشخاص حول العالم، فهو يعدهم بمستقبلٍ أفضل وأسهل للبشرية، فهو أحد سبل الراحة والرفاهية إضافةً إلى كونه يسهّل عمل العديد من الأشخاص في قطّاع التكنولوجيا. ولتستطيع صناعة وهندسة الروبوتات عليك أن تتقن اللغة التي تتحدثها وتتواصل مع العالم الخارجي من خلالها، وهنا يأتي دور لغات برمجة الروبوتات، التي تمنح الروبوتات القدرة والذكاء لإنجاز المهام بشكلٍ مشابهٍ للأسلوب البشريّ. سنتحدّث عن كيفيّة برمجة الروبوتات وأشهر اللغات المستخدمة في هذا المجال كما سنخبرك بالمسار الصحيح الذي يجب أن تسلكه لتكون مبرمج روبوتاتٍ ناجحاً وتشارك في بناء هذا المستقبل الواعد. ما هي برمجة الروبوتات تعرّف برمجة الروبوتات بأنها التعليمات المحددة المدخلة إلى نظام التحكّم الخاص بالروبوت لتنفيذ مهام معيّنة. وتحدّد هذه التعليمات الطريقة التي يعمل بها الروبوت والمهام التي يقدر على تنفيذها. وتتمّ هذه العملية بأسلوبين: برمجة الروبوتات بشكل مباشر: وذلك من خلال تحريك ذراع الروبوت، على سبيل المثال عبر سلسلة من الوضعيات التي تُسجَّل وتُحفظ في أنظمة التحكم الخاصة بالروبوت. برمجة الروبوتات بشكل منفصل: وهي عندما يقوم المبرمج بكتابة تعليمات دقيقة ومفصّلة على الحاسوب للتحكم بحركات الروبوت ومن ثمّ يحمل هذه التعليمات على الروبوت. ولبرمجة الروبوت، نستخدم العديد من اللغات البرمجية، وجميعها فعّالة وقادرة على أداء المهمة إلا أن أفضل لغات برمجة الروبوتات هي C أو C++‎ و Python و Java وغيرها من اللغات التي سنتعرف عليها بعد قليل، وتختلف اللغات المستخدمة باختلاف الشركات المصنعة التي غالباً ما تعتمد على برامجها الخاصة لبرمجة الروبوتات، ما يعني أنّ قدرتك على العمل مع روبوت من تصنيع شركة تجارية معينة لا يعني بالضرورة قدرتك على العمل مع روبوت مصنّع من قبل شركةٍ أخرى. لغات برمجة الروبوتات تنقسم اللغات التي سنناقشها هنا ما بين مستوى مبتدأ ومستوى متقدّم، ويعتمد قرارك على الاعتماد على إحدى اللغات بناءً على مستواك البرمجي والتقني عمومًا. ما يعني أنّك ستبدأ رحلتك في مجال برمجة الروبوتات من مرحلةٍ متقدّمة إن كنت مهندس برمجيات قبل ذلك. وسواء كنت متخصصًا أو مبتدئًا من المهم أن تتعلم اللغة التي ستوصلك إلى حيث تريد أن تكون في مجال الروبوتات وتضع حياتك المهنية على المسار الصحيح. المستوى المبتدئ بدايةً؛ لا تركّز على الجانب النظري فقط وتبدأ بقراءة كتاب نظري في محاولةٍ لتعلم الخوارزميات المتقدمة المستخدمة في هذا المجال، وإلّا ستشعر بالملل وتفقد شغفك بعد عدة أيام. بدلًا من ذلك اقرن المعلومات النظري بالتنفيذ العملي، وذلك بالعمل على المشاريع المتعلّقة ببرمجة الروبوتات وبهذه الطريقة يكون لديك هدف واضح ويصبح مسار التعلم الخاص بك ببساطة هو كل ما تحتاج إلى معرفته لتحقيق هدفك. لذا، أثناء قيامك بمشاريع رائعة، ستكتسب المعرفة كمنتج ثانوي! وفي هذه الأثناء ستتعلم لغة البرمجة الأنسب بحسب المشروع الذي تعمل عليه. لكن لنبسّط الأمر قليلًا؛ سنحدّثك عن أفضل لغات برمجة الروبوتات للمبتدئين التي تتميّز بخصائص تعطيها الأفضلية على لغات البرمجة الأخرى مثلسهولة الاستخدام، وعتمادها على كتل شيفرات برمجية جاهزة مسبقًا بدلاً من كتابتها من الصفر، واستخدامها لواجهات متناسبة مع خبرات المبتدئين من مجالات البرمجة. من أفضل لغات برمجة الروبوتات للمبتدئين نذكر: لغة سكراتش Scratch لغة بلوكلي Blockly لغة ليغو مايند ستورمز LEGO Mindstorms لنوضح أبرز مميزات كل لغة من هذه اللغات. سكراتش Scratch لغة سكراتش Scratch هي لغة برمجة تعتمد على سحب وإفلات كتل الشيفرات البرمجية بشكل أساسيّ، وهي مجانية تماماً ولا تتطلب ترخيصًا للاستخدام، فقد صممت لخلق بيئة عملٍ برمجية للطلاب المهتمين بمجال برمجة الروبوتات، ولسهولة الاستخدام، يمكن كتابة التعليمات البرمجية للغة سكراتش باستخدام محررها غير المتصل بالإنترنت أو تنزيلها على جهازك. حيث أنّ كل ما تحتاجه هو جهاز كمبيوتر ومتصفح ويب حديث. ما يميّز لغة سكراتش هو مجتمعها الموجّه للمبتدئين، لذا ستجد عشرات الدروس التعليمية والأمثلة على مختلف الجوانب والمزايا من سكراتش. يمكنك الاطّلاع على إمكانيات اللغة عن طريق تجربة مشاريع عملية نفّذها أعضاء من المجتمع والتعلّم منها عن طريق مراجعة الشيفرة الخاصة بالبرنامج، كما يمكنك البدء بتعلّم مزايا سكراتش عن طريق الأدلة التعليمية. بلوكلي Blockly هي لغة برمجة مرئية طوّرتها Google. وهي مشابهة في عملها للغة Scratch حيث تسمح للمستخدمين بإنشاء برامج عن طريق سحب وإفلات كتل الشيفرات، وبالتالي تكون البرمجة أسهل على المبتدئين. كما تتميز Blockly بأنها مناسبة بشكل خاص للروبوتات، حيث تمكن المطورين من إنشاء برامج معقدة دون الحاجة إلى كتابة الشيفرات البرمجية من الصفر. وهذا يجعلها خيارًا مثاليًا للمبتدئين، أو أولئك الذين قد لا يمتلكون خبرة واسعة في البرمجة. ورغم أنها لغة للمبتدئين إلا أنها قادرة على برمجة الروبوتات بدءًا من الروبوتات التعليمية البسيطة إلى الروبوتات الصناعية المتقدمة. كما تتيح للمبرمجين العمل بشكل تعاوني، حيث يمكن مشاركة الشيفرات وتحريرها داخل المنصة. ليغو مايند ستورمز LEGO Mindstorms هي لغة برمجة بصرية مصممة خصيصًا لبرمجة الروبوتات باستخدام مجموعة ليغو مايندستورمز. وكما اللغات السابقة، تعتمد على واجهة سحب وإفلات لكتل الشيفرات البرمجية، مما يسهل على المبتدئين استخدامها، حتى لو لم يكن لديهم أي خبرة سابقة في البرمجة. أحد الفوائد الرئيسية لهذه اللغة أنها سهلة التعلم والاستخدام. حيث تسمح واجهة السحب والإفلات للمستخدمين بتحديد وترتيب الأوامر والوظائف المختلفة بسهولة، دون القلق بشأن بنية الجملة المعقدة أو المفاهيم البرمجية. بالتالي يمكن للمستخدمين إنشاء مجموعة واسعة من البرامج والمشاريع باستخدام هذه اللغة، بدءًا من المهام البسيطة مثل تحريك الروبوت للأمام أو الخلف، إلى سلوكيات أكثر تعقيدًا مثل تجنب العقبات أو متابعة الخط. وتعد هذه اللغة من أفضل لغات برمجة الروبوتات للمبتدئين حيث تجعل واجهتها سهلة الاستخدام وقابلية التكيف والتفاعلية منها أداة رائعة للتعلم والتجربة، في حين تسمح قدراتها القوية للمستخدمين بإنشاء برامج معقدة ومتطورة كما يكتسبون المزيد من الخبرة والمعرفة. المستوى المتقدّم لا يمكنك أن تعتمد على لغات مثل LEGO Mindstorms أو Blockly لباقي مسيرتك المهنية كمبرمج روبوتات، فعليًا هذه اللغات لا تزيد عن كونها شيئًا سهلًا للبدء به، وبعد فهمك لطبيعة عمل الروبوتات وكيفية برمجتها لابدّ من البدء بتعلّم لغات برمجة الروبوتات الفعلية التي تقدم ميزاتٍ أكثر تخصصًا وتعليماتٍ إضافية لم تكن قادرًا على إعطائها للروبوت عند استخدامك للغات البرمجة المخصصة للمبتدئين. هناك عدّة لغات معتمدة لمبرمجي الروبوتات لإنشاء أنظمة روبوتية معقدة ومتطورة. تشمل بعض أشهر هذه اللغات: بايثون Python سي C ماتلاب Matlab سي بلس بلس C++‎ جافا Java ليسب LISP لنناقش كلّ لغةٍ على حدًى ونتحدث عن أهم استخداماتها في المجال. لغة بايثون وهي إحدى أسهل لغات البرمجة وأكثرها شعبيةً بين المستخدمين، ذلك لكونها تمتلك مجموعة قويّة من المكتبات التي تسهّل تنفيذ الوظائف والتعليمات الأساسية. كما أنّ هناك حاجة إلى عدد أقل من أسطر الشيفرات البرمجية مع Python أيضًا، ما يجعلها أسرع في الاستخدام والتعلم من C و C++‎ و Java. تعمل لغة Python على تقليل وقت البرمجة من خلال إلغاء الحاجة إلى تحديد أنواع المتغيرات، والسماح بكتابة الشيفرة البرمجية داخل البرنامج النصي ذاته. ونظرًا لاستخدامها الواسع وشعبيتها، تمتلك بايثون أيضًا مجتمعًا كبيرًا من المبرمجين، والذي يمكن أن يكون مصدرًا ممتازًا للمبتدئين للاستفادة منه أثناء مرحلة التعلم. يمكنك البدء بتعلم لغة بايثون من خلال دورة تطوير التطبيقات باستخدام لغة بايثون المقدّمة من أكاديمية حسوب. لغة سي C (باستخدام آردوينو) آردوينو Arduino هي إحدى المنصات الشائعة لبرمجة الروبوتات باستخدام لغة C، فهو متحكّم مفتوح المصدر يعتمد على أجهزة وبرامج سهلة الاستخدام. صمم ليكون متاحًا للمبتدئين مع التركيز على إمكانياته بحيث يمكن لمستخدميه المتقدمين بناء مشاريع معقدة. باستخدام لغة C مع آردوينو، يمكن للمبرمجين الاستفادة الكاملة من قدرات المتحكم الصغير Microcontroller وتنفيذ خوارزميات وأنظمة تحكم مخصصة، كما تسمح لغة سي للمبرمجين بكتابة شيفرات فعالة ومحسّنة لتطبيقات الروبوتات ، مثل التحكم في المحركات والحساسات وغيرها. بالإضافة إلى ذلك، يوفر أردوينو مجموعة واسعة من المكتبات والأدوات التي تبسط عملية التطوير، وهذا يجعلها منصة مثالية للهواة والطلاب والمحترفين على حدٍّ سواء. بشكل عام، لغة C هي أداة قوية لبرمجة الروبوتات، واستخدامها مع أردوينو يجعلها متاحة لجمهور أوسع وبالتالي الاستفادة منها بشكل أكبر. لغة ماتلاب MATLAB تعد ماتلاب من أفضل لغات برمجة الروبوتات على الإطلاق فهي قادرة على تحليل البيانات وتشغيل المحاكاة ، وتطوير وتنفيذ أنظمة التحكم في الروبوتات بواسطة واجهات مصممة خصيصًا لأجل هذه المهمّة. عند استخدامها مع مجموعة أدوات Robotics Toolbox for MATLAB، التي تتضمن وظائف للحركة والديناميات وتوليد المسارات، كما يمكن للمطورين إنشاء أنظمة لمحاكاة ذراع الروبوت. تدمج هذه اللغة بين البيئة سهلة الاستخدام والكفاءة العالية وتحقيق أكبر قدرٍ ممكن من الاستفادة حيث عندما يتمّ تحليل المعلومات وبناء أنظمة التحكم. كما تساعد على التخلص من الأخطاء التي تظهر عند التنفيذ وذلك عبر السماح للمبرمجين بتحديد المشكلات أثناء مرحلة النمذجة الأولية بدلًا من اكتشافها لاحقًا بعد إنتاج الروبوت وتصنيعه مما سيكبّدك كلفةً أكبر وعملًا أكثر. لغة سي بلس بلس ++C تقف لغتا C و C++‎ إلى جانب بعضهما البعض لتشكّلا اللغتين الأساسيتين في عالم برمجة الروبوتات فهي توفّر مجموعةً كبيرة ومتنوعة من الأدوات والمكتبات والوظائف المفيدة في مجال الروبوتات. حيث يستخدمها معظم مهندسي الروبوتات الذين يعملون على برمجة الروبوتات بكفاءة وضمان أداءٍ عالٍ واستثمارها بالشكل الأمثل. واحدة من أعظم مزايا ++C كونها اللغة التي تتحكم في واجهة برمجة التطبيقات API لنظام التشغيل مباشرة. وهذا يعني أنها لا تحتاج إلى أي وسيط. بالتالي، يمكن للمبرمجين استخدام المكتبات السريعة الخاصة بالمنصة الأساسية. لغة البرمجة جافا تحظى لغة جافا بشعبية بين المبرمجين العاملين في مجال الذكاء الاصطناعي وبرمجة الروبوتات دونًا عن غيرها من لغات البرمجة حيث يمكن إنشاء شبكات عصبية من خلالها. ومع ذلك، لا تعدّ جافا الخيار الأول لبرمجة الروبوتات بسبب تكاليف تشغيل مكتبات جافا العالية وبطء سرعة المعالجة وقواعد بناء الجمل المعقدة. بالإضافة إلى تقديمها لبعض المزايا الفريدة في الروبوتات، مثل إدخال كشف الكلام وكشف اللغة في روبوتك باستخدام واجهة (Java Speech API) (JSAPI)، كما يمكنك استدعاء ردود الفعل البصرية من الروبوت باستخدام واجهة Computer Vision API of Java. بالإضافة إلى ذلك ، تحتوي جافا على مجموعة واسعة من واجهات برمجة التطبيقات API للذكاء الاصطناعي المصممة خصيصًا لاستخدامها في برمجة الروبوتات. ميزة أخرى لجافا هي آلة جافا الافتراضية Java Virtual Machine التي تسمح للمبرمجين بتقليل وقت البرمجة الإجمالي عن طريق السماح لهم باستخدام الشيفرة البرمجية ذاتها على أجهزة مختلفة. ليسب LISP هي إحدى أقدم لغات البرمجة المستخدمة لبرمجة الروبوتات وتستخدم لإنشاء تطبيقات الذكاء الاصطناعي حيث يتم كتابة نظام ROS - الإطار المفتوح المصدر المستخدم لتطوير تطبيقات الروبوت - بهذه اللغة. وتتميّز بإدارة التخزين التلقائي، والكتابة الديناميكية، وهياكل البيانات الشجرية، والتكرار، ووظائف الترتيب المرتفعة، والترجمة ذاتية الاستضافة وغيرها الكثير. طورت لغة LISP في الأصل للتدوين الرياضي التطبيقي في برامج الحاسوب. ومع ذلك، ستجد أن العديد من الأقسام الهامة لنظام ROS مكتوبة بلغة LISP. ولهذا السبب أصبحت أيضًا لغة حاسمة في مجال الذكاء الاصطناعي. وكلّ هذه الميزات تضع لغة LISP في مقدمة قائمة أفضل لغات برمجة الروبوتات. برامج محاكاة الروبوتات عالم برمجة الروبوتات يشكل المستقبل الواعد للمجال البرمجي، إلا أنه يأتي مع سلبيته الأبرز ألا وهي ارتفاع أسعار الروبوتات بشكلٍ يمنع المبرمجين من القدرة على القيام بالتجارب واكتساب الخبرات، ومن هذا المنطلق تمّ العمل على برامح محاكاة الروبوتات التي تُعرف بكونها برامج طوّرت لتخطيط مسار حركة الروبوت دون الاتصال بالإنترنت حيث تمثّل هذه البرامج حركة الذراع بدقة. يتضمن هيكل الروبوت العديد من المكونات بالإضافة إلى الذراع، وبالتالي يجب أن تكوّن فهمًا كاملًا لكيفية عمل الهيكل وتفاعله مع محيطه، مما يساعدك في تحديد فرص التحسين، وضمان تضمين كل عنصر مهم في الهيكل. يمكن فهم عمل الخلية بشكلٍ شامل باستخدام برنامج محاكاة مصنّع مع نموذج كامل للخلية، إذ يمكن برمجة الروبوت بشكل افتراضي. بحيث تتكامل حركات الروبوت مع المستشعرات وغيرها من المحفزات. أهمية برامج محاكاة الروبوتات تطورت عملية محاكاة الروبوتات بشكل طرديّ مع مرور الوقت لمواكبة القدرات المتزايدة للروبوتات الصناعية، بالإضافة إلى زيادة الطلب على الروبوتات المتقدمة من هذا النوع بما يتناسب مع زيادة تعقيد المنتج وتنوعه والتخصص لتلبية طلبات العملاء. وعند التفكير بالأمر، فإن السبب الرئيسي لاستخدام برامج محاكاة الروبوتات هو قدرتك على تصور العملية، على سبيل المثال، قد يكون أهم ما تحتاجه هو عرض كيفية عمل نظام الروبوت لفريقك. تسمح المحاكاة لزملائك في العمل والإدارة بتصور العملية. ويساعد على رؤية كيفية عمل الروبوت. إضافة إلى الكلفة المادية الأخفض مقارنةً مع أجزاء الروبوتات والقدرة على حل المشاكل والأخطاء قبل الوصول إلى مرحلة الإنتاج النهائية. برامج محاكاة الروبوتات تستمر برامج محاكاة الروبوتات في التطور كل عام، ما يضيف قيمةً ونجاحًا أكبر إلى مجال برمجة الروبوتات وخاصةً مع إطلاق محاكيات روبوتية مجانية مفتوحة المصدر بدأت في منافسة أداء البرامج التجارية. وتدعم معظم برامج محاكاة الروبوتات أيضًا مجموعة واسعة من لغات البرمجة مثل C و C++‎ وJava وMATLAB و LabVIEW و Python وأهمّها: تطبيق Webots Webots هو تطبيق متعدد المنصات ومفتوح المصدر يستخدم لمحاكاة الروبوتات. يوفر بيئة تطويرية كاملة لنمذجة وبرمجة ومحاكاة الروبوتات ويدعم مجموعة واسعة من المحاكيات بما في ذلك الروبوتات ذات العجلتين والأذرع الصناعية وروبوتات المشي والروبوتات النمطية والسيارات والطائرات بدون طيار والمركبات تحت الماء ذاتية الدفع والروبوتات المسارية ومركبات الفضاء وما إلى ذلك. ويمكن برمجة الروبوتات باستخدام هذا المحاكي بلغات C و C++‎ وبايثون Python و Java و MATLAB باستخدام واجهة برمجة تطبيقات بسيطة تغطي جميع الاحتياجات الأساسية للروبوتات. محاكي Gazebo محاكي Gazebo هو منصة مفتوحة المصدر ومجانية يمكن استخدامها لتصميم وتطوير واختبار ومحاكاة أي نوع من الروبوتات وهو مدعوم من قبل أنظمة تشغيل Linux و Windows و Mac، يأتي محاكي Gazebo أيضًا مع بعض نماذج الروبوتات مثل PR2 و DX و Irobot Create و TurtleBot، لتبدأ بسرعة حتى لو لم يكن لديك نماذج روبوت خاصة بك. كما يدعم Gazebo مجموعة واسعة من الحساسات، ويمكنك محاكاة الضوضاء وفشل الحساسات لمحاكاة المشاكل الحقيقية في العالم. محاكي V-REP أو CoppeliaSim يتوفر هذا المحاكي مجاناً لأغراض تعليمية، أو من الممكن أن تختار الإصدار الاحترافي إذا كنت تخطط لاستخدامه لمشاريع تجارية. يمكن تشغيل V-REP على أنظمة Windows و Linux و macOS، كما يدعم ستّ لغات برمجةٍ مختلفة. يمكنه التعامل بدقة مع تفاعلات الأجسام مثل التصادمات ونقاط الاتصال والتقاط. كما يدعم V-REP فيزياء الجسيمات لمحاكاة الهواء والماء بدقة، حتى تتمكن من نمذجة محركات الطائرات والمروحيات بدقة. محاكي NVIDIA ISAAC هذا هو محاكي روبوتات قابل للتوسعة، إذ يمكّن المطورين والباحثين من تصميم واختبار وتدريب الروبوتات القائمة على الذكاء الاصطناعي. يُشغَّل بواسطة Omniverse لتوفير بيئات افتراضية قابلة للتوسعة ومفصلة ودقيقة فيزيائياً. كما يدعم مجموعة متنوعة من التطبيقات بما في ذلك التلاعب والتنقل وإنشاء بيانات اصطناعية لتدريب البيانات. يدعم NVIDIA ISAAC وحدات البرامج التي يطلقون عليها GEMs. تتيح هذه الوحدات لك إضافة ميزات بسرعة إلى روبوتاتك مثل أنظمة التعرف المرئي للشبكات العصبية المدربة مسبقًا. محاكي Virtual Reality Simulator محاكي العوالم الافتراضيّة يوفّر تجربة غنيّة للمستخدمين، فهو يهدف في المقام الأول إلى استخدامه كمنصة تعليمية لتعليم الشباب أساسيات الروبوتات. كما تدعم عوالم الروبوت الافتراضية برمجة روبوتات LEGO Mindstorms باستخدام NXT-G أو LabVIEW. يمكنك أيضًا إنشاء نماذج روبوت مخصصة أو مستويات في المحاكي باستخدام نظام التمديد المدمج. مسار تعلّم مبرمج الروبوتات هندسة الروبوتات هي مجال ينمو يوماً بعد يوم يتضمن مزيجًا من تحليل البيانات والهندسة وعلوم الحاسوب. يستخدم الأشخاص العاملون في هذه المجالات البرامج والأجهزة الميكانيكية لتصميم وبناء واختبار الروبوتات وعملياتها المرتبطة بالآلات. ومع ذلك، فإن كل وظيفة في هندسة الروبوتات فريدة من نوعها، وغالبًا ما تعتمد الجوانب العملية للوظيفة في هذا المجال على خلفية الشخص. على سبيل المثال، يميل مهندسو الروبوتات الذين لديهم خلفية في البرمجة إلى التركيز أكثر على الجانب البرمجي، بينما يميل أولئك الذين لديهم خلفية في الهندسة الميكانيكية إلى العناصر الفيزيائية للروبوت. ما يعني أنّ المسار إلى هذا المجال يشمل عمومًا أربع خطوات رئيسية: الحصول على التعليم. كسب الخبرة. الانضمام إلى المشاريع ذات الصّلة. تقديم طلبات العمل. أولًا: دراسة هندسة الروبوتات هندسة الروبوتات هي مجالٌ جديد نوعاً ما، لذلك لا يوجد سوى عدد قليل من البرامج الرسمية المصممة لإعداد المتعلمين للحصول على وظيفة في هذا المجال. ومع ذلك، هناك العديد من المسارات البديلة المتاحة لك في هذا المجال - حتى تتمكن من اختيار المسار الذي يناسبك. والخطوة الأولى نحو مهنة في الروبوتات هي دراسة اختصاص الرياضيات أو الهندسة أو علوم الحاسوب. مع العلم أنّ؛ دراسة علوم الحاسوب تساعد على إعدادك لجوانب البرمجة في العمل، بينما ستساعد دراسة هندسة الميكانيك على إعدادك لبناء عتاد الروبوت وضبط تحركاته. ثانيًا: اكتساب المهارات المطلوبة بمجرد أن تدرس المجال أو الموضوع الذي تختاره، من المهم تطوير مهاراتك الشخصية، أو التدرب من خلال مشاريع شخصية في وقت فراغك، أو الحصول على تدريب في شركة روبوتية. وبعد الانتهاء من برنامج التدريب الخاص بك، قم بإجراء استبيان للمهارات التي اكتسبتها وقارنها بالمهارات المطلوبة لمهندس الروبوتات. تذكر أنه يجب عليك أن تكون ملمًا بالبرمجة والإحصاء والرياضيات والهندسة والأتمتة. وبحسب نقاط الصعف لديك يمكنك أن تعرف المجال الذي يجب أن تقوّي نفسك فيه وتسجل دوراتٍ دراسيّة إضافية. ثالثًا: الانضمام إلى تجمّع مهني يتعلق بالمجال الانضمام إلى المشاريع والجماعات المهنيّة لا يقدّم لك عملاً وحسب، بل يضيف خبرةً ويجعل منك مهندساً أفضل وأكثر مهارة. تأسست جمعية تقدُّم الذكاء الاصطناعي (AAAI) في عام 1979، وهي جمعية غير ربحية تركز على الذكاء الاصطناعي، ملتزمة بـ "تعزيز الفهم العلمي للآليات التي تكمن وراء الفكر والسلوك الذكي وتجسدها في الآلات"، وكذلك جمعية الروبوتات والأتمتة IEEE، والانضمام إلى إحدى هذه الجمعيات من خلال حضور ورش العمل والمؤتمرات القيمة، يؤدي إلى إتاحة فرصة الوصول إلى آلاف من مهندسي الروبوتات للتعلم منهم وإضافتهم إلى شبكة اتصالاتك المهنية. رابعًا: التقدم للوظائف تعتبر الروبوتات مجالًا تنافسيًا، ما يعني أنه عند التقدم للوظائف، يجب أن تكون مفكرًا واستراتيجيًا. ما هي الوظائف التي تهتمّ بها؟ وما هي المؤهلات التي لديك؟ وعند بناء سيرتك الذاتية، تأكد من التركيز على جميع الخبرات والدروس ذات الصلة بالروبوتات، سواء كان ذلك تخصص رياضيات في الجامعة أو روبوت قمت ببنائه كمشروعٍ خاص بك. وحضّر نفسك لمقابلات العمل المحتملة، مثلاً استعرض بعضًا من أهم الأسئلة التي تطرح في مقابلات الروبوتات على LinkedIn؛ ستساعدك هذه الأسئلة على ممارسة الحديث عن بعض الموضوعات الأكثر أهمية التي قد يغطيها المقابل، ليكون لديك فهمٌ واضح عن هذه الوظيفة، وما يجعلك مؤهلاً جيدًا لها. الخاتمة تلعب لغات البرمجة دوراً عظيمًا وحاسمًا في مجال تطوير وبرمجة الروبوتات، لذلك من المهم أن تختار اللغة الأنسب للمجال الذي تريد التخصص به، مع ضمان دعم محاكي الروبوتات الذي ستستخدمه لهذه اللغة. يمكن للمبرمجين المبتدئين في مجال الروبوتات البدء بتعلم لغات البرمجة البسيطة مثل سكراتش أو بلوكلي وثمّ الانتقال إلى لغات أكثر تخصصاً مثل Python و C++‎ و Java، ثم التحول إلى إتقان لغات خاصة بالروبوتات مثل ROS. كما يمكن للانضمام إلى الجمعيات المهنية والمجتمعات التي تشاركك الشغف ذاته لبناء علاقات مهنية مع مهندسي روبوتات آخرين وحضور ورشات عمل ومؤتمرات متعلقة بالمجال. اقرأ أيضًا برمجة الروبوت: الدليل الشامل تحريك شخصية كرتونية عبر سكراتش في راسبري باي تعلم الآلة Machine Learning تعلم الذكاء الاصطناعي
  2. عالم تطوير الألعاب هو عالمٌ غنيّ بالإثارة والإبداع والتطور وأن تكون مبرمج ألعاب فيديو يعني أن تشارك في بناء عالمٍ يدخله ملايين الأشخاص يوميًا، حيث يوجد ملايين اللاعبين حول العالم المهتمين بمجال ألعاب الفيديو ومتابعة كل تطور حاصل فيه ، ما يعزّز فكرة العمل على بناء ألعاب جديدة تفوق توقعات اللاعبين وابتكار بيئات لعب استثنائية تنال استحسانهم وتحقيق مبيعات عالية. ولا عجب أن مجال تطوير الألعاب يعد واحدًا من أسرع مجالات البرمجة نموًا حول العالم، فسواء كان المجال الذي ترغب في العمل به هو التصميم الفني لشخصيات الألعاب وعوالمها، أو برمجة الألعاب، فإن هذه الصناعة تسمح لك بتطوير مهاراتك وتحقيق أحلامك المهنية، ومن الجدير بالذكر أنّ متوسط دخل الوظائف البرمجية المرتبطة بالألعاب الإلكترونية مرتفعٌ مقارنةً مع متوسط دخل باقي الوظائف لا سيما إذا تمكنك من الوصول لمرحلة احترافية في هذا التخصص. سنحدّثك في هذا المقال عن أبرز النقاط الأساسية حول مبرمج ألعاب الفيديو الناجح، وما هي مهامه، وكيف يختار مساره المهنيّ، ونجيبك عن العديد من التساؤلات الأخرى التي قد تخطر ببالك حول هذا التخصص المميز. ما هي مهام مبرمج الألعاب؟ يتطلّب تطوير الألعاب مزيجًا من المهارات الإبداعية والتحليلية، وبالتالي على مبرمج الألعاب الناجح أن يمتلك مهارات العمل وحده أو مع فريق وذلك اعتمادًا على نوع اللعبة التي يعمل عليها وهل سيطورها بمفرده أو ضمن استوديو ألعاب مع فريق عمل متكامل، وأن يكون قادرًا على التفكير خارج الصندوق لابتكار أساليب جديدةٍ وجذابة للعب، وأن يكون منظّمًا بما يكفي لاتباع جدولٍ زمنيٍّ دقيق يمشي عليه طوال فترة تطوير اللعبة والتفاعل مع أعضاء الفريق، والعمل ضمن حدود الميزانية والزمن المخصص لإنجاز اللعبة المطلوبة. وهنا يأتي السؤال؛ ما هي مهام مبرمج الألعاب الأساسية؟ في الواقع تتمثل مهمة مبرمج الألعاب الأساسية في تطوير البرامج اللازمة لإنشاء ألعاب الفيديو، من خلال المنصات والمحركات التي تدعم اللعبة التي يعمل عليها، وكتابة الشيفرات البرمجية المناسبة لبناء لعبةٍ مثالية. وتقسم الأدوار عادة بين كلٍّ من المبرمج والمطوّر والمصمّم، الذين يعملون معًا لصناعة اللعبة الإلكترونية وضمان سير العمل بسلاسة؛ حيث يعمل المبرمجون على كتابة منطق اللعبة البرمجي وتفاعل الكائنات مع بعضها البعض في عالم اللعبة، بينما يعمل المصممون على تصميم هذه الكائنات والانتقالات الخاصة بها. بناءً على ما سبق تتركز مهام مبرمج الألعاب على النقاط التالية: إعداد بيئة التطوير المناسبة للعبة الفيديو. البحث عن مجموعة البرامج والمنصات التي سيتم استخدامها لدعم اللعبة. ضمان تحقيق التصميم الكامل للعبة وأداءها بكفاءة وجودة. العمل وفقًا لجدول زمني محكم والالتزام بالميزانية. إنتاج نماذج أوليّة لاختبارها وتحديد الأخطاء والإصلاحات. إجراء اختبارات ضمان الجودة والاستجابة للآراء والتعليقات المختلفة. التعاون مع جميع الأقسام لحلّ المشكلات التقنية خلال عملية إنتاج اللعبة. توفير الدعم التقني المستمر بعد إطلاق اللعبة والعمل على ترقيتها لمواكبة التطورات التقنيّة. كيف تبرز نفسك كمبرمج ألعاب ناجح ضمن المنافسة؟ إذا كنت مهتمًا لتكون مبرمج ألعاب فعليك أن تضع في الحسبان أن المنافسة في هذ المجال كبيرة وهناك بعض المهارات التي لا بدّ أن تكون موجودة فيك لتتميز في برمجة الألعاب، فامتلاكك لهذه المهارات وتمكّنك منها سيمنحك أفضليّة على منافسيك وبقيّة المبرمجين الآخرين. إذ ينبغي أن يمتلك مبرمج الألعاب عمومًا خلفية واسعة في علوم الحاسوب ويتقن إحدى لغات برمجة الألعاب ويكون ماهرًا في التعامل مع أحد محركات الألعاب؛ لكنك ستكون مخطئًا إن اعتقدتّ أن هذه المهارات الفنية هي الوحيدة التي ستقرّر نجاحك وتضمن توظيفك، إذ تولي استوديوهات ألعاب الفيديو المهارات التالية في مبرمجي الألعاب انتباهًا شديدًا لعوامل أخرى من أبرزها ما يلي: التواصل الفعال: من الضروريّ أن يكون مبرمجو ألعاب الفيديو قادرين على التواصل فيما بينهم بشكلٍ صحيح ليتمكّنوا من فهم طبيعة العمل والمواد المتوفّرة والتقنيات المتاحة. العمل ضمن فريق: على الرغم من قدرتك على كون مبرمج ألعابٍ منفردًا إلّا أن تطوير الألعاب ضمن فريق سواء أكان مستقلًا أو في شركة يتطلّب قدرًا من التعاون مع بقية الأشخاص، لذلك يجب أن يتمتع المبرمجون بقدرات تعاونية ممتازة، بما في ذلك القدرة على التفاعل وتبادل الأفكار وتقديم التعليقات والآراء لتمكين الفريق من تحقيق أهدافه المشتركة. حل المشكلات: يجب على المبرمجين أن يكونوا قادرين على حل المشكلات بفعالية من خلال التفكير المنطقي والبحث والحكم السليم. الإبداع: يتيح الإبداع للمبرمجين التفكير في طرق جديدة لحل المشاكل المعقدة في ألعاب الفيديو والارتقاء إلى مستوى أعلى. القابلية للتكيف: نظرًا لأن صناعة ألعاب الفيديو تتغير باستمرار، فإن القابلية للتكيف والتآلف مع ظهور تقنيات جديدة تؤثر على عملية تطوير الألعاب مهارة حاسمة للمبرمجين. اختيار الأدوات المناسبة لبرمجة ألعاب الفيديو قبل أن تبدأ رحلتك في عالم برمجة الألعاب، عليك بداية تحديد الأدوات التي ستستخدمها. تتمثّل هذه الأدوات بشكل رئيس بلغة البرمجة المُختارة ومحرك الألعاب، وقد تلجأ لتعلّم لغة معينة إذا ناسبك محرك ألعاب معين يعمل بها أو بالعكس، وفيما يلي نسرد لك أهم أدوات مبرمج ألعاب الفيديو لنساعدك على اختيار ما يناسبك من بينها. لغة البرمجة تتنوّع لغات البرمجة في مهامها والمجالات التي تخدمها، ولبرمجة ألعاب الفيديو نصيب من لغات البرمجة المخصصة لها، لذلك إن كنت تريد أن تصبح مبرمج ألعاب فيديو محترف يجب أن تتعرف على لغات البرمجة المتخصصة في مجال برمجة ألعاب الفيديو. وهناك عدّة لغات يجب على أي مبرمج ألعاب فيديو تعلّمها ومن أبرزها: C++‎ C#‎ Java Python JavaScript ولكي لا تضيع بتعدّد الخيارات يمكنك التعرف على ميزات كل لغة على حدى وأيّ اللغات هي الأنسب للمجال الذي تبحث عنه من خلال قراءة مقال لغات برمجة الألعاب. بعد أن تتعلّم لغة البرمجة الأنسب للعبتك يجب أن تختار محرك الألعاب الذي يحقق الغاية المرجوّة من اللعبة. محرك الألعاب يسعى أيّ مبرمج ألعاب فيديو إلى اختيار محرك ألعاب يسهّل عليه عمله ويشكّل إطارًا خلّاقًا يعكس طبيعة اللعبة وأبعادها، فمحرك الألعاب هو البيئة المسؤولة عن تشغيل اللعبة، حيث يوفر لأي مبرمج ألعاب فيديو إطار عمل برمجيّ يتضمن التعليمات البرمجية والمكتبات المستخدمة والأدوات اللازمة لبرمجة وتطوير هذه اللعبة بسهولة تامة. لهذا تعدّ عملية اختيار محرك الألعاب الخطوة الأولى في طريق إظهار اللعبة إلى النّور، فمحرك الألعاب يوفر البنية التحتية الرقمية اللازمة لتشغيل عناصر اللعبة وجعلها تعمل بالشكل المطلوب. وقد شكّلت محركات الألعاب ثورة برمجية في مجال تطوير الألعاب، فقبل وجودها كان مبرمج ألعاب الفيديو يبرمج تفاصيل اللعبة من الألف إلى الياء ويكتب الشيفرات البرمجية اللازمة لتصميم اللعبة ونشرها، إلا أنه مع محركات الألعاب بات عمل كلّ مبرمج ألعاب فيديو أسهل بكثير من حيث التصميم والنشر وإنشاء ألعاب متعددة المنصات وصار كل ما تحتاجه لتصميم لعبة متكاملة اليوم هو تعلم استخدام أحد محركات الألعاب ومعرفة أساسية بإحدى لغات البرمجة التي يدعمها هذا المحرك على سبيل المثال: محرك يونيتي Unity بلغة سي شارب C#‎ محرك أنريل Unreal بلغة سي بلس بلس C++‎ محرك جودو Godot بلغة جي دي سكربت GDScript - أو سي شارب C#‎ محرك جيم ميكر GameMaker بلغة برمجة GML المعتمددة على لغات جافا سكريبت و C++‎ و C#‎ ولمطالعة المزيد عن محركات الألعاب ومميزاتها ولغات البرمجة المستخدمة في كل منها أنصحك بمطالعة مقال تعرف على أشهر محركات برمجة الألعاب الإلكترونية ما الفرق بين مبرمج ألعاب فيديو ومصمم ألعاب فيديو؟ الفرق الرئيس بينهما هو نوع العمل الذي يقومون به في عملية تطوير اللعبة. إذ يتولى مصمم ألعاب الفيديو إنشاء الرؤية العامة والمفهوم للعبة الفيديو. فهو يصمم حركة اللعبة والمستويات والشخصيات وخطوط القصة وعناصر أخرى تشكل اللعبة. ويعمل فريق تصميم الألعاب بشكل وثيق مع فريق البرمجة لضمان تنفيذ تصميم اللعبة بشكل صحيح. من ناحية أخرى، يتولى مبرمج ألعاب الفيديو برمجة منطق اللعبة. وهذا يتضمّن الجوانب التقنية للعبة، مثل إنشاء محرك اللعبة وتصميم واجهة المستخدم وتنفيذ حركات اللعبة، ويتواصل مع المصممين لضمان بناء اللعبة وفقًا لمواصفات التصميم. باختصار، يتعامل مصمم ألعاب الفيديو مع الرؤية الإبداعية للعبة، بينما يكون مبرمج ألعاب الفيديو مسؤولًا عن تحويل تلك الرؤية إلى حقيقة من خلال البرمجة والتنفيذ التقني. هل يجب أن أكون مبرمج ألعاب فيديو مستقلّ أم ضمن شركة؟ إذا كنت مستقلًا، فأنت مسؤولٌ عن عملك وفوائدك الخاصة وتطويره بالشكل الذي تريد، إلا أنه يجب عليك العمل في هذه الحالة على تسويق نفسك على منصات التواصل الاجتماعي وتعلّم إدارة ربحك وتكاليفك بشكل مضاعف. إلّا أنه من الجدير بالذكر أن كلّ مبرمج ألعاب فيديو وضع في عين الاعتبار العمل كمستقل في بداية مسيرته لما يحصل عليه من ميزات. أولًا؛ يمكنك الحصول على دفع أعلى للساعة كمستقل. إذا يجني بعض مبرمجي الألعاب المستقلين الخبراء أكثر من 100$ في الساعة، وهو أكثر بكثير مما يحصلون عليه كموظفين عاديين. ثانيًا؛ المرونة في العمل، يمكنك أن تعمل متى تريد، كيفما تريد، على مشاريع من اختيارك، فأنت رئيس نفسك هنا! إلا أنه لا يمكن نكران صعوبة الحصول على منصب مبرمج ألعاب فيديو في شركات مرموقة تحكي منتجاتها عنها وتجذب مشاريع جديدةً بشكل دائم ما يشجّع مبرمجي ألعاب الفيديو على تطوير مهاراتهم للعمل مع هذه الشركات. فإن كنت قادرًا على التسويق لنفسك وجذب عملائك يمكنك العمل كمستقلّ، أما إن كنت تبحث عن التطور والمشاريع الدائمة وبيئة العمل الجماعيّة فقدّم طلب عملٍ إلى شركات تطوير الألعاب الآن!. متوسّط دخل مبرمجي الألعاب يعتمد متوسط دخل أيّ مبرمج ألعاب فيديو على التقنيات المستخدمة ولغة البرمجة، فلا يمكن أن يكون أجر مبرمج ألعاب فيديو iOS يساوي أجر مبرمج ألعاب فيديو باستخدام لغة C++‎ لأنهم يستخدمون نهجًا ومهارات مختلفة، بالإضافة إلى عدد سنوات الخبرة في المجال وسرعة الإنجاز والاحترافية في التقنيات والمهارات. إليك قائمة بمتوسط دخل مبرمجي ألعاب الفيديو سنويًا في الولايات المتحدة الأمريكية اعتمادًا على الاختصاص ولغة البرمجة بحسب إحصائية لكيوبت عام 2023 : 2000$ مبرمج ألعاب فيديو مختصّ بنظام iOS 11 113000$ مبرمج ألعاب فيديو مختص بنظام Android 53000$ مبرمج ألعاب فيديو مختص بلغة #C 113000$ مبرمج ألعاب مختص بلغة ++C 98000$ مبرمج ألعاب فيديو مختص بلغة HTML5 89000$ مبرمج ألعاب فيديو مختص بلغة JavaScript كيفية البدء بالعمل كمبرمج ألعاب فيديو للدخول إلى عالم برمجة ألعاب الفيديو، تحتاج إلى استراتيجية عمل متينة توصلك إلى وظيفتك الأولى، فليس هناك طريقٌ واحد يجب أن تسلكه وإنما استراتيجيات تمكنك من الوصول لهدفك والبدء بالعمل. فيما يلي 3 استراتيجيات يمكنك اعتمادها لبدء رحلتك في برمجة الألعاب: برمج لعبتك الأولى: فأكبر مبرمجي الألعاب حول العالم بدؤوا مسيرتهم من خلال أجهزةٍ محدودة الإمكانيات، إلا أن الشغف شجعهم على إنشاء لعبةٍ متميزة. هناك الكثير من أدوات تطوير الألعاب المجانية حولك، كل ما عليك فعله هو اختيار اللعبة التي تريد برمجتها ومن ثمّ نشرها على المنصات الخاصة بالألعاب، والآن أصبح لديك خبرة ومعرض أعمال في المجال، وعندما تلقى لعبتك رواجًا ستلاحظك شركات برمجة الألعاب الشهيرة. أنشئ معرض أعمال: تبحث شركات برمجة الألعاب عن مرشحين يسعون إلى التطور بشكلٍ دائم في مجالهم، لذلك حاول أن تسعى بشكل دائم إلى تطوير نفسك في المجال وزيادة خبراتك وشارك أحدث أخبارك عبر منصات التواصل الاجتماعي المختلفة. صمّم معرض أعمالٍ تضع فيه تفاصيل مشاريعك المختلفة والأدوات والمنصات التي اعتمدتها لتحقيق النتيجة النهائية يمكنك استخدام موقع مثل itch.io لإنشاء معرض أعمال لألعابك ورفعها. شارك في مسابقات الألعاب Game Jams: عندما تشارك في مسابقات الألعاب ستحصل على خبرةٍ تضيفها إلى سيرتك الذاتية، وستكتسب بعض المهارات في العمل مع فريق تحت الضغط، وستوسع شبكتك المهنية عن طريق لقاء أشخاص جدد - كثير منهم قد يكونون مطوري ألعاب محترفين يمكنهم مساعدتك في الحصول على وظيفةٍ يومًا ما. الخاتمة لتكون مبرمج ألعاب فيديو محترف يجب أن تتحلّى بالعمل الجاد والتفاني والإصرار على الاستمراريّة لاكتساب الخبرات والمهارات التقنية والإبداعية. ولأجل ذلك استمرّ في تحسين مهاراتك وتواصل مع المحترفين في المجال، وتعلّم طريقة سرد قصص الألعاب وكتابة حبكتها بشكل مشوّق ومقنع، وشارك في أي فعاليات تخصّ صناعة الألعاب للتعرف على الأشخاص العاملين في مجال تطوير الألعاب والاستفادة من أفكارهم وتجاربهم وفهم المزيد حول صناعة الألعاب وتطوير مهاراتك وخبراتك. أخيرًا، ابحث عن فرصة عمل مناسبة من خلال التعاقد مع استوديوهات ألعاب أو شركات متخصصة في تطوير ألعاب احترافية أو اعمل على نفسك بشكل مستقل وطوّر ألعابًا خاصة بك وانشرها على متاجر الألعاب لزيادة شهرتك وتحقيق الأرباح. اقرأ أيضًا أشهر أنواع الألعاب الإلكترونية مطور الألعاب: من هو وما هي مهامه تعرف على أهمية صناعة الألعاب الإلكترونية تعرف على أفضل برنامج تصميم الألعاب الإلكترونية
  3. نعرفك في مقال اليوم على طريقة الحصول على أفكار ألعاب فيديو ناجحة ومميزة لمشروعك القادم؟ فسواءً أكنت مبتدئًا أو محترفًا متمرسًا في مجال تطوير الألعاب الإلكترونية، فإن الاستراتيجيات والأفكار في هذه المقالة ستزودك بأهم النصائح والأدوات التي تساعدك على صياغة أفكار ألعاب فيديو ناجحة. لا شك أن عملية إنشاء لعبة إلكترونية ناجحة تتطلب الكثير من الوقت والجهد لتصل بها إلى المستوى المرغوب، ولعل أول ما يتبادر إلى ذهننا عند التفكير بصعوبتها هي البرمجة اللازمة لتطبيقها، وآلية تنفيذ التصميم المقترح لهذه اللعبة وما إلى ذلك. ولكن في الواقع إن سألت أي مصمم ألعاب فيديو أو مطورًا متمرسًا عن ذلك، سيجيبك بأن أصعب خطوة في صناعة الألعاب هي تحديد فكرة اللعبة الأساسية، فقبل البدء بأي مرحلة من مراحل البرمجة أو تصميم شخصيات الألعاب الإلكترونية يلزم ابتكار فكرتها الأساسية، لكن هذه العملية ليست بالسهولة التي تتخيلها نظرًا لوجود عدد هائل من الألعاب المطورة فعلًا الأمر الذي يستلزم منك اختيار أفكار مبتكرة تشهد إقبالًا ورواجًا بين جمهور اللاعبين. أهم النصائح التي تساعدك في ابتكار أفكار ألعاب فيديو قد يكون لديك شغف بصناعة الألعاب الإلكترونية ومعرفة تقنية بلغات برمجة الألعاب الإلكترونية وباستخدام أحد محركات الألعاب المساعدة، لكنك تجد صعوبة في توليد فكرة لعبة جيدة تلقى رواجًا بين اللاعبين. لذلك سنعرض لك أهم الخطوات التي ستساعدك بالحصول على فكرة مبتكرة، ونعرفك على أفضل الاستراتيجيات من أجل الوصول لهذا الهدف. ليس بالضرورة أن تكون فكرة اللعبة الناجحة معقدة، فقد تكون الفكرة الناجحة هي فكرة بغاية البساطة والوضوح. فمثلًا من لا يعرف اللعبة الشهيرة Subway Surfers؟ تدور فكرة هذه اللعبة حول شاب يركض هُروبًا من الشرطي وكلبه، حيث يركض فوق القطارات التي تواجهه ليجمع خلال ركضه أكبر عدد من القطع المعدنية. وبالرغم من بساطة هذه الفكرة إلا أنها مصممة ومبرمجة بشكل جيد، وهذا يجعلها محبوبة من قبل الجميع. وفيما يلي أهم الخطوات التي عليك اتباعها للحصول على أفكار ألعاب فيديو ناجحة: أولًا:حدد جمهورك المُستهدف. ثانيًا: اختر تصنيفًا محددًا. ثالثًا: اعرف نطاق مشروعك. رابعًا: حدد أفكار الألعاب من المواضيع الرائجة حاليًا. خامسًا: تأمل العالم من حولك للبحث عن فكرة لعبة مميزة. سادسًا: اشترك في مسابقة Game Jam. سابعًا: استعن بمولدات الأفكار العشوائية وأدوات الذكاء الاصطناعي. ثامنًا: تعلم واستلهم الأفكار من محبّي الألعاب والمطورين. تاسعًا: اعرف ما هي مواصفات اللعبة - التي لن تلعبها! عاشرًا: جرب مفهوم اللعبة الأساسي قبل الاعتماد على الفكرة. لنناقش كل خطوة من هذه الخطوات بمزيد من التفصيل ونتعرف على دورها وأهميتها في الحصول على أفكار ألعاب ناجحة. أولًا: حدد جمهورك المُستهدف برأيك هل ستنجح لعبة تتضمن قوانين عديدة ومعقدة والكثير من التحركات المتقدمة إن كانت موجهة للأطفال؟ بالتأكيد الجواب هو لا! فمن الضروري قبل البدء بالتفكير باللعبة وقواعدها وغيره من التفاصيل أن تحدد من هي الفئة التي تستهدفها بلعبتك القادمة؟ وعلى هذا الأساس تبدأ في التوسع بتفاصيل هذه اللعبة. هذه العملية مهمة لعدة أسباب: تصميم اللعبة حسب تفضيلات الفئة المستهدفة: يتيح لك فهم الاهتمامات والسلوكيات لجمهورك المستهدف إنشاء لعبة تناسبهم. وذلك يشمل موضوع اللعبة وطريقة اللعب ومستوى الصعوبة. فمثلًا تختلف مستويات اللعبة وقوانينها بحسب الفئة العمرية الموجهة إليها. التسويق: إن معرفة جمهورك المستهدف يمكنك من تسويق لعبتك والترويج لها بشكل فعال للأشخاص المناسبين، فإن كانت اللعبة موجهة للأطفال فعليك أن تستخدم أسلوبًا بسيطًا ومرحًا في شرح فكرتها بالإضافة لألوان زاهية، أما إن كانت موجهة للأعمار الأكبر فمن الجيد تضمين أساليب تشويقية للعبتك. رضا اللاعب: يمكنك إنشاء لعبة توفر تجربة أكثر إرضاءً ومتعة للاعبين من خلال تلبية تفضيلات واحتياجات جمهورك المستهدف. ثانيًا: اختر تصنيفًا محددًا تتبع كل لعبة تصنيف معين تجري فيه أحداثها، وهذه نصيحة أساسية للتركيز على نوع معين من تصنيفات الألعاب وبناء قصتك وأحداثك على أساسها. ومن أشهر الأنواع: ألعاب العالم المفتوح Sandbox: نوع يتمتع فيه اللاعبون بطريقة لعب حرَة ويمكنهم استكشاف عالم اللعبة وإنشائه والتغيير به. مثال: ماين كرافت. استراتيجية الوقت الفعلي (RTS): نوع يتحكم فيه اللاعبون في الموارد ويديرونها، ويبنون القواعد، ويقودون الوحدات في معارك بأسلوب تفاعلي لحظي. مثال: ستار كرافت. ألعاب إطلاق النار (FPS و TPS): نوع يستخدم فيه اللاعبون الأسلحة النارية للمشاركة في القتال. مثال: Counter StrikeGears of War. ساحة معركة متعددة اللاعبين عبر الإنترنت (MOBA): نوع يتحكم فيه اللاعبون في شخصية واحدة في لعبة تنافسية قائمة على الفريق، بهدف تدمير قاعدة الفريق المنافس. مثال: League of Legends ألعاب المحاكاة والرياضة: نوع يحاكي فيه اللاعبون أنشطة العالم الحقيقي أو يشاركون في اللعب المتعلق بالرياضة. مثال: The Sims و FIFA . ألعاب الألغاز: نوع حيث يحلّ اللاعبون الألغاز أو يشاركون في مجموعة ألعاب مصغرة متعددة اللاعبين. مثال: Portal 2. ثالثًا: اعرف نطاق مشروعك لتضمن بدء المشروع بوعي كامل بالتفاصيل ونقاط القوة والضعف والعوائق التي ستواجهك، احرص قبل الشروع بإنشاء لعبتك وحتى قبل البحث عن أفكار ألعاب فيديو على طرح هذه الأسئلة على نفسك لمعرفة هذه الحدود: هل تعمل ضمن فريق؟ ما حجمه؟ أم أنه مشروع خاص بك فقط؟ ما هي المهارات التي تمتلكها أنت وفريقك؟ وهل هي كافية لتطوير فكرة اللعبة؟ ما هي المهارات التي تفتقدها، ما الذي يمكنك تعلمه أو الاستعانة بمصادر خارجية؟ هل هناك حزمة تطوير جاهزة ستستخدمها أم ستبني اللعبة بالكامل من الصفر؟ هل ستكون اللعبة ثنائية الأبعاد 2D أم ثلاثية الأبعاد 3D؟ كم من الوقت يمكنك استثماره في تطوير اللعبة أنت وفريقك وسطيًا؟ رابعًا: حدد أفكار الألعاب من المواضيع الرائجة حاليًا إذا كنت تبحث عن أفكار ألعاب فيديو جيدة ومحببة، خذ نظرة من حولك وتعرف على أبرز الأفكار والاتجاهات الرائجة التي تتجه إليه صناعة الألعاب؟ بالإضافة إلى ذلك، فكّر بأساليب تستطيع من خلالها جذب الجمهور إلى لعبتك، كأن تستغل مثلًا شهرة مسلسل تلفزيوني وتقتبس من أحداثه وشخصياته. وبذلك تضمن وجود إقبال أكبر على لعبتك. مثال على ذلك فكرة لعبة أنتون بلاست ANTONBLAST والمستوحاة بشكل كبير من سلسلة Wario Land المنسية منذ فترة طويلة والتي لم تشهد إصدارًا جديدًا منذ عام 2008، إلا أنها لاقت بعد نشرها إقبالًا كبيرًا من الأشخاص بسبب شهرة هذه السلسلة واستغراب الجمهور من إحيائها بهذه الطريقة. خامسًا: تأمل العالم من حولك للبحث عن فكرة لعبة مميزة. لا يمكنك الخروج بأفكار إبداعية وأنت مستلقي على سريرك في غرفتك وحيدًا، انهض وتأمل الدنيا من حولك! قد يبدو لك الأمر يسيرًا وليس ذا أهمية كبيرة، ولكن هل تعلم أن أعظم الأفكار الإبداعية لألعاب الفيديو تشكلت عبر محاكاة الواقع؟ عليك بمراقبة البيئة المحيطة بك لتنشيط خيالك والبدء في الخروج بأفكار ألعاب إبداعية. انتبه إلى الأشياء الصغيرة، وكيف تبدو وتعمل الأشياء من حولك، شاهد الأفلام المتنوعة، اقرأ الروايات، فقد تجد فيها عدد كبير جدًا من الأفكار الرائعة التي ما كانت لتخطر على بالك. في الواقع، يميل الناس لمحبة الألعاب التي صممت وطورت باستخدام قصص واقعية بشكل أكبر. فالنصيحة الذهبية لك هنا أن تحصل على الإلهام من البيئة المحيطة بك وأن تحاول استخدامها في ألعابك لإبقاء اللاعبين مهتمين ومتحمسين. من الأمثلة على الألعاب التي بنيت على محاكاة الواقع هي سلسلة GTA الشهيرة، حيث تحدث الأحداث في هذه السلسلة ضمن أماكن مستوحاة من مدن الحياة الواقعية، وذلك رغبةً من المطورين في أن يركز اللاعب على إكمال المهام اللازمة للتقدم في القصة مع شعوره بأنه الشخصية بحد ذاتها. مثالٌ آخر، هو لعبة بوكيمون Pokemon، حيث استلهم مطوّرها فكرة لعبته من اهتمامه وهو طفل بجمع الحشرات. سادسًا: اشترك في مسابقة Game Jam مسابقات Game Jam هي عبارة ماراثونات مخصصة لتطوير الألعاب تهدف لجمع مطوري الألعاب بإنشاء ألعاب إلكترونية من الصفر وتعاونهم معًا للخروج بأفكار ألعاب فيديو حول موضوع معين أو بشروط معينة والبدء بإنشائها واختبارها وتلقي الآراء حولها.والمشاركة في هذه المسابقة تساعدك على تطوير إمكانياتك وبناء مجتمع في مجالك والحصول على العديد من أفكار ألعاب الفيديو المميزة خارج الصندوق. من أشهر هذه المسابقات مسابقة GMTK game jam، وهو حدث سنوي يهدف لإنشاء لعبة واحدة أو أكثر خلال فترة زمنية قصيرة تتراوح عادة بين 24 إلى 72 ساعة والمشاركة فيها من شأنه تعزيز الإبداع والابتكار في تصميم الألعاب. سابعًا: استعن بمولدات الأفكار العشوائية وأدوات الذكاء الاصطناعي هناك العديد من أدوات الذكاء الاصطناعي التي يمكنها مساعدتك في توليد أفكار ألعاب فيديو ترغب في إنشائها. فيما يلي أربع أدوات مفيدة بالإضافة إلى معلومات حول كيفية عملها وكيف يمكنك استخدامها: تشات جي بي تي ChatGPT يمكنك الاستفادة من خدمات ChatGPT في جميع مراحل إنشاء اللعبة متضمنًا عملية البحث عن الفكرة الأساسية وتفاصيلها. وذلك من خلال تزويده بوصف لما تبحث عنه في فكرة لعبة الفيديو سواءً كان نوع لعبة محدد اخترته أم فئة معينة توجه لها هذه اللعبة وسيقدم لك أفكار إبداعية بناءً على مدخلاتك. أداة Let's Make a Game هي أداة مصممة خصيصًا لمساعدة مصممي الألعاب على توليد أفكار لألعاب الفيديو إذ يمكنك إدخال كلمات رئيسية أو معايير محددة تتعلق بنوع اللعبة التي ترغب في إنشائها، وسيزودك المولد بمفاهيم مختلفة للعبة تتوافق مع تلك المواصفات. توفر لك هذه الأداة الوقت من خلال اقتراح أفكار الألعاب تلقائيًا بناءً على تفضيلاتك، مما يسمح لك باستكشاف الاحتمالات المختلفة بسرعة. أداة Plot Generator هي أداة متعددة الاستخدامات يمكن استخدامها لتطوير القصص والروايات الخاصة بالألعاب. حيث يُنشئ المولد قصص وشخصيات ومهام فريدة للعبتك من خلال اختيار نوع اللعبة وعناصر القصة الأساسية ما يساعدك على العثور على أفكار ألعاب فيديو مبتكرة قد لا تخطر ببالك. أداة Concept and Art Idea Generator توفر لك هذه الأداة المدعومة بالذكاء الاصطناعي طريقة لتوليد العديد الأفكار الملهمة لألعاب الفيديو التي تريد تطويرها وتعطيك اقتراحات حول بيئة اللعبة ومظهر شخصيات الألعاب التي تشترك في اللعب وغيرها من الأفكار المميزة. وذلك عبر إدخال كلمات رئيسية أو سمات محددة تتعلق بلعبتك. ثامنًا: تعلم واستلهم الأفكار من محبّي الألعاب توجد العديد من المجتمعات والمنتديات على الإنترنت تهتم بتطوير الألعاب وتضم أشخاصًا يشاركونك الاهتمام ذاته، وتستطيع الاشتراك بها ومتابعتها للحصول على العديد من أفكار الألعاب أو طرح فكرتك والحصول على آراء المستخدمين حولها. حيث يمكنك إنشاء على سبيل المثال إنشاء منشور على مجتمع حسوب وطرح فكرة لعبة تدور ببالك للحصول على آراء متعددة ومتنوعة من المهتمين لا سيما إذا كان استهداف لعبتك هو السوق العربيّ فهذا المكان هو الأمثل. تاسعًا: اعرف ما هي مواصفات اللعبة التي لن تلعبها! هل تعلم أن لعب لعبة سيئة وغير محببة قد يساعدك بشكل كبير عندما يتعلق الأمر بالبحث عن أفكار ألعاب! فهذا الأمر من شأنه أن يساعدك بشكل كبيرعلى اكتشاف العيوب والأخطاء التي تواجهك في اللعبة السيئة كي تتجنبها ولا تقع بمثلها عند اختيار فكرة لعبتك، كما يمكنك بذلك استلهام أفكار جديدة من أفكار قديمة لم يتم تطويرها بشكل جيد والعمل على تحسينها وصياغتها بلعبة ناجحة. عاشرًا: جرب مفهوم اللعبة الأساسي قبل الاعتماد على الفكرة تعد تجربة فكرة اللعبة الخاصة بك قبل العمل عليها أمرًا بالغ الأهمية لعدة أسباب: تحديد العيوب: يساعدك اختبار فكرة لعبتك على تحديد أي عيوب أو مشكلات محتملة قبل استثمار وقت وموارد كبيرة في تطويرها. فهو يسمح لك باكتشاف المشكلات في المفهوم أو آليات اللعب ومعالجتها. تقييم الجمهور المستهدف: يتيح لك اختبار فكرة لعبة الفيديو التي تريدها قياس اهتمام وتفاعل جمهورك المستهدف. يمكن أن تساعدك تعليقاتهم المبكرة على فهم ما إذا كانت لعبتك تنال إعجاب الفئة المستهدفة أم لا، مما يساعدك على تحسين المفهوم ليناسب تفضيلاتهم وتوقعاتهم. التحسين: يمكنك تحصيل بيانات حول نقاط نجاح لعبتك وفشلها من من خلال مراقبة أداء اللاعب ودرجة انغماسه في اللعب. تضمن لك هذه العملية أن تطور من لعبتك بما يحقق نجاحها. توقع التكلفة: يمكنك توفير الوقت والموارد في التطوير من خلال اختبار فكرة لعبتك في وقت مبكر، وذلك بتحديد العيوب ونقاط الضعف في مرحلة مبكرة مما يسمح لك بإجراء التعديلات اللازمة قبل استثمار أموالك في البرمجة والتصميم والتفاصيل الأخرى. أمثلة على أفكار ألعاب فيديو لاقت نجاحًا كبيرًا إذا كنت مهتمًا بصناعة ألعاب إلكترونية ومعرفة السر وراء نجاح العديد من الألعاب الرائجة فإليك أمثلة على أبرز الألعاب الإلكترونية التي لاقت رواجًا وأهم الأسباب في نجاحها وشهرتها: لعبة ماين كرافت Minecraft. لعبة كاندي كراش Candy Crash. لعبة سوبر ماريو برو Super Mario Bros. لعبة وي سوبرتس Wii Sports. لعبة أمونغ آس Among Us. لنحاول اكتشاف أبرز الأسباب التي جعلت من أفكار هذه الألعاب محبوبة ومنتشرة بين جمهور كبير من اللاعبين حول العالم. لعبة ماين كرافت Minecraft تقوم فكرة لعبة ماين كرافت على أسلوب اللعب المفتوح من خلال السماح للاعبين ببناء واستكشاف عوالم افتراضية بناءً على تفضيلاتهم الشخصية، مما يوفر فرصة للإبداع والخيال ففي هذه اللعبة لايوجد طريقة أو أسلوب معين للعب وهذه الإمكانيات الإبداعية غير المحدودة وقدرة اللاعبين على مشاركة إبداعاتهم مع الآخرين هي ما أكسب اللعبة شعبية هائلة، كما ساهمت طبيعتها التي يتحكم فيها مجتمع اللاعبين والتحديثات المتكررة لها في زيادة نجاحها. لعبة كاندي كراش Candy Crash هي لعبة ألغاز يقوم اللاعبون فيها بمطابقة ثلاث قطع حلوى أو أكثر من نفس اللون لإزالتها من اللوحة. وقد لاقت اللعبة شهرة واسعة منذ نشرها إلى الآن واستقطبت اللاعبين من مختلف الأعمار بفضل أسلوبها الجذاب وقوانينها البسيطة في اللعب وتصميم رسوماتها الملونة ذات الألوان الزاهية وتضمنها لمئات المستويات التي تزداد صعوبة مع تقدم اللاعب في اللعب وتقدم له مكافآت تحفزه على مواصلة اللعب، كما ساعد توفرها على منصات متعددة كالهواتف الجوالة والأجهزة اللوحية وأجهزة الحاسوب في زيادة شعبيتها وانتشارها على نطاق واسع. لعبة سوبر ماريو Super Mario Bros يعود نجاح فكرة لعبة سوبر ماريو التي طورتها شركة نينتندو Nintendo إلى عدة عوامل من أبرزها أسلوب اللعب الممتع والوضح إلى جانب تصميم شخصية لعبة ماريو المحبب والتي أصبحت واحدة من أشهر شخصيات ألعاب الفيديو، كما ساهم إصدار اللعبة على العديد من المنصات في زيادة شعبيتها وانتشارها وهي واحدة من أكثر ألعاب الفيديو مبيعًا على الإطلاق. لعبة وي سبورتس Wii Sports تعتمد فكرة لعبة Wii Sports التي طورتها شركة Nintendo EAD عام 2006 على تضمين من ألعاب المحاكاة الرياضية وقد حققت هذه اللعبة نجاحًا واسعًا بسبب سهولة لعبها وقواعدها البسيطة التي تسمح للاعبين من مختلف الأعمار والمستويات المهارية اللعب والاستمتاع بها كما تتميز هذه اللعبة باستخدام تقنية الحركة وتتضمن وحدة تحكم Wii التي تستخدم تقنية الاستشعار عن الحركة للتحكم بالألعاب عن طريق حركة الجسم مما وفر تجربة لعب واقعية وممتعة. لعبة أمونغ آس Among Us هي لعبة جماعية تتضمن فريق من اللاعبين الذين يجتمعون معًا لإصلاح سفينة فضائية أثناء التعامل مع وجود المحتالين الذين يحاولون القضاء على الطاقم. تشجع اللعبة على التواصل والشك والعمل الجماعي حيث يحاول اللاعبون التعرف على المحتالين والتصويت ضدهم. لاقت اللعبة نجاحًا عظيمًا بسبب طريقة لعبها البسيطة والجذابة التي تشجع العمل الجماعي والتفكير للوصول إلى استنتاجات. كما ارتفعت شعبيتها مع بدء العديد من اللاعبين وصنّاع المحتوى المشهورين بلعبها على البث المباشر، مما ساهم بخلق ضجة كبيرة حولها على مستوى العالم. الخلاصة لا ريب أن عملية تطوير الألعاب تتطلّب عددًا من المهارات للحصول على منتج نهائي ناجح يستمتع به اللاعبون بدءًا من التطوير والتصميم وصولًا إلى التسويق وإطلاق اللعبة على المتاجر، إلا أن التوصل إلى فكرة ممتعة فريدة من نوعها هو أساس هذه العملية وستضمن أن المنتج النهائي سيبدأ على أسس قوية تضمن نجاحه. هل هناك فكرة في بالك للعبة إلكترونية مميزة وناجحة تعتقد أنها ستحقق النجاح لكنك متردد في آلية تنفيذها؟ اطرحها للمناقشة أسفل المقال، ودع القراء الآخرين يبرزون لك مواضع ضعفها وقوتها! اقرأ أيضًا مطور الألعاب: من هو وما هي مهامه تعرف على أشهر لغات برمجة الألعاب مدخل إلى محرك الألعاب جودو Godot نبذة عن صناعة الألعاب ومحرك Unity3D
  4. تعد عملية اختيار وتصميم شخصيات الألعاب جزءًا لا يتجزأ من صناعة الألعاب الإلكترونية، وهي لا تقتصر على التركيز على الجانب الجمالي للتصميم وجعله جذابًا، بل تلعب دورًا حاسمًا في تشكيل تجربة اللاعبين وتضمن تناسق قصة اللعبة وانسجام اللاعب معها. حيث تتمتع الشخصية المنتقاة جيدًا بالقدرة على التأثير على المشاعر، وتحفيز اللاعب على اللعب باستمرار، وتضمن الارتقاء باللعبة في النهاية إلى آفاق جديدة من النجاح. سننظر سويًا في هذا المقال إلى أهم الاعتبارات التي عليك الأخذ بها لاختيار شخصية ناجحة في لعبتك القادمة وأهم برامج وأدوات تصميم شخصية لعبة، ونختم المقال بأمثلة لأبرز شخصيات الألعاب المصممة جيدًا فإذا كنت مهتمًا بتطوير ألعاب الفيديو فتابع قراءة المقال للنهاية. معايير تصميم شخصية لعبة ناجحة لا شك أن تصميم شخصيات الألعاب يلعب دورًا مهمًا جداً في صناعة الألعاب الإلكترونية، فالشخصيات هي وسيلة اللاعبين للتفاعل مع العالم الافتراضي للعبة، فإذا كنت مبرمج ألعاب فيجب أن تكون الشخصيات التي تختارها في لعبتك محببة وجيدة التصميم ومنسجمة مع نوع اللعبة وقصتها، وتعكس الأهداف التي تهدف اللعبة إلى تحقيقها، وإليك مجموعة من المعايير أو النقاط التي عليك الانتباه لها عند اختيار أو تصميم شخصية لعبة إلكترونية: تحديد المفهوم العام لشخصية اللعبة ويشمل: السمات الجسدية والنفسية لشخصية اللعبة. سلوك الشخصية وتفاعلها مع محيطها. نبرة صوت الشخصية أو التأثيرات الصوتية الخاصة بها. نقاط قوة الشخصية ونقاط ضعفها والموازنة فيما بينهما. قصة الشخصية. مظهر شخصية اللعبة. لنناقش كل نقطة من هذه النقاط ونتعرف على دورها عند التفكير في تصميم شخصيات الألعاب الإلكترونية. أولًا: تحديد المفهوم العام للشخصية من المهم تحديد الدور العام لشخصيات الألعاب أولًا قبل البدء بتصميمها، والانتباه لأن تكون لكل شخصية لعبة مميزات وخصائص فريدة مختلفة عن بقية الشخصيات الأخرى لضمان تميزها وتفردها وتحديد خصائصها العامة مثل السمات النفسية والجسدية ونبرة الصوت وغيرها من التفاصيل التي تساهم بتشكيل مفهوم شخصية اللعبة، وإليك أهم العوامل التي تساعدك على تصور المفهوم العام للشخصية: 1. السمات الجسدية والنفسية لشخصية اللعبة أول ما عليك التفكير به عند اختيار شخصية لعبة فريدة هو تحديد السمات الجسدية والنفسية لشخصية اللعبة لأنها تساعدك على تصور شكل جسدها وملامحها بما يتوافق مع هدف الشخصية في اللعبة. فمثلًا من المهم أن تكون الشخصية ذات جسد رياضي في حال كانت اللعبة تتضمن الركض والهروب. 2. سلوك الشخصية وتفاعلها مع محيطها يعد سلوك الشخصية جانبًا حيويًا مهمًا في تصورها وتحديد طريقة تفاعلها مع بيئة اللعبة ومع الشخصيات الأخرى لذا عليك أن تحدد هل الشخصية التي تريدها في لعبتك هي شخصية بطولية أو مغامرة أو مرحة أو مؤذية أن ويتماشى تصميم الشخصية مع أفعالها وسلوكها، فإذا كانت الشخصية عدوانية وشريرة فيجب أن تعكس ملامح وجه الشخصية عدوانيتها حتى تكون مقنعة. 3. نبرة صوت الشخصية أو التأثيرات الصوتية الخاصة بها تشير نبرة الشخصية إلى سلوكها وموقفها العام في اللعبة. فهي تحدد الحالة المزاجية للعبة وتؤثر على كيفية إدراك اللاعبين للشخصيات والتفاعل معها فينبغي أن تكون النغمة متسقة طوال اللعبة، ومتناسبة من الحديث أو الموقف الحاصل مما يعزز هوية الشخصية ويعزز الاتصال العاطفي للاعب معها. كما أن النبرة المميزة والمتغيرة بحسب الموقف مثلًا نبرة حماسيّة، حزينة، مبتهجة …إلخ. تجعل منها شخصية متفردة وتبقى عالقة في ذهن اللاعب. 4. نقاط قوة الشخصية ونقاط ضعفها والموازنة فيما بينهما يعد تحديد نقاط القوة والضعف لدى شخصية لعبة الفيديو أمرًا مهمًا لعدة أسباب فهو يضيف عمقًا وتعقيدًا للشخصية، ويجعلها أكثر واقعية وإقناعًا كما أنه يؤثر على آليات اللعب ويجعله أكثر حماسًا، على سبيل المثال تتميز شخصية ماريو في لعبة Super Mario Bros بعدة نقاط قوة مثل القدرة على الركض السريع والقفز عاليًا مما يسمح له بالهروب من الأعداء والوصول إلى الأماكن المرتفعة وتجاوز العقبات، لكنه يملك نقاط ضعف فهو صغير الحجم ولا يستطيع السباحة فهو يغرق إذا سقط في الماء. ثانيًا: قصّة الشخصية تساهم قصة شخصية اللعبة الجذابة في إضافة عمق وتشويق على اللعبة وتجعل اللاعب يتفاعل معها بشكل أفضل، ويشعر بالإنجاز عندما ينتقل عبر مستويات اللعبة ويفوز في النهاية ويحقق الهدف المطلوب، فلا يمكن التغاضي عن أهمية قصة شخصية لعبة الفيديو أثناء اللعب ودورها في ارتباط اللاعب في اللعبة وتحفيزه واهتمامه بأدق تفاصيلها. وليس بالضرورة أن تكون قصة الشخصية معقدة ومطولة فقد تكون بسيطة بحسب حجم اللعبة. خذ لعبة الطيور الغاضبة Angry Birds على سبيل المثال، إذ أنّ هذه الطيور غاضبة بسبب أن الخنازير الأشرار في اللعبة قد اختطفوا بيوض صغارهم وهذا ما يمنح للعبة دافعًا وسببًا لوجود الشخصيات في عالم اللعبة الإلكترونية. ثالثًا: مظهر الشخصية يلعب مظهر الشخصية وتصميمها المرئي دورًا مهمًا في إنشاء الشخصية وتصميمها إذ يجب أن يكون مظهر الشخصية جذابًا وفريدًا ويعكس شخصيتها ودورها في اللعبة وعند تصميم مظهر الشخصية يجب أن تهتم بعدة عوامل إلى جانب ملامحها الجسدية والنفسية مثل زي الشخصية حيث تلعب أزياء شخصيات ألعاب الفيديو دورًا مهمًا في المساهمة في نجاح اللعبة وتساعد في تحديد الهوية المرئية للشخصية وتجعلها أكثر قابلية للتمييز والتذكر بالنسبة للاعب. كما تعكس أزياء الشخصيات أيضًا إعدادات اللعبة أو الفترة الزمنية لها على سبيل المثال، قد يشتمل زي إحدى الشخصيات في لعبة خيالية من العصور الوسطى على دروع وتروس، بينما قد تتمتع الشخصية في لعبة خيال علمي مستقبلية بملابس عالية التقنية. علاوة على ذلك، يمكن أن تؤثر أزياء الشخصيات أيضًا على آليات اللعب من خلال توفير مكافآت أو قدرات متنوعة. على سبيل المثال، قد يوفر الزي دفاعًا معززًا أو خفة الحركة، أو يمنح قدرات خاصة يمكن استخدامها بشكل استراتيجي أثناء اللعب. رابعًا: حركات الشخصية والمقصود بحركات الشخصية الأفعال التي تقوم بها شخصية اللعبة مثل المشي او الجري أو القفز أو التسلق أو إطلاق النار …إلخ. وتؤثر انسيابية واستجابة حركات الشخصية بشكل مباشر على تجربة اللاعب وقدرته على التنقل في عالم اللعبة بفعالية. حيث يمكن للحركات السلسة والطبيعية أن تجعل تجربة اللعب أكثر متعة وواقعية وتعزز تفاعل اللاعب مع اللعبة وتطيل مدة اللعب، بالمقابل ستتسبب حركات الشخصية المصممة بشكل سيء في إحباط اللاعبين وتقلل من استمتاعهم باللعبة. خطوات تصميم شخصيات الألعاب بعد تعرّفنا على معايير تصميم الشخصية الناجحة، كيف تبدأ فعلًا ببدء التصميم؟ نستطيع تجزئة خطوات تصميم الشخصية إلى ما يلي: تحديد تصنيف الشخصية الأولية. بناء قصة الشخصية. البحث عن موارد لشخصيات الألعاب. استخدام أدوات وبرامج مخصصة لتصميم الشخصية. 1. تحديد الشخصية الأولية قد تكون الشخصية بعد الانتهاء من تصميمها فريدةً من نوعها بمزاياها المختلفة من صفات جسدية وحركة وشكل وقصة، إلا أن تصميم الشخصية يبدأ دائمًا بتحديد شخصيتها الأولية أو الابتدائية، حيث أن تصنيف شخصيات الألعاب يحدد توجه الشخصية العام وكيفية تطبيق تصميمك عليها لتلائمها، وإليك بعض الأمثلة على أشهر شخصيات الألعاب الأولية التي يمكنك اختيارها للعبتك الإلكترونية: شخصية البطل: وهي الشخصية التقليدية في معظم ألعاب الفيديو التي تواجه التحديات بقوة وتنتصر عليها، وتتميز هذه الشخصية بشجاعتها وأخلاقياتها العالية، مثال على هذه الشخصية شخصية سوبرمان أو شخصية لينك من سلسلة أسطورة زيلدا The Legend Of Zelda. شخصية الطيب المحبّ للغير: تتميز هذه الشخصية بإنسانيتها وشغفها إلا أنها قد تكون غير منطقية في بعض الأحيان مما يتسبب في وقوعها في بعض المشكلات. مثال على هذه الشخصية هي شخصية الكوماندر شيبرد في لعبة ماس إيفيكت Mass Effect. شخصية الساحر: هي شخصية لعبة غامضة تملك حيلًا وأسرار خاصة لتحقيق غاياتها، مثال على هذه الشخصية شخصية هاري بوتر Harry Potter وشخصية جاندالف Gandalf في لعبة أمير الخواتم The Lord Of The Rings: Shadow of Mordor. كما يوجد عدة أنواع أخرى من الشخصيات، كشخصية المتمرد وشخصية المغامر والمحب للاستكشاف والقدوة والساذج والمنبوذ والشرير، وقد تتشارك بعض الشخصيات فيما بينها بعدد من الصفات ومن المهم أن تحدد الشخصية الأولية لكل شخصية تستخدمها في ألعابك لأنها تساعدك في اختيار التصميم الملائم بشكل أفضل. 2. بناء قصة الشخصية بعد تحديد تصنيف شخصيات الألعاب الأولية تحتاج للبدء بتخيل قصة الشخصية وتحديد سبب وجودها في عالم اللعبة ودورها فهذا أمر مهم وذلك لتجعل لاعبي اللعبة يرتبطون عاطفيًا مع الشخصية ويحزنون لحزنها ويفرحون لفرحها. لذا تأكّد من تحديد قصة حياة الشخصية وماضيها وهدفها في قصة اللعبة كما يساعد ذلك أيضًا مصممي الغرافيك ومبرمحي الألعاب على الاعتماد على هذه المعلومات في تصميم الشخصية بصريًا بشكل يلائم قصتها وبرمجة تحركاتها بالشكل الصحيح المناسب لتحقيق أهدافها. 3.البحث عن مراجع لشخصيات الألعاب عندما تفكر في اختيار شخصيات ألعابك ستجد أمامك خيارين الأول هو تصميم الشخصية من الصفر باستخدام أدوات وبرامج مخصصة أو استخدام شخصيات جاهزة، فمن المرجّح أنك عندما تفكر بتصميم لشخصية لعبة خاصة بك ستجد رسومات لشخصيات افتراضية تخيلية تطابق الشخصية التي تريد أن تكون موجودة في لعبتك ويمكنك في هذه الحالة الاعتماد عليها بدلًا من تصميمها من الصفر. هناك عشرات المصادر على الإنترنت التي تستطيع من خلالها تحميل عدد من الشخصيات تشابه الشخصية التي تريدها ومن ثم التعديل عليها أو ربما حتى استخدامها مباشرةً في لعبتك الإلكترونية دون أي تعديلات إن وجدت أنها مطابقة للمواصفات التي تريدها. نذكر من هذه المصادر: متجر محرك يونيتي Unity متجر محرك أنريل Unreal متجر كيني الذي يحتوي على شخصيات مجانية متجر itch.io الشهير موقع Adobe Stock موقع Pinterest وغيرها من المواقع التي توفر مجموعات منوعة من الصور والتصاميم لشخصيات ألعاب مجانية أو مدفوعة يمكنك استخدامها في لعبتك الإلكترونية، لكن انتبه لصيغة الشخصية وتوافقها مع محرك الألعاب الذي تود استخدامه في برمجة اللعبة أو تحريك الشخصية وهذا الأمر مفيد ويوفر عليك الكثير من الوقت خصوصًا لو كنت تعمل ضمن فريق حيث أن مصمم الشخصية مختلف عن محركها وعن مبرمجها. 4. استخدام أدوات وبرامج مخصصة لتصميم الشخصية إذا كنت مهتمًا بتصميم شخصيات الألعاب من الصفر أو لم تعثر على تصميم شخصية يناسب ما رسمته في مخيلتك في المواقع التي توفر مصادر لشخصيات للألعاب، فستجد عدة أدوات وبرامج مخصصة تساعدك على تصميم شخصيات الألعاب وتحويلها من أفكار إلى رسومات ثنائية أو ثلاثية الأبعاد جاهزة لبرمجتها ومن أشهر هذه البرامج نذكر: Piskel: هو تطبيق ويب مجاني ومفتوح المصدر يعمل في المتصفح ويمكنك كذلك تثبيته على أنظمة تشغيل لينكس وويندوز ويوفر العديد من الأدوات السهلة لإنشاء شخصيات ألعاب بسيطة ثنائية الأبعاد المعروفة باسم sprite وحفظها بتنسيق PNG أو GIF. GIMP: هو برنامج مفتوح المصدر لمعالجة الصور وهو يوفر إمكانية تصميم شخصيات ألعاب ثنائية الأبعاد وتصديرها إلى العديد من التنسيقات المختلفة. Sketchbook: برنامج مميز يوفر إصدار مجاني لرسم شخصيات الألعاب ثنائية الأبعاد ويوفر عدة خيارات لتصدير الرسومات. بليندر Blender: هو برنامج مجاني ومفتوح المصدر ومجاني لتصميم ورسم الشخصيات ثلاثية الأبعاد ويمكنه تصدير الرسومات إلى العديد من تنسيقات الملفات الجاهز لاستخدامها في برامج تطوير الألعاب. Magic Voxel: هو برنامج مجاني لتصميم شخصيات ألعاب ثلاثية ويمكنك من تصدير الرسومات إلى التنسيق OBJ. والبدء ببرمجتها في محركات الألعاب. MakeHuman: برنامج مفتوح المصدر لتصميم شخصيات ألعاب ثلاثية قريبة من الواقع. SculptGL: برنامج فعال يعمل في المتصفح ويمكنك من تصميم الأشكال والشخصيات ثلاثية الأبعاد بسهولة. مايا Maya: هو برنامج احترافي مدفوع من شركة Autodesk مخصص للهندسة المعمارية والتصاميم الداخلية كما يستخدم في صناعة الأفلام ورسم ونمذجة شخصيات الألعاب ثلاثية الأبعاد ويوفر ميزات متقدمة للتحكم بإضاءتها وحركتها وغيرها من المميزات الاحترافية. 3Ds Max: برنامج مدفوع من شركة Autodesk يشابه برنامج مايا ويوفر مجموعة قوية من الأدوات لنمذجة لتصميم ونمذجة شخصيات الألعاب والرسوم المتحركة ثلاثية الأبعاد ويوفر أيضًا تراخيص مجانية للطلاب. كانت هذه قائمة بأبرز أدوات تصميم شخصيات الألعاب وهناك الكثير غيرها، لذا عليك اختيار الأداة التي تناسبك يعتمد على طبيعة الرسومات التي تود الحصول عليها ثنائية الأبعاد 2D أم ثلاثية الأبعاد 3D، وعلى ميزانيك وخبرتك في استخدام الأداة، وفي محرك اللعبة الذي ستجلب هذه الشخصية إليه وتبرمجها فيه فبعض محركات الألعاب تدعم تنسيقات ملفات محددة فقط. أمثلة لأبرز شخصيات الألعاب المصممة جيدًا تضفي الشخصية المصممة بدقة الحياة على اللعبة، مما يجعلها أكثر جاذبية وبقاء في الذهن. ومن أهم شخصيات الألعاب وسنختم المقال بأكثر شخصيات الألعاب شهرة بين جمهور اللاعبين ونكتشف سبب شهرتها والتعلق بها بالرغم من بساطة بعضها. شخصية لعبة ماريو أصبح ماريو، الشخصية الشهيرة من سلسلة سوبر ماريو رمزًا لها بالرغم من بساطة تصميمه، ولعل السر في شخصية لعبة ماريو هو مظهره المميز وكلامه وأفعاله. فيمكن التعرف على مظهر ماريو المحبوب على الفور بفضل قبعته الحمراء المميزة، وبدلته الزرقاء، وشاربه الكثيف إلى جانب صوته عالي النبرة وعباراته الشهيرة "هذا أنا ماريو!" التي جعلت منه شخصية محبوبة لأجيال من اللاعبين، كما أن خفة الحركة التي يتمتع بها ماريو في القفز والجري والمناورة عبر المستويات المختلفة جعلت منه شخصيةً ممتعة للتحكم واللعب بها، أضف إلى ذلك دافعه طوال اللعبة لإنقاذ الأميرة المخطوفة من قبل السلحفاة الشريرة باوزر. شخصية باك مان حققت باك مان، الشخصية الدائرية الصفراء من لعبة الآركيد Arcade شهرة واسعة من خلال تصميمها وأسلوب لعبها البسيط. إذ أصبحت شخصيتها الأيقونية رمزًا لألعاب الفيديو من نوع الآركيد التقليدية التي كانت مشهورة في فترة سابقة، وعلى الرغم من القيود الموجودة على أجهزة ألعاب الفيديو آنذاك إلا أن مصممي اللعبة استطاعوا ببراعة خلق شخصية لعبة بقيت عالقةً في أذهان الجميع لحد اليوم على الرغم من بساطتها. فمن منا لا يتذكر باك مان بلونه الأصفر الفاقع وفمه المفتوح والأشباح الملونة التي تلاحقه! ولعل هذه الأمثلة تجعلنا نلاحظ أنه ليس من الضروري للشخصية أن تمتلك الكثير من الأشياء والمميزات لتصبح شخصية لعبة ناجحة، فشخصية باك مان ما هي سوى نقطة صفراء في غاية البساطة، ومع ذلك فقد حصدت الكثير من المعجبين. شخصية لعبة كريتوس من سلسلة God of War استطاعت شخصية كريتوس الوصول إلى الشهرة وجذب أعداد هائلة الجماهير من خلال التركيز على قصة الشخصية وتصميم شكلها بناءً على ذلك، متضمنًا تصميم جسد الشخصية ببنية قوية وملابس ملائمة لدوره في اللعبة بشكل مثالي، فهذه التفاصيل بالإضافة للتأثيرات الصوتية جعلت منه شخصية مهيبة، كما أن قدراته القتالية المتعددة أضافت إلى كريتوس إحساس القوة والتصميم، وهو الشيء ذاته الذي انجذب له الجمهور وجعلت منه شخصية هائلة لا تُنسى. بالإضافة إلى ذلك، فإن أسلوب كريتوس القتالي الوحشي، جنبًا إلى جنب مع خلفيته المعقدة وعمقه العاطفي، جعل منه شخصية مقنعة ومبدعة في عالم ألعاب الفيديو. إلى هنا نكون قد وصلنا لنهاية مقالنا الذي فصلنا فيه مرحلة اختيار شخصيات الألعاب التي تعد من أهم مراحل صناعة ألعاب الفيديو، حيث يقوم مصمم اللعبة بإنشاء المفهوم والأسلوب والعمل الفني الكامل للشخصية من الصفر بعملية معقدة ودقيقة يضمن بها تحليل السمات الشخصية لشخصيات الألعاب من أجل إضافة الحياة إليها وجعلها أكثر واقعيّة. ويجب على من يقوم بهذه الوظيفة أن يتمتع بالموهبة والمهارات المتطورة فتصميم شخصية اللعبة يعد جانبًا حيويًا في تصميم ألعاب الفيديو ومن شأنه أن يترك صدى لدى اللاعبين على مستوى واسع من خلال التركيز على الخصائص الجسدية والنفسية والسلوك والنبرة والمظهر وغيرها من التفاصيل. الخلاصة إن التركيز على شخصيات ألعاب الفيديو الخاصة بك يضمن لك ارتباط اللاعبين بلعبتك ارتباطًا عميقًا ويزيد من شعبية اللعبة، ومن شأنه أيضًا أن يكوّن مجتمعًا من الأشخاص الذين ينتظرون جديد الشخصية من قصص وتطوّرات متعلقة بها فيما إذا أردت تطوير جزء آخر أو لعبة مشتقة من لعبتك السابقة. لذا، تأكد من أنك تمنح هذا الجانب من تصميم لعبتك وقتًا وجهدًا مناسبين. هل هناك لعبةٌ حاضرة في ذهنك من أيام الطفولة بفضل شخصياتها المنفّذة والمصممة بشكل جيد؟ شاركها معنا! اقرأ أيضًا مطور الألعاب: من هو وما هي مهامه نبذة عن صناعة الألعاب ومحرك Unity3D إنشاء الوحدات البنائية وشخصيات الخصوم في Unity3D ما هي برمجة الألعاب؟
  5. ننشئ التوابع السحرية العددية والمعكوسة كما رأينا سابقًا كائنات جديدة بدلًا من تعديل الكائنات الموضعية، إلا أن التوابع السحرية الموضعية المُستدعاة باستخدام معاملات الإسناد المدعوم مثل =+ و =* تعدل الكائنات موضعيًا بدلًا من إنشاء كائنات جديدة (هناك استثناء سنشرحه في نهاية الفقرة). تبدأ أسماء هذه التوابع السحرية بحرفi، مثل ()__iadd__ و ()__imul__ من أجل العوامل =+ و =* على التتالي. مثلًا، عندما تنفذ بايثون الشيفرة purse *= 2 لا يكون السلوك المتوقع أن تابع ()__imul__ الخاص بالصنف WizCoin سينشئ ويعيد كائن WizCoin جديد بضعف عدد النقود ويسنده للمتغير purse، ولكن بدلًا من ذلك، يعدل التابع ()__imul__ كائن WizCoin الحالي في purse ليكون له ضعف عدد النقود. هذا فرق بسيط ولكن مهم إذا أردت لأصنافك أن تقوم بتحميل زائد overload لمعاملات الإسناد المدعومة. عرّف الصنف 'WizCoin' الذي أنشأناه العاملين + و *، لذا لنُعرّف التابعين السحريين ()__iadd__ و ()__imul__ ليتمكّنوا بدورهم من تعريف العاملين =+ و =* أيضًا، نستدعي في التعبيرين purse += tipJar و purse *= 2 التابعين ()__iadd__ و ()__imul__ على التتالي وتمرر tipJar و 2 إلى المعامل other على التتالي. ضِف التالي إلى نهاية ملف wizcoin.py: --snip-- def __iadd__(self, other): """Add the amounts in another WizCoin object to this object.""" if not isinstance(other, WizCoin): return NotImplemented # نعدل من قيمة الكائن‫ self موضعيًا self.galleons += other.galleons self.sickles += other.sickles self.knuts += other.knuts return self # تعيد التوابع السحرية الموضعية القيمة‫ self على الدوام تقريبًا def __imul__(self, other): """Multiply the amount of galleons, sickles, and knuts in this object by a non-negative integer amount.""" if not isinstance(other, int): return NotImplemented if other < 0: raise WizCoinException('cannot multiply with negative integers') # يُنشئ الصنف‫ WizCoin كائنات متغيّرة، لذا لا تنشئ كائن جديد كما هو موضح في الشيفرة المعلّقة: #return WizCoin(self.galleons * other, self.sickles * other, self.knuts * other) # نعدل من قيمة الكائن‫ self موضعيًا self.galleons *= other self.sickles *= other self.knuts *= other return self # تعيد التوابع السحرية الموضعية القيمة‫ self دائمًا تقريبًا يمكن أن تستخدم كائنات WizCoin العامل =+ مع كائنات WizCoin أخرى والعامل ‎=* مع الأعداد الصحيحة الموجبة. تعدّل التوابع الموضعية الكائن 'self' موضعيًا بدلًا من إنشاء كائن 'WizCoin' جديد بعد التأكد من أن المعامل الآخر صالح. أدخل التالي إلى الصدفة التفاعلية لرؤية كيف تعدل عوامل الإسناد المدعوم كائنات WizCoin موضعيًا: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> tipJar = wizcoin.WizCoin(0, 0, 37) 1 >>> purse + tipJar 2 WizCoin(2, 5, 46) >>> purse WizCoin(2, 5, 10) 3 >>> purse += tipJar >>> purse WizCoin(2, 5, 47) 4 >>> purse *= 10 >>> purse WizCoin(20, 50, 470) يستدعي العامل + التابعين السحريين ()__add__ و ()__radd__ لإنشاء وإعادة كائنات جديدة. تبقى الكائنات الأصلية التي يعمل عليها العامل + على حالها. يجب على التوابع السحرية الموضعية أن تعدل الكائنات موضعيًا طالما أن الكائن متغيّر mutable (أي هو كائن يمكن تغيير قيمته). الاستثناء هو للكائنات الثابتة immutable objects، إذ لا يمكن تعديلها ومن المستحيل تعديلها موضعيًا. في هذه الحالة يجب على التابع السحري الموضعي إنشاء وإعادة كائن جديد كما في التوابع السحرية العددية والمعكوسة. إذا لم نجعل السمات galleons و sickles و knuts للقراءة فقط، فهذا يعني أنه يمكن تعديلها، وبالتالي كائنات WizCoin هي متغيّرة، كما أن معظم الأصناف التي تكتبه تُنشئ كائنات متغيّرة لذا يجب تصميم توابع سحرية موضعية لتعديل الكائن موضعيًا. تستدعي بايثون تلقائيًا التابع السحري العددي في حال لم تُنفذ التابع السحري الموضعي. مثلًا، إذا لم يكن للصنف WizCoin تابع ()__imul__ سيستدعي التعبير purse *= 10 التابع ()__mul__ بدلًا عنه ويسند له القيمة المرجعة purse، لأن كائنات WizCoin متغيّرة وهذا سلوك غير متوقع وقد يؤدي لأخطاء بسيطة. توابع المقارنة السحرية يحتوي تابع ‎‎sort‎‎‎‎‎()‎‎‎ ودالة sorted()‎‎‏‏ خوارزميات ترتيب فعالة، ويمكن الوصول إليها باستدعاء بسيط، ولكن إذا أردت ترتيب ومقارنة كائنات أصنافك، ستحتاج لإخبار بايثون كيفية المقارنة بين الكائنين عن طريق تنفيذ توابع المقارنة السحرية، تستدعي بايثون التوابع المقارنة في الخلفية عندما تُستخدم الكائنات الخاصة بك في التعبير مع عوامل المقارنة< و > و =< و => و == و =!. قبل أن نستكشف توابع المقارنة السحرية، فلنفحص الدوال الست في وحدة 'operator' التي تنجز نفس وظائف عوامل المُقارنة الستة، إذ ستستدعي توابع المقارنة السحرية هذه الدوال. أدخل التالي في الصدفة التفاعلية: >>> import operator >>> operator.eq(42, 42) # أي يساوي، وهي مماثلة للتعبير 42 == 42 True >>> operator.ne('cat', 'dog') # أي لا يساوي وهي مماثلة للتعبير‫ 'cat' != 'dog' True >>> operator.gt(10, 20) # أكبر من، وهي مماثلة للتعبير 20 < 10 False >>> operator.ge(10, 10) # أكبر من أو يساوي، وهي مماثلة للتعبير 10 =< 10 True >>> operator.lt(10, 20) # أصغر من، وهي مماثلة للتعبير 20 > 10 True >>> operator.le(10, 20) # أصغر من أو يساوي وهي مماثلة للتعبير 10 => 20 True ستعطينا وحدة operator نسخ دوال من عوامل المقارنة ويكون تنفيذها بسيط. مثلًا يمكننا كتابة دالة operator.eq()‎ في سطرين: def eq(a, b): return a == b من المفيد امتلاك نسخ لعوامل المقارنة على هيئة دوال لأنه على عكس العوامل، يمكن تمرير الدوال مثل وسطاء لاستدعاءات الدالة، وسنفعل ذلك لتنفيذ تابع مساعدة لتوابع المقارنة السحرية. أولًا، ضِف التالي إلى بداية الملف wizcoin.py، إذ تعطي تعليمات الاستيراد import هذه الإذن بالوصول للدوال في وحدة operator وتسمح لك بالتحقق أن الوسيط other في التابع هو متتالية sequence عن طريق مقارنته مع collections.abc.Sequence: import collections.abc import operator ثم ضِف التالي في نهاية ملف wizcoin.py: --snip-- 1 def _comparisonOperatorHelper(self, operatorFunc, other): """A helper method for our comparison dunder methods.""" 2 if isinstance(other, WizCoin): return operatorFunc(self.total, other.total) 3 elif isinstance(other, (int, float)): return operatorFunc(self.total, other) 4 elif isinstance(other, collections.abc.Sequence): otherValue = (other[0] * 17 * 29) + (other[1] * 29) + other[2] return operatorFunc(self.total, otherValue) elif operatorFunc == operator.eq: return False elif operatorFunc == operator.ne: return True else: return NotImplemented def __eq__(self, other): # eq is "EQual" 5 return self._comparisonOperatorHelper(operator.eq, other) def __ne__(self, other): # ne is "Not Equal" 6 return self._comparisonOperatorHelper(operator.ne, other) def __lt__(self, other): # lt is "Less Than" 7 return self._comparisonOperatorHelper(operator.lt, other) def __le__(self, other): # le is "Less than or Equal" 8 return self._comparisonOperatorHelper(operator.le, other) def __gt__(self, other): # gt is "Greater Than" 9 return self._comparisonOperatorHelper(operator.gt, other) def __ge__(self, other): # ge is "Greater than or Equal" a return self._comparisonOperatorHelper(operator.ge, other) تستدعي توابع المقارنة السحرية التابع ‎__comparisonOperatorHelper()‎ وتمرر الدالة المناسبة من وحدة operator إلى المعامل operatorFunc، عند استدعاء operatorFunc()‎ فنحن هنا نستدعي الدالة المُمرّرة إلى معامل operatorFunc الذي هو eq()‎ أو ne()‎ أو lt()‎ أو le()‎ أو gt()‎ أو ge()‎ من وحدة operator، أو سيكون علينا تكرار الشيفرة في ‎__comparisonOperatorHelper()‎ في كل من توابع المقارنة السحرية الستة. ملاحظة: تدعى الدوال (أو التوابع) التي تقبل دوال أخرى على أنها وسطاء، مثل ‎__comparisonOperatorHelper()‎ بدوال المراتب الأعلى higher-order functions. يمكن الآن مقارنة كائنات WizCoin مع كائنات WizCoin أخرى وأعداد صحيحة وعشرية وقيم سلسلة من ثلاث قيم عددية تمثل galleons و sickles و knuts. أدخل التالي في الصدفة التفاعلية لرؤية الأمر عمليًا: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) # إنشاء كائن‫ WizCoin >>> tipJar = wizcoin.WizCoin(0, 0, 37) # إنشاء كائن‫ WizCoin آخر >>> purse.total, tipJar.total # فحص القيم وفقًا إلى‫ knuts (1141, 37) >>> purse > tipJar # ‫المقارنة بين كائنات WizCoin باستخدام عامل مقارنة True >>> purse < tipJar False >>> purse > 1000 # الموازنة مع عدد صحيح True >>> purse <= 1000 False >>> purse == 1141 True >>> purse == 1141.0 # المقارنة مع عدد عشري True >>> purse == '1141' # ‫كائن WizCoin ليس مساويًا لأي قيمة سلسلة نصية False >>> bagOfKnuts = wizcoin.WizCoin(0, 0, 1141) >>> purse == bagOfKnuts True >>> purse == (2, 5, 10) # يمكننا المقارنة مع صف يتكون من ثلاثة أعداد صحيحة True >>> purse >= [2, 5, 10] # يمكننا المقارنة مع قائمة تحتوي على ثلاثة أعداد صحيحة True >>> purse >= ['cat', 'dog'] # يجب أن تتسبب هذه التعليمة بخطأ Traceback (most recent call last): File "<stdin>", line 1, in <module> File "C:\Users\Al\Desktop\wizcoin.py", line 265, in __ge__ return self._comparisonOperatorHelper(operator.ge, other) File "C:\Users\Al\Desktop\wizcoin.py", line 237, in _comparisonOperatorHelper otherValue = (other[0] * 17 * 29) + (other[1] * 29) + other[2] IndexError: list index out of range يستدعي التابع المساعد isinstance(other, collections.abc.Sequence)‎ لرؤية ما إذا كان other هو نوع بيانات متتالية مثل صف tuple أو قائمة list. بإمكاننا كتابة شيفرة مثل purse >= [2, 5, 10]‎ لعمل مقارنة سريعة، وذلك بجعل كائنات WizCoin قابلة للمقارنة مع متتاليات. مقارنة المتتاليات تضع بايثون أهمية أكبر على العناصر الأولى في المتتالية عند مقارنة كائنين من أنواع المتتاليات المضمنة مثل السلاسل النصية والقوائم والصفوف أي أنها لا تقارن العناصر الأخيرة إلا إذا كانت لدى العناصر الأولى قيم متساوية. مثلًا أدخل التالي في الصدفة التفاعلية: >>> 'Azriel' < 'Zelda' True >>> (1, 2, 3) > (0, 8888, 9999) True تأتي السلسلة النصية Azriel قبل (أي هي أقل من) Zelda لأن 'A' تأتي قبل 'Z'. الصف (3, 2, 1) يأتي بعد (أي هو أكبر من) (9999, 8888, 0) لأن 1 هي أكبر من 0. أدخل التالي في الصدفة التفاعلية: >>> 'Azriel' < 'Aaron' False >>> (1, 0, 0) > (1, 0, 9999) False لا تأتي Azriel قبل Aaron على الرغم من أن 'A' في 'Azriel' تساوي 'A' في 'Aaron' ولكن 'z' التالية في 'Azriel' لا تأتي قبل 'a' في 'Aaron'، ويمكن تطبيق الشيء ذاته في الصفين (1, 0, 0) و (1, 0, 9999)، إذ أن العنصرين في كل صف متساويين لذا تحدد العناصر الثالثة (0 و 9999 على التتالي) أن (0, 0, 1) تأتي قبل (9999, 0, 1). هذا يجبرنا على اتخاذ قرار بشأن تصميم صنف WizCoin فهل يجب أن تأتي WizCoin(0, 0, 9999)‎ قبل أو بعد WizCoin(1, 0, 0)‎؟ إذا كان عدد galleons أهم من عدد sickles أو knuts فيجب على WizCoin(0, 0, 9999)‎ أن تأتي قبل WizCoin(1, 0, 0)‎، أما إذا قارننا الكائنات بالاعتماد على قيمة knuts فيجب أن تأتي WizCoin(0, 0, 9999)‎ (قيمتها ‎9999 knuts) بعد WizCoin(1, 0, 0)‎ (قيمتها 493‎ knuts).وُضعت قيمة الكائن في ملف wzicoin.py على أنها مقدرة بـ knuts لأنها تجعل السلوك متناسقًا مع كيفية مقارنةWizCoin مع الأعداد الصحيحة والعشرية. هذا نوع من الاختيارات التي يجب أن تفعلها عند تصميم الأصناف الخاصة بك. لا توجد توابع سحرية مقارنة معكوسة مثل ()__req__ أو ()__rne__ تحتاج لتنفيذها، وبدلًا عن ذلك نجد أن ()__lt__ و ()__gt__ تعكس بعضها و ()__le__ و ()__ge__ تعكس بعضها و ()__eq__ و ()__ne__ تعكس نفسها، سبب ذلك هو أن العلاقات التالية صحيحة مهما كانت القيم في يمين أو يسار المعامل. purse > [2, 5, 10]‎ هي نفس ‎[2, 5, 10] < purse purse >= [2, 5, 10]‎ هي نفس ‎[2, 5, 10] <= purse purse == [2, 5, 10]‎ هي نفس ‎[2, 5, 10] == purse purse! = [2, 5, 10]‎ هي نفس ‎[2, 5, 10] != purse بمجرد تطبيقك للدوال السحرية المقارنة، ستستخدم بايثون تلقائيًا دالة sort()‎ لترتيب الكائنات الخاصة بك. أدخل التالي في الصدفة التفاعلية: >>> import wizcoin >>> oneGalleon = wizcoin.WizCoin(1, 0, 0) # ‫تكافئ 493 knut >>> oneSickle = wizcoin.WizCoin(0, 1, 0) # ‫تكافئ 29 knut >>> oneKnut = wizcoin.WizCoin(0, 0, 1) # ‫تكافئ 1 knut >>> coins = [oneSickle, oneKnut, oneGalleon, 100] >>> coins.sort() # رتّب من القيمة الأقل إلى الأعلى >>> coins [WizCoin(0, 0, 1), WizCoin(0, 1, 0), 100, WizCoin(1, 0, 0)] يحتوي الجدول 3 قائمة كاملة من توابع المقارنة السحرية ودوال operator. التابع السحري المعامل معامل المقارنة الدالة في وحدة operator ()__eq__ يساوي == operator.eq()‎ ()__ne__ لا يساوي =! operator.nt()‎ ()__lt__ أصغر من < operator.lt()‎ ()__le__ أصغر أو يساوي => operator.le()‎ ()__gt__ أكبر من < operator.gt()‎ ()__ge__ أكبر أو يساوي =< operator.ge()‎ الجدول 3: توابع المقارنة السحرية ودوال وحدة operator. يمكنك رؤية تطبيق هذه التوابع في https://autbor.com/wizcoinfull. التوثيق الكامل لتوابع المقارنة السحرية في توثيقات بايثون https://docs.python.org/3/reference/datamodel.html#object.lt. الخلاصة تسمح توابع المقارنة السحرية لكائنات الأصناف الخاصة بك أن تستخدم معاملات بايثون للمقارنة بدلًا من إجبارك على إنشاء توابع خاصة بك. إذا كنت تُنشئ توابعًا اسمها equals()‎ و isGreaterThan()‎ فهذه ليست خاصة ببايثون، وعدّ هذه إشارة لك لتبدأ باستخدام توابع المقارنة السحرية. ترجمة -وبتصرف- لقسم من الفصل PYTHONIC OOP: PROPERTIES AND DUNDER METHODS من كتاب Beyond the Basic Stuff with Python. اقرأ المزيد المقال السابق البرمجة كائنية التوجه في بايثون: التوابع السحرية Dunder Methods. كيفية إنشاء الأصناف وتعريف الكائنات في بايثون 3. التوابع السحرية (Magic Methods) في PHP. البرمجة الوظيفية Functional Programming وتطبيقها في بايثون
  6. لدى لغة بايثون Python أسماء توابع خاصة تبدأ وتنتهي بشرطتين سفليتين وتختصر بالسحرية، وتسمى عادةً التوابع السحرية أو التوابع الخاصة أو توابع داندر Dunder Methods، أنت تعرف مسبقًا اسم التابع السحري ‎__init__()‎ ولكن لدى بايثون العديد غيره، نستخدمهم عادةً لزيادة تحميل المعامل، أي إضافة سلوكيات خاصة تسمح لنا باستخدام كائنات الأصناف الخاصة بنا مع معاملات بايثون، مثل + أو >=. تسمح التوابع السحرية الأخرى لكائنات الأصناف الخاصة بنا بالعمل مع وظائف بايثون المضمنة مثل len()‎ و repe()‎. كما هي الحال في ‎__init__()‎ أو توابع الجلب والضبط والحذف، لا نستدعي التوابع السحرية مباشرةً، يل تستدعيهم بايثون في الخلفية عندما تستخدم الكائنات مع المعاملات أو بعض الوظائف المضمنة. مثلًا، إذا أنشأت تابعًا اسمه ‎__len__()‎ أو ‎__repr__()‎ للأصناف الخاصة بك فستُستدعى في الخلفية عندما يمرر كائن من هذا الصنف إلى الدالة len()‎ أو repr()‎ على التوالي. هذه التوابع موثقة على الويب في توثيقات بايثون الرسمية. سنحرص على التوسع في صنف WizCoin أثناء استكشافنا لأنواع التوابع السحرية المختلفة وذلك لتحقيق أكبر استفادة ممكنة. توابع تمثيل السلاسل النصية السحرية يمكن استخدام التوابع السحرية ‎__repr__()‎ و ‎__str__()‎ لإنشاء سلسلة نصية تمثل كائنات لا تتعامل معها بايثون عادةً، إذ تُنشئ بايثون عادةً سلاسل تمثيل نصية للكائنات بطريقتين، سلسلة repr النصية وهي سلسلة نصية لشيفرة بايثون التي تُنشئ نسخة من الكائن عندما تُنفذ، وسلسلة str النصية التي هي سلسلة يستطيع الإنسان قراءتها وتؤمن معلومات واضحة ومفيدة عن الكائن. تعاد سلاسل repr و str عن طريق الدوال المبنية مسبقًا repr()‎ و str()‎ على التوالي. مثلًا، أدخل التالي إلى الصدفة التفاعلية لرؤية السلسلتين النصيتين repr و str للكائن datetime.date: >>> import datetime 1 >>> newyears = datetime.date(2021, 1, 1) >>> repr(newyears) 2 'datetime.date(2021, 1, 1)' >>> str(newyears) 3 '2021-01-01' 4 >>> newyears datetime.date(2021, 1, 1) في هذا المثال، سلسلة repr‏ -أي datetime.date(2021, 1, 1)‎- للكائن datetime.date(السطر 2) هي حرفيًا سلسلة نصية لشيفرة بايثون التي تُنشئ نسخةً من الكائن (السطر 1). تؤمن هذه النسخة تمثيلًا دقيقًا للكائن، ومن جهة أخرى، السلسلة النصية str‏ -أي 2021-01-01- للكائن datetime.date (السطر 3) هي سلسلة نصية تمثل قيمة الكائن بطريقة سهلة القراءة للبشر. إذا أدخلنا ببساطة الكائن في الصدفة التفاعلية (السطر 4)، تظهِر السلسلة النصية repr. تظهر غالبًا السلسلة النصية str للمستخدمين وتُستخدم السلسلة النصية repr للكائن في السياق التقني مثل رسائل الخطأ والسجلات. تعلم بايثون كيفية إظهار الكائنات في أنواعها المبنية مسبقًا مثل الأعداد الصحيحة والسلاسل النصية، ولكنها لا تعلم كيفية إظهار الكائنات للأصناف التي أنشأناها نحن. إذا لم يعرف repr()‎ كيفية إنشاء سلسلة نصية repr أو str لكائن، ستكون السلسلة النصية مغلفة بأقواس مثلثة وتحتوي عنوان الذاكرة واسم للكائن '<wizcoin.WizCoin object at 0x00000212B4148EE0>' لإنشاء هذا النوع من السلاسل النصية لكائن WizCoin أدخل التالي إلى الصدفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> str(purse) '<wizcoin.WizCoin object at 0x00000212B4148EE0>' >>> repr(purse) '<wizcoin.WizCoin object at 0x00000212B4148EE0>' >>> purse <wizcoin.WizCoin object at 0x00000212B4148EE0> لا تمتلك هذه السلاسل فائدة كبيرة وصعبة القراءة، لذا يمكننا إخبار بايثون ما نريد استخدامه عن طريق تطبيق التوابع السحرية ‎__repr__()‎ و ‎__str__()‎؛ إذ يحدد التابع ‎__repr__()‎ أي سلسلة نصية يجب أن تُعيدها بايثون عندما يمرر الكائن إلى الدالة المبنية مسبقًا repr()‎؛ بينما يحدد التابع ‎__str__()‎ أي سلسلة نصية يجب أن تُعيدها بايثون عندما يمرر الكائن إلى الدالة المبنية مسبقًا str()‎. ضِف التالي إلى نهاية ملف wizcoin.py: --snip-- def __repr__(self): """Returns a string of an expression that re-creates this object.""" return f'{self.__class__.__qualname__}({self.galleons}, {self.sickles}, {self.knuts})' def __str__(self): """Returns a human-readable string representation of this object.""" return f'{self.galleons}g, {self.sickles}s, {self.knuts}k' عندما نمرر purse إلى repr()‎ و str()‎ يستدعي بايثون التوابع السحرية ‎__repr__()‎ و ‎__str__()‎، أي نحن لا نستدعي التوابع السحرية في الشيفرة الخاصة بنا. لاحظ أن السلسة النصية f التي تضم الكائن في الأقواس تستدعي ضمنًا str()‎ للحصول على السلسة النصية str. مثلًا أدخل التالي إلى الصدفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> repr(purse) # Calls WizCoin's __repr__() behind the scenes. 'WizCoin(2, 5, 10)' >>> str(purse) # Calls WizCoin's __str__() behind the scenes. '2g, 5s, 10k' >>> print(f'My purse contains {purse}.') # Calls WizCoin's __str__(). My purse contains 2g, 5s, 10k. عندما نمرر الكائن WizCoin في purse إلى الدالتين repr()‎ و str()‎، تستدعي بايثون في الخلفية التابعين ‎__repr__()‎ و ‎__str__()‎ الخاصين بالصنف WizCoin. برمجنا هذين التابعين ليعيدا سلاسلًا نصيةً مفيدةً وسهلة القراءة. إذا أدخلت نص السلسلة النصية repr‏ التالية ‎'WizCoin(2, 5, 10)'‎ إلى الصدفة التفاعلية ستُنشئ كائن WizCoin لديه نفس سمات الكائن في purse. السلسلة النصية str هي تمثيل أسهل للقراءة لقيمة الكائن 2g, 5s, 10k. إذا استخدمت الكائن WizCoin في السلسلة النصية f، ستستخدم بايثون السلسلة النصية str الخاصة بالكائن. إذا كانت الكائنات WizCoin معقدة لدرجة أنه من المستحيل إنشاء نسخة منها باستدعاء دالة بانية Constructor Function واحدة، نغلف السلسلة النصية repr في قوسين مثلثين للتنويه على أنه لا يمكن أن تصبح شيفرة بايثون. هكذا تكون سلسلة تمثيل نصي العامة، مثل '<wizcoin.WizCoin object at 0x00000212B4148EE0>'. كتابة ذلك في الصَدَفة التفاعلية سيرفع خطأ SyntaxError حتى لا يحدث ارتباك بشيفرة بايثون التي تُنشئ نسخة من ذلك الكائن. نستخدم __self.__class__.__qualname بدلًا من توفير السلسلة النصية WizCoin في الشيفرة داخل التابع ‎__repr__()‎، إذ يستخدم التابع الموروث ‎__repr__()‎ اسم الصنف الفرعي بدلًا من WizCoin. إذا أعدنا تسمية الصنف WizCoin سيستخدم التابع ‎__repr__()‎ الاسم الجديد تلقائيًا. تظهِر السلسلة النصية str للكائن WizCoin السمة بصورة أنيقة ومختصرة. يُفضّل جدًا تطبيق ‎__repr__()‎ و ‎__str__()‎ في كل الأصناف الخاصة بك. المعلومات الحساسة في سلاسل REPR النصية كما ذكرنا سابقًا، نظهر السلاسل النصية str للمستخدمين ونستعمل السلاسل النصية repr في سياق تقني مثل السجلات. ولكن يمكن أن تسبب السلاسل النصية repr مشاكل أمنية، إذا كان الكائن المُنشئ يحتوي على معلومات حساسة مثل كلمات المرور والتفاصيل الطبية والمعلومات الشخصية؛ ففي هذه الحالة تأكد من خلو التابع ‎__repr__()‎ من هذه المعلومات في السلسلة النصية المرجعة، وعند تعطل البرنامج، يجري إعداده بصورة متكررة لتضمين محتويات المتغيرات في ملف السجل للمساعدة في تصحيح الأخطاء، ولا تُعامل عادةً ملفات الدخول هذه على أنها معلومات حساسة. تحتوي ملفات الدخول المفتوحة للعلن في العديد من الحوادث الأمنية كلمات المرور وأرقام بطاقات بنكية وعناوين المنازل ومعلومات حساسة أخرى، خذ ذلك بالحسبان عند كتابة التوابع ‎__repr__()‎ الخاص بصنفك. التوابع السحرية العددية Numeric Dunder Methods تزيد التوابع السحرية العددية أو التوابع السحرية الرياضية من تحميل عامل بايثون الرياضية، مثل + و - و * و / وما شابه. لا نستطيع حاليًا تنفيذ عملية رياضية مثل جمع كائني WizCoin باستخدام العامل +، وإذا حاولنا فعل ذلك سترفع بايثون استثناء TypeError لأنها لا تعرف كيفية إضافة كائنات WizCoin. أدخل التالي إلى الصدفة التفاعلية لمشاهدة هذا الخطأ: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> tipJar = wizcoin.WizCoin(0, 0, 37) >>> purse + tipJar Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'WizCoin' and 'WizCoin' يمكنك استخدام التابع السحري ‏()‏‏__add__‏ بدلًا من كتابة التابع ‎addWizCoin()‎‎‏‏‏‎ لصنف WizCoin، لكي تعمل كائنات WizCoin مع العامل +. أضف التالي إلى نهاية ملف wizcoin.py: --snip-- 1 def __add__(self, other): """Adds the coin amounts in two WizCoin objects together.""" 2 if not isinstance(other, WizCoin): return NotImplemented 3 return WizCoin(other.galleons + self.galleons, other.sickles + self.sickles, other.knuts + self.knuts) تستدعي بايثون التابع ()__add__عندما يكون الكائن WizCoin على يسار المعامل + وتمرر القيمة على الجانب الأيمن من المعامل + للمعامل other (يمكن تسمية المعامل أي شيء ولكن الاصطلاح هو other). تذكر أنه يمكن تمرير أي نوع من أنواع الكائنات إلى التابع ()__add__، لذا يجب على التابع أن يحتوي اختبارات من النوع، فمثلًا ليس من المنطقي إضافة رقم عشري أو عدد صحيح إلى كائن WizCoin لأننا لا نعرف إذا كان يجب إضافته إلى galleons أو sickles أو knuts. يُنشئ التابع ()__add__ كائن WizCoin جديد مع كميات تساوي مجموع السمات galleons و sickles و knuts من self و other3 لأن هذه السمات الثلاث تحتوي الأعداد الصحيحة التي يمكننا استخدام المعامل + عليهم. الآن بعد أن حمّلنا العامل + لصنف WizCoin، يمكننا استخدام العامل + على الكائن WizCoin. يسمح لنا زيادة تحميل العامل + بكتابة شيفرة أكثر قابليّة للقراءة. مثلًا، أدخل التالي إلى الصدفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) # إنشاء كائن‫ WizCoin >>> tipJar = wizcoin.WizCoin(0, 0, 37) # إنشاء كائن‫ WizCoin آخر >>> purse + tipJar # إنشاء كائن‫ WizCoin آخر يحتوي على المجموع WizCoin(2, 5, 47) إذا مُرر نوع الكائن الخطأ إلى other، لن يرفع التابع السحري استثناءً ولكنه سيعيد القيمة المبنية مسبقًا NotImplemented، فمثلًا، other في الشيفرة التالية هي عدد صحيح: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> purse + 42 # لا يمكن إضافة كائنات‫ WizCoin مع الأعداد الصحيحة Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'WizCoin' and 'int' تشير إعادة NotImplemented إلى بايثون لاستدعاء التوابع لتؤدي هذه العملية. سنتوسع حول "التوابع السحرية العددية المعكوسة" بتفصيل أكثر في هذا المقال. تستدعي بايثون في الخلفية التابع ()__add__ مع 42 للمعامل other الذي يعيد NotImplemented مما يؤدي لأن ترفع بايثون TypeError. على الرغم من أنه لا يجب إضافة الأعداد الصحيحة أو طرحهم من الكائن WizCoin إلا أنه من المنطقي السماح للشيفرة بضرب كائنات WizCoin بأعداد صحيحة موجبة عن طريق تعريف تابع سحري ()__mul__. ضِف التالي في نهاية الملف wizcoin.py: --snip-- def __mul__(self, other): """Multiplies the coin amounts by a non-negative integer.""" if not isinstance(other, int): return NotImplemented if other < 0: # Multiplying by a negative int results in negative # amounts of coins, which is invalid. raise WizCoinException('cannot multiply with negative integers') return WizCoin(self.galleons * other, self.sickles * other, self.knuts * other) يسمح لك التابع ()__mul__ بضرب كائنات WizCoin بأعداد صحيحة موجبة. إذا كان other عدد صحيح، فهذا يعني أنه نوع البيانات التي يتوقعه التابع ()__mul__ ولا يجب أن نعيد NotImplemented. ولكن إذا كان العدد الصحيح سالبًا، هذا يعني أن ضربه الكائن WizCoin سيعطي قيم سلبية للنقود في الكائن WizCoin لأن هذا يتعارض مع تصميمنا للصنف، نرفع WizCoinException مع رسالة خطأ مفصلة. ملاحظة: لا يجب تغيير الكائن self في التابع السحري العددي، بل يجب على التابع إنشاء وإعادة كائن جديد بدلًا من ذلك، إذ يُتوقع من العامل + ومن باقي العوامل أيضًا تقييم كائن جديد بدلًا من تعديل قيمة الكائن الموضعي. أدخل التالي في الصدفة التفاعلية لمشاهدة عمل التابع السحري ()__mul__: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) # إنشاء كائن‫ WizCoin >>> purse * 10 # ‫اضرب كائن WizCoin بعدد صحيح WizCoin(20, 50, 100) >>> purse * -2 # الضرب بعدد صحيح سالب يتسبب بخطأ Traceback (most recent call last): File "<stdin>", line 1, in <module> File "C:\Users\Al\Desktop\wizcoin.py", line 86, in __mul__ raise WizCoinException('cannot multiply with negative integers') wizcoin.WizCoinException: cannot multiply with negative integers يظهر الجدول 1 قائمة التوابع السحرية العددية، لا توجد حاجة لتنفيذ كل التوابع في الصنف الخاص بك. حدد التوابع التي تفيدك. التابع السحري العملية المعامل أو الدالة المضمنة ()__add__ جمع + ()__sub__ طرح - ()__mul__ ضرب * ()__matmul__ ضرب المصفوفات (جديد في بايثون 3.5) @ ()__truediv__ قسمة / ()__floordiv__ قسمة عدد صحيح // ()__mod__ نسبة % ()__divmod__ قسمة ونسبة divmode()‎ ()__pow__ رفع للأس **, pow ()__lshift__ انتقال لليسار >> ()__rshift__ انتقال لليمين << ()__and__ عملية ثنائية و & ()__or__ عملية ثنائية أو | ()__xor__ عملية ثنائية أو حصرية ^ ()__neg__ سلبي أحادي - كما في -42 ()__pos__ هوية أحادي + كما في +42 ()__abs__ قيمة مطلقة ()abs ()__invert__ عملية ثنائية عكس ~ ()__complex__ شكل العدد العقدي complex()‎ ()__int__ شكل العدد الصحيح int()‎ ()__float__ شكل العدد العشري float()‎ ()__bool__ شكل بولياني bool()‎ ()__round__ التدوير round()‎ ()__trunc__ الاختصار math.trunc()‎ ()__floor__ التدوير للأسفل math.floor()‎ ()__ceil__ التدوير للأعلى math.ceil()‎ الجدول 1: التوابع السحرية العددية بعض هذه التوابع مهمة لصنف WizCoin، حاول كتابة التطبيق الخاص بك لكل من التوابع ()__sub__ و ()__pow__ و ()__int__ و ()__float__ و ()__bool__. يمكنك مشاهدة أمثلة عن التطبيقات من خلال الرابط https://autbor.com/wizcoinfull. التوثيق الكامل للتوابع السحرية العددية موجود في توثيقات بايثون على الرابط https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types. تسمح التوابع السحرية العددية للكائنات الخاصة بأصنافك استخدام العوامل الرياضية الخاصة ببايثون. استخدم التوابع السحرية العددية في حال كتبت توابع تصف مهمة تابع موجود سابقًا أو دالة مبنية مسبقًا، مثل التابعين multiplyBy()‎ أو convertToInt()‎ أو ما شابه، إضافةً إلى التوابع السحرية المعكوسة أو الموضعية المشروحة في الفقرتين التاليتين. التوابع السحرية العددية المعكوسة تستدعي بايثون التوابع السحرية العددية عندما يكون الكائن على يسار العامل الرياضي، ولكنها تستدعي التابع السحري العددي المعكوس (يسمى أيضًا التابع السحري العددي العكوس أو اليد اليمين) عندما يكون الكائن على الطرف اليمين من العامل الرياضي. التوابع السحرية العددية المعكوسة مفيدة لأن المبرمجين الذين يستخدمون الأصناف الخاصة بك لا يكتبون دومًا الكائن على الطرف اليسار من العامل الذي يقود بدوره لسلوك غير متوقع. لنرى مثلًا ما سيحدث عندما تحتوي purse كائن WizCoin وتعطي بايثون القيمة للتعبير 2 * purse حيث purse هي على الطرف اليمين للمعامل. يُستدعى التابع ()__mul__ للصنف int لأن 2 هو عدد صحيح، وذلك مع تمرير purse للمعامل other. لا يعرف التابع ()__mul__ للصنف int كيف يتعامل مع الكائنات WizCoin لذا يُعيد NotImplemented. لا ترفع بايثون الخطأ TypeError الآن لأن purse تحتوي كائن WizCoin، ويُستدعى التابع ()__rmul__ الخاص بالصنف WizCoin باستخدام 2 ويُمرر إلى المعامل other. ترفع بايثون الخطأ TypeError إذا أعاد التابع ‎__‎rmul__‎()‎‏ القيمة NotImplemented. ما عدا ذلك تكون القيمة المعادة من ()__rmul__ هي نتيجة التعبير 2 * purse. يعمل التعبيرpurse * 2 بصورة مختلفة عندما تكون purse على الجانب الأيسر من المعامل: لأن purse تحتوي كائن WizCoin، إذ يُستدعى تابع ()__mul__ الخاص بالصنف WizCoin ويمرر 2 للمعامل other. ينشئ التابع ()__mul__ كائن WizCoin جديد ويعيده. الكائن المُعاد هو قيمة التعبير purse * 2. لدى التوابع السحرية العددية والتوابع السحرية العددية المعكوسة نفس الشيفرة إذا كانت متبادلة. العوامل المتبادلة مثل الجمع لديها نفس النتيجة بالاتجاهين، 3+2 هي نفس 2+3، ولكن المعاملات الأخرى ليست تبادلية فمثلًا 3-2 ليست 2-3. أي عملية تبادلية يمكنها استدعاء نفس التابع السحري العددي الأساسي عندما يُستدعى التابع السحري العددي المعكوس؛ فمثلًا، أضف التالي في نهاية ملف wizcoin.py لتعريف التابع السحري العددي المعكوس لعامل الضرب: --snip-- def __rmul__(self, other): """Multiplies the coin amounts by a non-negative integer.""" return self.__mul__(other) ضرب عدد صحيح بكائن WizCoin هو تبادلي، إذ أن 2 * purse هي نفس purse * 2. بدلًا من نسخ ولصق الشيفرة من ()__mul__ نستدعي فقط self.__mul__()‎ ونمررها للمعامل other. بعد تحديث mizcoin.py، جرب استخدام تابع الضرب السحري المعكوس عن طريق إدخال التالي إلى الصدفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> purse * 10 # ‪‫يستدعي ‎__mul__()‎ بقيمة 10 للمعامل other WizCoin(20, 50, 100) >>> 10 * purse # ‪‫يستدعي ‎__rmul__()‎ بقيمة 10 للمعامل other WizCoin(20, 50, 100) تذكر أن بايثون تستدعي في التعبير 10 * purse تابع ‏‏()__mul__‏‏ الخاص بالصنف int لمعرفة ما إذا كان بإمكان العدد الصحيح أن يُضرب بكائنات WizCoin. لا يعلم طبعًا صنف بايثون int المبني مسبقًا أي شيء عن الأصناف التي أنشأناها، لذا تعيد NotImplemented. هذا يشير لبايثون باستدعاء التابع ()__rmul__ الخاص بصنف WizCoin، وإذا كان موجودًا للتعامل مع العملية الحسابية،ترفع بايثون استثناء TypeError إذا كان الاستدعائين للتابعين ()__mul__ و ()__rmul__ للصنفين Int و WizCoin على التتالي يعيدان NotImplemented. يمكن إضافة كائنات WizCoin إلى بعضها فقط، وهذا يضمن أن التابع الأول ()__add__ الخاص بالصنف WizCoin سيتعامل مع المعامل لذا لا نحتاج لتنفيذ ()__radd__. مثلًا، في التعبير purse + tipJar يُستدعى التابع ()__add__ للكائن purse وتمرّر tipJar للمعامل other. لا تحاول بايثون استدعاء تابع ()__radd__ الخاص بالكائن tipJar لأن هذا الاستدعاء لن يعيد NotImplemented، وتكون purse هي المعامل other. يحتوي الجدول 2 على قائمة كاملة للتوابع السحرية العددية المعكوسة. التابع السحري العملية المعامل أو الدالة المضمنة ()__radd__ جمع + ()__rsub__ طرح - ()__rmul__ ضرب * ()__rmatmul__ ضرب المصفوفات (جديد في بايثون 3.5) @ ()__rtruediv__ قسمة / ()__rfloordiv__ قسمة عدد صحيح // ()__rmod__ نسبة % ()__rdivmod__ قسمة ونسبة divmode()‎ ()__rpow__ رفع للأس pow, ** ()__rlshift__ انتقال لليسار << ()__rrshift__ انتقال لليمين >> ()__rand__ عملية ثنائية و & ()__ror__ عملية ثنائية أو | ()__rxor__ عملية ثنائية أو حصرية ^ الجدول 2: التوابع السحرية العددية المعكوسة التوثيق الكامل للتوابع السحرية المعكوسة موجود في توثيقات بايثون. الخلاصة تسمح لك بايثون بإعادة تعريف العوامل باستخدام التوابع السحرية التي تبدأ وتنتهي بمحرفي شرطة سفلية، كما يمكن إعادة صياغة العوامل الرياضية الشائعة باستخدام التوابع السحرية العددية والمعكوسة، إذ تقدم هذه التوابع طريقة لعمل عوامل بايثون الموضعية مع كائنات الأصناف التي أنشأتها وإذا لم تكن قادرة على التعامل مع نوع بيانات الكائن على الطرف الأخر من المعامل ستعيد قيمة NotImplemented المبنية مسبقًا. تُنشئ هذه التوابع السحرية وتعيد كائنات جديدة، في حين تُعدل التوابع السحرية الموضعية (التي تُعيد تعريف معاملات الإسناد المدعومة) الكائنات موضعيًا. لا تنفذ التوابع السحرية المقارنة معاملات بايثون الستة للمقارنة فقط، ولكن تسمح لدالة بايثون sort()‎ بترتيب كائنات الأصناف الخاصة بك. ستحتاج لاستخدام الدوال eq()‎ و ne()‎ و lt()‎ و le()‎ و gt()‎ و ge()‎ في وحدة العامل لمساعدتك في تنفيذ هذه التوابع السحرية. تسمح الخواص والتوابع السحرية بكتابة الأصناف الخاصة بك بطريقة متناسقة وقابلة للقراءة، كما تسمح لك بتفادي الشيفرة النمطية التي تتطلبها لغات البرمجة الأخرى مثل جافا. لتعلم كتابة شيفرة بايثون هناك حديثان لريموند هيتغير Raymond Hettiger يتوسعان في هذه الأفكار "تحويل الشيفرة إلى بايثون اصطلاحية". و"ما وراء PEP 8 - أفضل الممارسات لشيفرة جميلة وواضحة" التي تغطي بعض المفاهيم التي ذكرناها وأكثر. ترجمة -وبتصرف- لقسم من الفصل Pythonic OOP: Properities and dunder methods من كتاب Beyond the Basic Stuff with Python. اقرأ المزيد المقال السابق البرمجة كائنية التوجه في بايثون: الخاصيات Properties. الدوال الرياضية المضمنة في بايثون 3. توثيق بايثون.
  7. لدى العديد من اللغات البرمجية ميزات البرمجة كائنية التوجه ولكن تمتلك لغة بايثون Python أفضلها. سيساعدك تعلم كيفية استخدام هذه التقنيات الخاصة ببايثون على كتابة شيفرة مختصرة وسهلة القراءة. تسمح الخاصيات بتنفيذ شيفرة معينة في كل مرة تُقرأ أو تُعدل أو تُحذف سمة كائن معين لضمان عدم وضع الكائن في حالة غير صالحة. تسمى هذه التوابع في لغات البرمجة الأخرى بالجالبة getters أو الضابطة setters. تسمح لك التوابع السحرية Dunder methods باستخدام الكائن الخاص بك مع عوامل بايثون، مثل عامل + ويسمح لك ذلك بجمع كائني datetime.timedelta مثل datetime.timedelta(days=2)‎‏‏‏‏‏‏ و datetime.timedelta(days=3)‎‏ لإنشاء كائن datetime.timedelta(days=5)‎. سنتوسع في الصنف WizCoin الذي بدأنا به سابقًا بالإضافة لاستخدام أمثلة أخرى، وذلك بإضافة الخاصيات وإعادة تعريف العوامل في التابع السحري. ستجعل هذه الميزات كائنات WizCoin معبرةً بصورةٍ أكبر وأسهل للاستخدام في أي تطبيق يستورد وحدة wizcoin. الخاصيات Properties وسَمَ الصنف BankAccount المستخدم سابقًا السمة ‏‏balance_ بأنها خاصة private عن طريق وضع شرطة سفلية في بداية الاسم، ولكن تذكر أن وصف سمة أنها خاصة هو اصطلاح. كل السمات في بايثون هي تقنيًا عامة، يعني أنه يمكن الوصول لها من شيفرة خارج الصنف، كما أنه لا توجد طريقة لمنع الشيفرة من تغيير سمة balance_ سواءً عن قصد أو بغير قصد إلى قيمة غير صالحة. يمكن منع التغييرات العرضية لهذه السمات الخاصة عن طريق الخواص. إذ أن الخاصيات في بايثون هي سمات خُصصت لها توابع جالبة وضابطة وحاذفة، وهذه التوابع بدورها تنظم كيفية قراءة السمة وتغييرها وحذفها. مثلًا، إذا كان لدى السمة فقط قيم أعداد صحيحة سيتسبب تحويلها إلى سلسلة نصية 42 بأخطاء. تستدعي الخاصية تابع الضبط لتنفيذ الشيفرة التي تحل أو على الأقل تنبه مبكرًا على ضبط قيمة غير صالحة. إذا فكرت "لطالما أردت تنفيذ شيفرة في كل مرة يجري فيها الوصول إلى السمة أو تعديلها بتعليمة إسناد أو حذفها بتعليمة del" إذًا عليك استخدام الخواص. تحويل السمة إلى خاصية لننشئ أولًا صنفًا بسيطًا لديه سمة عادية بدلًا من خاصية. افتح نافذة محرر ملفات جديدة وأدخِل الشيفرة التالية واحفظه على النحو التالي regularAttributeExample.py: class ClassWithRegularAttributes: def __init__(self, someParameter): self.someAttribute = someParameter obj = ClassWithRegularAttributes('some initial value') print(obj.someAttribute) # ‫يطبع‏ 'some initial value' obj.someAttribute = 'changed value' print(obj.someAttribute) # يطبع‫ 'changed value' del obj.someAttribute # يحذف السمة‫ someAttribute يحتوي الصنف ClassWithRegularAttributes على سمة عادية اسمها someAttribute. يضبط التابع ‎__init__()‎ السمة someAttribute إلى some initial value، ومن ثم مباشرة نغير قيمة السمة إلى changed value، وعند تنفيذ البرنامَج ستكون المُخرجات على النحو التالي: some initial value changed value يشير هذا الخرج إلى أن الشيفرة بإمكانها تغيير someAttribute لأي قيمة. من مساوئ استخدام السمات العادية هي أن الشيفرة قد تضبط السمة someAttribute إلى قيم غير صالحة. هذه المرونة بسيطة ومريحة ولكن تعني أن someAttribute يمكن أن تُضبط لقيمة غير صالحة وتسبب أخطاء. لنعيد كتابة هذا الصنف باستخدام الخواص، وذلك عن طريق تطبيق الخطوات التالية على سمة تُدعى someAttribute: أعِد تسمية السمة مع بادئة هي شرطة سفلية ‎_someAttribute أنشئ تابعًا اسمه someAttribue مع المزخرف @property. لدى هذا التابع الجالب المعامل self الموجود لدى كل التوابع. أنشئ تابع آخر اسمه someAttribute مع المزخرف someAttribute.setter@. لدى هذا التابع الضابط المعاملين self و value. أنشئ تابع آخر اسمه someAttribute مع المزخرف someAttribute.deleter@. لدى هذا التابع الحاذف المعامل self الموجود لدى كل التوابع. افتح نافذة محرر ملفات جديدة وادخل الشيفرة التالية واحفظها على النحو التالي propertiesExample.py‎: class ClassWithProperties: def __init__(self): self.someAttribute = 'some initial value' @property def someAttribute(self): # هذا التابع هو الجالب return self._someAttribute @someAttribute.setter def someAttribute(self, value): # هذا التابع الضابط self._someAttribute = value @someAttribute.deleter def someAttribute(self): # هذا التابع الحاذف del self._someAttribute obj = ClassWithProperties() print(obj.someAttribute) # ‫يطبع 'some initial value' obj.someAttribute = 'changed value' print(obj.someAttribute) # يطبع‫ 'changed value' del obj.someAttribute # يحذف السمة‫ _someAttribute خرج هذا البرنامج هو خرج الشيفرة في regularAttributeExample.py‎ ذاتها لأنهما ينفذان المهمة ذاتها وهي طباعة السمة الأولية للكائن ومن ثم تحديث السمة وطباعتها مجددًا. لاحظ أن الشيفرة خارج الصنف لا تصل مباشرةً إلى السمة someAttribute_ (لأنها خاصة)، ولكنها تصل إلى خاصية someAttribute. مكونات هذه الخاصية هي مجردة نوعًا ما، وهي التوابع الجالبة والضابطة والحاذفة. عندما نعيد تسمية سمة اسمها someAttribute إلى ‎_someAttribute أثناء إنشاء توابع جالية وضابطة وحاذفة نسمي ذلك خاصية someAttribute. تسمى في هذا السياق سمة ‎_someAttribute حقل الرجوع backing field أو متغير الرجوع backing variable وهي السمة التي تُبنى عليها الخاصية. لمعظم الخاصيات متغير رجوع ولكن ليس جميعها، سننشئ خاصيات بدون متغير رجوع لاحقًا. عندما تنفذ بايثون شيفرة تصل إلى تابع مثل print(obj.someAttribute)‎، فإنها تستدعي في الخلفية تابع الجلب وتستخدم القيمة المعادة. عندما تنفذ بايثون تعليمة إسناد مع خاصية، مثل 'obj.someAttribute = 'changed value، فإنها تستدعي في الخلفية تابع الضبط وتمرر السلسلة النصية 'changed value' من أجل المعامل value. عندما تنفذ بايثون تعليمة del مع خاصية مثل del obj.someAttribute، تستدعي في الخلفية تابع الحذف. تعمل الشيفرة في توابع الجلب والضبط والحذف الخاصة بالخاصية على متغير الرجوع مباشرةً، لأن وصول توابع الجلب والضبط والحذف إلى الخاصية قد يسبب أخطاء. فمثلًا عندما يصل تابع الجلب إلى الخاصية مسببًا استدعاء تابع الجلب لنفسه، وهذا يجعله يصل إلى الخاصية مجددًا ويسبب استدعاء نفسه مجددًا وهكذا دواليك إلى أن يتعطل البرنامج. افتح محرر النصوص وادخل الشيفرة التالية واحفظ التالي في badPropertyExample.py. class ClassWithBadProperty: def __init__(self): self.someAttribute = 'some initial value' @property def someAttribute(self): # التابع الجالب # نسينا هنا استخدام الشرطة السفلية (_) مما تسبب باستخدامنا للخاصية واستدعاء التابع الجالب مجددًا return self.someAttribute # هذا يستدعي التابع الجالب مجددًا @someAttribute.setter def someAttribute(self, value): # التابع الضابط self._someAttribute = value obj = ClassWithBadProperty() print(obj.someAttribute) # ينتج خطأ هنا بسبب استدعاء الدالة الجالبة للدالة الجالبة يستمر الجالب باستدعاء نفسه عند تنفيذ هذه الشيفرة إلى أن يعطي بايثون الاستثناء recursionError: Traceback (most recent call last): File "badPropertyExample.py", line 16, in <module> print(obj.someAttribute) # ينتج خطأ هنا بسبب استدعاء الدالة الجالبة للدالة الجالبة File "badPropertyExample.py", line 9, in someAttribute return self.someAttribute # يستدعي هذا السطر الجالب مجددًا File "badPropertyExample.py", line 9, in someAttribute return self.someAttribute # يستدعي هذا السطر الجالب مجددًا File "badPropertyExample.py", line 9, in someAttribute return self.someAttribute # يستدعي هذا السطر الجالب مجددًا [Previous line repeated 996 more times] RecursionError: maximum recursion depth exceeded يجب على الشيفرة داخل توابع الجلب والضبط والحذف أن تعمل دائمًا على متغير الرجوع لمنع هذا التكرار من الحصول (الذي في بداية اسمه شرطة سفلية) وليس على الخاصية. كما لا يوجد ما يمنع من كتابة الشيفرة على متغير الرجوع حتى مع وجود بادئة الشرطة السفلية التي تجعل الوصول خاص. استخدام الضوابط للتحقق من البيانات الغرض الأساسي من استخدام الخاصيات هي للتحقق من البيانات أو التأكد من أنها في الصيغة المرغوبة. ربما لا تريد لشيفرة خارج الصنف أن تضبط سمة لأي قيمة لأن هذا قد يؤدي إلى أخطاء. يمكنك استخدام الخاصيات لإضافة فحوصات checks تضمن أن القيمة التي تسند إلى السمة هي القيمة الصحيحة. تسمح هذه الفحوصات ملاحظة الأخطاء في مراحل تطوير الشيفرة المبكرة لأنها تسمح برفع استثناء عندما تُضبط أي قيمة غير صالحة. لنحدث ملف wizcoin.py الذي أنشأناه سابقًا ليُحوّل السمات galleons و sickles و knuts إلى خاصيات. سنغير ضابطة هذه الخاصيات لتكون الأرقام الصحيحة الموجبة فقط صالحة. تمثل WizCoin كمية النقود ولا يمكن أن تحتوي نصف قطعة نقدية أو أي قيمة أقل من الصفر، سنرفع استثناء WizCoinException في حال حاولت شيفرة خارج الصنف أن تضبط الخاصيات galleons أو sickles أو knuts إلى قيمة غير صالحة . افتح ملف wizoin.py الذي حفظته سابقًا وعدله ليصبح على النحو التالي: 1 class WizCoinException(Exception): 2 """The wizcoin module raises this when the module is misused.""" pass class WizCoin: def __init__(self, galleons, sickles, knuts): """Create a new WizCoin object with galleons, sickles, and knuts.""" 3 self.galleons = galleons self.sickles = sickles self.knuts = knuts # NOTE: __init__() methods NEVER have a return statement. --snip-- @property 4 def galleons(self): """Returns the number of galleon coins in this object.""" return self._galleons @galleons.setter 5 def galleons(self, value): 6 if not isinstance(value, int): 7 raise WizCoinException('galleons attr must be set to an int, not a ' + value.__class__.__qualname__) 8 if value < 0: raise WizCoinException('galleons attr must be a positive int, not ' + value.__class__.__qualname__) self._galleons = value --snip-- تضيف التغييرات الجديدة صنف WizCoinException الذي يرث من صنف Exception المبني مسبقًا في بايثون. توضح سلسلة توثيق النصية docstring الخاصة بالصنف كيف تستخدمه وحدة wizcoin. تُعد هذه ممارسة جيدة لوحدات بايثون يمكن أن ترفعها كائنات صنف wizcoin عندما يُساء استخدامها، وهكذا عندما يرفع كائن WizCoin أصناف استثناءات أخرى مثل ValueError و TypeError، سيشير هذا غالبًا إلى خطأ في صنف WizCoin. ضبطنا في التابع ‎__‎init__()‎ الخاصيات self.galleons و slef.sickles و self.knuts إلى المعاملات الموافقة. أضفنا في آخر الملف تابع جالب وضابط للسمة self._galleons بعد التابعين total()‎ و weight()‎. يعيد هذا الجالب القيمة في self._galleons ويتحقق التابع الضابط إذا كان القيمة المسندة إلى الخاصية galleons هي عدد صحيح وموجب، إذا فشل واحد من التحقيقين تُرفع WizCoinException برسالة خطأ، كما يمنع هذا التحقق ‎_galleons من أن تُضبط بقيمة غير صالحة طالما تستخدم الشيفرة الخاصية galleons. لدى كل كائنات بايثون تلقائيًا سمة __class__ التي تشير إلى صنف الكائن. بمعنى أخر، __value.__class هي نفس صنف الكائن الذي يعيده type(value)‎ ، كما أنه لدى كائن الصنف هذا سمة __qualname__ التي هي سلسلة نصية لاسم الصنف. تحديدًا هو الاسم المؤهل للصنف الذي يتضمن أسماء أي أصناف يكون كائن الصنف متداخلًا فيها. الأصناف المتداخلة Nested classes محدودة الاستخدام وخارج نطاق موضوعنا. فمثلًا إذا كانت value قد خزّنت الكائن date المعاد بالصيغة datetime.date(2021, 1, 1)‎، ستكون __value.__class__.__qualname هي السلسلة النصية 'date'. تستخدِم رسالة الاستثناء __value.__class__.__qualname (في السطر 7) للوصول إلى السلسلة النصية لقيمة اسم الكائن، إذ يجعل اسم الكائن رسالة الخطأ هذه أكثر إفادة للمبرمج الذي يقرأها لأنها تحدّد أن الوسيط 'value' ليس من النوع الصحيح، وتحدد أيضًا ما هو نوعه السابق وما النوع الذي يجب أن يكون. ستحتاج لنسخ الشيفرة من الجالب والضابط ليستخدمها ‎_galleons ومن أجل سمات ‎_sickles و ‎_knuts أيضًا، إذ تكون شيفراتهم نفسها ما عدا أنها تستخدم السمات ‎_sickles و ‎_knuts بدلًا من ‎_galleons للمتغيرات الراجعة. خاصيات القراءة فقط تحتاج الكائنات الخاصة بك بعض خاصيات القراءة فقط التي لا يمكن ضبطها بمعامل الإسناد =، إذ يمكن جعل الخاصية للقراءة فقط عن طريق حذف التوابع الضابطة والحاذفة. مثلًا، يعيد التابع total()‎ في الصنف WizCoin قيمة الكائن في knuts. يمكننا تغيير ذلك من تابع عادي لخاصية القراءة فقط لأنه في النهاية لا توجد طريقة منطقية لضبط total لكائن WizCoin؛ فإذا ضبطنا total إلى العدد الصحيح 1000، هل ذلك يعني ‎1000 knuts؟ أو ‎1 galleon و ‎493 knuts؟ أو أي تشكيلة أخرى؟ لهذا السبب سنجعل total خاصية للقراءة فقط عن طريق إضافة الشيفرة بالخط الغامق في ملف wizcoin.py: @property def total(self): """Total value (in knuts) of all the coins in this WizCoin object.""" return (self.galleons * 17 * 29) + (self.sickles * 29) + (self.knuts) # Note that there is no setter or deleter method for `total`. بعد إضافة مزخرف التابع porperty@ أمام total()‎، سيستدعي بايثون تابع total()‎ عند الوصول إلى total، ولأنه لا يوجد تابع ضابط ولا حاذف، ترفع بايثون AtrributeError إذا حاولت أي شيفرة تعديل أو حذف total باستخدامه في وسيط أو تعليمة delعلى التتالي. تعتمد قيمة الخاصية total على قيمة الخاصيات galleons و sickles و knuts ولا تعتمد الخاصية على متغير الرجوع المسمى ‎_total . أدخل التالي في الصَدَفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> purse.total 1141 >>> purse.total = 1000 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: can't set attribute ربما لا تريد لبرنامجك أن يتوقف فورًا عند محاولتك تغيير خاصية للقراءة فقط، ولكن هذا السلوك يُفضل على السماح بتغيير خاصية للقراءة فقط؛ فإذا كان برنامجك يسمح بتعديل خاصية للقراءة فقط، فهذا سيسبب بالتأكيد خطأ في مرحلة ما عندما يُنفذ البرنامج. إذا حصل هذا الخطأ لاحقًا بعد تعديل خاصية للقراءة فقط، سيصبح من الصعب تحديد السبب الرئيس، لهذا يسمح لك التعطيل الفوري بملاحظة المشكلة بأقرب وقت. لا تخلط بين خاصيات للقراءة فقط والمتغيرات الثابتة؛ فالمتغيرات الثابتة تكون مكتوبة كلها بحروف كبيرة وتعتمد على المبرمج بعدم تعديلها، ويُفترض أن تبقى قيمتها ثابتة طول فترة تنفيذ البرنامج. خاصية للقراءة فقط هي مثل أي سمة تكون مرتبطة مع كائن. لا يمكن ضبط أو حذف خاصيات للقراءة فقط مباشرةً ولكنها يمكن أن تُقَيّم مع قيمة متغيرة. تتغير الخاصية total للصنف الخاص بنا WizCoin، إذا تغيرت الخاصيات galleons و sickles و knuts. أين تستخدم الخواص كما رأينا في القسم السابق، تقدم الخاصيات سيطرةً أكثر على كيفية استخدام سمات الصنف، وهذه طريقة خاصة ببايثون لكتابة الشيفرة. تشير التوابع المسماة ‏‏‏‏‏‏‏‏getSomeAttribute()‎‏‏‏‏‏‎‎‎‎‎ و setSomeAttribute()‎ أنه يجب استخدام الخاصيات بدلًا عن ذلك. هذا لا يعني أن كل نسخة من تابع مبدوء بجلب get أو ضبط set يجب استبداله مباشرةً بخاصية، بل هناك مواقف يجب فيها استخدام التابع حتى لو بدأ اسمه بجلب أو ضبط وهذه بعض الأمثلة: العمليات البطيئة التي تستغرق أكثر من ثانية أو ثانيتين، مثل تنزيل أو رفع الملفات. العمليات التي لديها آثار جانبية، مثل حدوث تغييرات لسمات وكائنات أخرى. العمليات التي تتطلب وسائط إضافية لتمرر إلى عمليات الجلب أو الضبط، مثل استدعاء تابع emailObj.getFileAttachment(filename)‎. الخلاصة ينظر المبرمجون إلى التوابع بكونها أفعالًا (أي أن التوابع تنجز أعمالًا)، ويعدّوا السمات والخاصيات أسماءً (أي أنها تدل على عنصر أو كائن). إذا كانت الشيفرة الخاصة بك تفعل فعل الجلب أو الضبط بدلًا من جلب أو ضبط العنصر، ربما من الأفضل استخدام تابع جلب أو ضبط. يعتمد هذا القرار على ما يبدو صحيحًا لك كمبرمج بنهاية المطاف. الميزة العظمى لاستخدام خاصيات بايثون هو أنك لست بحاجة لاستخدامها عندما تُنشئ الصنف الخاص بك، إذ يمكنك استخدام السمات العادية وإذا احتجت استخدام الخاصيات لاحقًا يمكنك تحويل السمات إلى خاصيات دون تغيير أو الاستغناء عن أي شيفرة خارج الصنف. عندما ننشئ خاصية باسم السمة، يمكننا إعادة تسمية السمة باستخدام بادئة الشرطة السفلية وسيعمل البرنامج الخاص بنا كما فعل سابقًا. تطبّق بايثون الخاصيات كائنية التوجه بصورةٍ مختلفة عن باقي لغات البرمجة كائنية التوجه مثل جافا و C++‎، فبدلًا من تقديم توابع جالبة وضابطة محددة، لدى بايثون خاصيات تسمح لك بتدقيق السمات وجعلها للقراءة فقط. ترجمة -وبتصرف- لقسم من الفصل Pythonic OOP: Properties and dunder methods من كتاب Beyond the Basic Stuff with Python. اقرأ المزيد الدليل السريع إلى لغة البرمجة بايثون Python 3 البرمجة كائنية التوجه البرمجة كائنية التوجه (Object Oriented Programming) في بايثون - الجزء الأول مختصر البرمجة كائنية التوجه OOP وتطبيقها في بايثون توثيق بايثون
  8. استعرضنا في المقال السابق مفهوم الوراثة في البرامج كائنية التوجه، سنتابع في هذا المقال الموضوع ذاته إذ سنستعرض بعض التوابع المهمة بهذا الخصوص، إضافةً إلى مناقشة مفهوم الوراثة المتعددة الموجودة في لغة بايثون. الدالتين isinstance()‎ و isssubclass()‎ يمكننا تمرير الكائن عندما نريد معرفة نوعه إلى الدالة type()‎ المضمنة كما تحدثنا سابقًا، ولكن إذا أردنا التحقق من نوع كائن فيُفضل استخدام الدالة المبنية مسبقًا isinstance()‎، التي تعيد الدالة قيمة True إذا كان الكائن في الصنف المعطى أو صنفها الفرعي. اكتب ما يلي في الصدفة التفاعلية: >>> class ParentClass: ... pass ... >>> class ChildClass(ParentClass): ... pass ... >>> parent = ParentClass() # إنشاء كائن ‫ParentClass >>> child = ChildClass() # إنشاء كائن ‫ChildClass >>> isinstance(parent, ParentClass) True >>> isinstance(parent, ChildClass) False 1 >>> isinstance(child, ChildClass) True 2 >>> isinstance(child, ParentClass) True لاحظ أن isinstance()‎ تشير إلى أن كائن ChildClass في child هو نسخةٌ من ‎ChildClass‎ (السطر ذو الرقم 1) ونسخةٌ من ‎ParentClass‎ (السطر ذو الرقم 2)، وهذا منطقي لأن كائن ChildClass له علاقة من نوع "is a" مع نوع كائن ParentClass، أي أنه نوع من هذا الكائن. يمكن أيضًا تمرير صف tuple من كائنات الأصناف مثل وسيط ثانٍ لمعرفة إذا كان الوسيط الأول هو واحد من الأصناف الموجودة في الصف: # ‫تُعيد True إذا كانت القيمة 42 عددًا صحيحًا أو سلسلة نصية أو قيمة بوليانية >>> isinstance(42, (int, str, bool)) True if 42 is an int, str, or bool. True الدالة المضمنة الأخرى issubclass()‎ أقل شيوعًا من isinstance()‎ ويمكنها التعرُّف ما إذا كان كائن الصنف الممر إلى الوسيط الأول هو صنف فرعي (أو نفس الصنف) لكائن الصنف المرر إلى الوسيط الثاني: >>> issubclass(ChildClass, ParentClass) # ‫ChildClass صنف فرعي من ParentClass True >>> issubclass(ChildClass, str) # ‫ChildClass ليس صنفًا فرعيًا من من str False >>> issubclass(ChildClass, ChildClass) # ChildClass هو ChildClass True يمكنك تمرير صف من كائنات الصنف بمثابة وسيط ثاني إلى issubclass()‎ كما هو الحال مع Isinstance()‎، وذلك لرؤية ما إذا كان الوسيط الأول هو صنف فرعي لأي من الأصناف في الصف. الفارق الأساسي بين isinstance()‎ و issubclass()‎ هو أن issubclass()‎ تمرر كائني صنف و isinstance()‎ تمرر كائن وكائن صنف. توابع الصنف ترتبط توابع الصنف مع صنف أكثر مقارنةً بالكائنات المفردة مثل التوابع العادية. يمكنك ملاحظة تابع الصنف في الشيفرة عندما ترى علامتين، هما: المزخرف ‎@‎classmethod قبل تعليمة التابع def، واستخدام cls معاملًا أولًا كما في المثال التالي: class ExampleClass: def exampleRegularMethod(self): print('This is a regular method.') @classmethod def exampleClassMethod(cls): print('This is a class method.') # استدعاء تابع الصنف دون إنشاء نسخة كائن ExampleClass.exampleClassMethod() obj = ExampleClass() # بالنظر إلى السطر السابق، السطرين التاليين متكافئين obj.exampleClassMethod() obj.__class__.exampleClassMethod() يعمل المعامل cls مثل self ولكن self تشير إلى كائن بينما يشير المعامل cls إلى صنف الكائن، هذا يعني أن الشيفرة في تابع الصنف لا يمكنها الوصول إلى خاصيات الكائن المفردة أو استدعاء توابع الكائن العادية. تستدعي توابع الأصناف توابع أصناف أخرى وتستطيع الوصول إلى سمات الصنف. نستخدم الاسم cls لأن class هي كلمة مفتاحية في بايثون وكما هو الحال مع باقي الكلمات المفتاحية مثل if و while و import، فنحن لا نستطيع استخدامها في أسماء المعاملات، ونستدعي غالبًا سمات الأصناف من خلال كائن الصنف، مثل ExampleClass.exampleClassMethod()‎، إلا أنه يمكننا استدعاؤهم من خلال أي كائن من الصنف كما في obj.exampleClassMethod()‎. لا تُستخدم توابع الصنف عمومًا وأكثر الحالات استخدامًا هي لتوفير بديل عن توابع الباني constructorإضافةً للتابع ‎__‎‎init‎__()‎. على سبيل المثال، ماذا لو كانت دالة الباني تقبل سلسةً نصيةً من البيانات يحتاجها الكائن الجديد أو سلسلة نصية لاسم ملف يحتوي البيانات التي يحتاجها الكائن الجديد؟ لا نحتاج إلى قائمة معاملات التابع ‏‏()‏‏__init__ لأنها ستكون طويلة ومعقدة، ونستخدم تابع دالة يعيد كائن جديد بدلًا من ذلك. مثلًا، لننشئ صنف AsciiArt (مررنا عليه سابقًا) الذي يستخدم محارف نصية ليشكل صورة: class AsciiArt: def __init__(self, characters): self._characters = characters @classmethod def fromFile(cls, filename): with open(filename) as fileObj: characters = fileObj.read() return cls(characters) def display(self): print(self._characters) # Other AsciiArt methods would go here... face1 = AsciiArt(' _______\n' + '| . . |\n' + '| \\___/ |\n' + '|_______|') face1.display() face2 = AsciiArt.fromFile('face.txt') face2.display() لدى صنف AsciiArt تابع ()__init__ الذي يمكن أن يمرر محارف النص الصورة مثل سلسلة نصية. لديه أيضًا تابع صنف fromFile()‎ الذي يمكن أن يمرر السلسلة النصية لاسم الملف مثل ملف نصي يحتوي فن آسكي ASCII art. يُنشئ كلا التابعين كائنات AsciiArt. نفذ البرنامج وسيكون هناك ملف face.txt يحتوي على وجه فن آسكي ASCII، ليكون الخرج على النحو التالي: _______ | . . | | \___/ | |_______| _______ | . . | | \___/ | |_______| يجعل تابع الصنف fromFile()‎ الشيفرة الخاصة بك سهلة القراءة مقارنةً بجعل ()__init__ يفعل كل شيء. ميزة أُخرى لتابع الصنف هو أن صنف فرعي من AsciiArt يمكن أن يرث تابع fromFile()‎ الخاص (وإعادة تعريفه إذا لزم)، وهذا هو سبب استدعاء cls(characters)‎ في تابع صنف AsciiArt بدلًا من AsciiArt(characters)‎. يعمل استدعاء ()cls أيضًا في الأصناف الفرعية للصنف AsciiArt دون تعديل لأن صنف AsciiArt ليس متوفرًا في التابع، ولكن استدعاء AsciiArt()‎ يستدعي ()__init__ الخاص بصنف AsciiArt بدلًا من ()__init__ الخاص بالصنف الفرعي. يمكنك التفكير في cls على أنها "كائن يمثل هذا الصنف". خذ بالحسبان أنه يجب أن تستخدم التوابع العادية معامل self في مكان ما في الشيفرة الخاصة بهم، ويجب على تابع الصنف دائمًا استخدام المعامل cls. إذا لم يستخدم أبدًا تابع الصنف المعامل cls، فهذه إشارة أن تابع الصنف الخاص بك يجب أن يكون تابعًا عاديًا. سمات الأصناف سمة الصنف هي متغير ينتمي إلى صنف بدلًا من كائن. ننشئ سمة صنف داخل الصنف ولكن خارج كل التوابع كما أنشأنا متغيرات عامة في ملف "‎.py" ولكن خارج كل الدوال. هذا مثال عن سمة صنف اسمها count التي تحصي عدد كائنات CreatCounter المُنشأة. class CreateCounter: count = 0 # هذه سمة لصنف def __init__(self): CreateCounter.count += 1 print('Objects created:', CreateCounter.count) # تطبع 0 a = CreateCounter() b = CreateCounter() c = CreateCounter() print('Objects created:', CreateCounter.count) # تطبع 3 لدى صنف CreatCounter سمة صنف واحدة اسمها count. كل كائنات CreatCounter لديهم هذه السمة بدلًا من أن يكون لكل منهم سمات count منفصلة. لهذا يعد السطر CreateCounter.count += 1 في دالة الباني كل كائن CreatCounter مُنشأ. عندما تنفذ البرنامج، يكون الخرج على النحو التالي. Objects created: 0 Objects created: 3 نادرًا ما نستخدم سمات الصنف حتى هذا المثال "عد كم كائن CreatCounter مُنشأ" يمكن عمله باستخدام متغير عام بدلًا من سمة صنف. التوابع الساكنة لا يحتوي التابع الساكن معاملي self و cls، فالتوابع الساكنة هي دوال لأنها لا تستطيع الوصول إلى سمات أو توابع الصنف وكائناتها. نادرًا ما ستحتاج لاستخدام التوابع الساكنة في بايثون، وإذا قررت إنشاء واحد ننصح جدًا بإنشاء تابع عادي بدلًا عنه. نعرّف التوابع الساكنة بوضع مزخرف ‎@‎staticmethod قبل تعليمة def الخاصة بهم. هذا مثال عن تابع ساكن: class ExampleClassWithStaticMethod: @staticmethod def sayHello(): print('Hello!') # لم يُنشأ أي كائن، فاسم الصنف يسبق‪ ‪ sayHello() Note that no object is created, the class name precedes sayHello(): ExampleClassWithStaticMethod.sayHello() لا يوجد فرق تقريبًا بين التابع الساكن ‏‏sayHello‎()‎‏‏‏ في صنف ExampleClassWithStaticMethod والدالة sayHello()‎. ربما تفضل بالواقع استخدام دالة لأنك تستطيع استدعائها دون الدخول إلى اسم الصنف مسبقًا. التوابع الساكنة شائعة في لغات برمجة أخرى ليس لديها ميزات لغة بايثون المرنة. تضمين التوابع الساكنة inclusion of static methods في بايثون هو لمحاكاة اللغات الأخرى ولا يقدم قيمةً عملية. متى تستخدم الأصناف والميزات كائنية التوجه الساكنة؟ نادرًا ما تحتاج لاستخدام توابع الصنف وسمات الصنف والتوابع الساكنة، فهم عرضةً للاستخدام الزائد إذا كنت تعتقد بالتساؤل التالي: " لماذا لا استخدم الدوال أو المتغيرات العامة بدلًا عن ذلك؟" هذا تلميح لعدم استخدام توابع الأصناف أو سمات الأصناف أو التوابع الساكنة. السبب الوحيد الذي جعلنا نناقش هذه المفاهيم في سلسلة المقالات متوسطة المستوى هو للتعرف عليهم عندما تراهم في الشيفرة، ولكن لا يشجع كثير من المبرمجين على استخدامهم، إذ سيكونوا مفيدين إذا أردت استخدام هيكلية خاصة بك مع مجموعة معقدة من الأصناف التي تتوقع أن تكون أصناف فرعية للمبرمجين الذين سيستخدمون الهيكلية، لكنك لا تحتاجهم عندما تكتب تطبيقات بايثون مباشرةً. للمزيد عن هذه الميزات وعن احتياجهم أو لا، اقرأ منشور فيليب ج. ايبي Phillip J. Eby "بايثون ليس جافا" الموجود على الرابط dirtsimple.org/2004/12/python-is-not-java.html ومنشور ريان تومايكو Ryan Tomayko "مفهوم التابع الساكن" على الرابط tomayko.com/blog/2004/the-static-method-thing. كلمات مهمة كائنية التوجه يبدأ شرح البرمجة كائنية التوجه OOP بالكثير من المصطلحات مثل الوراثة والتغليف Encapsulation والتعددية الشكلية Polymorphism. أهمية معرفة هذه المصطلحات مبالغ فيه، ولكن يجب عليك أن يكون لديك فهم أساسي لهم، شرحنا الوراثة سابقًا، لذا سنشرح المصطلحات الباقية تاليًا، ويمكنك الاطلاع على مقال البرمجة كائنية التوجه (Object Oriented Programming) في لغة سي شارب #C على أكاديمية حسوب لمزيدٍ من المعلومات حول هذه المصطلحات. التغليف لدى كلمة تغليف معنيين شائعين ولكن متقاربين. التعريف الأول هو تجميع البيانات المتعلقة والشيفرة في وحدة واحدة، أي لتغلف يعني أن تضع في صندوق. هذا ما تفعله الأصناف عمومًا؛ فهي تدمج السمات والتوابع. مثلًا، يغلف صنف WizCoin ثلاثة أعداد صحيحة لـ knuts و sickles و galleons إلى كائن WizCoin واحد. أما التعريف الثاني فهو تقنية لإخفاء المعلومات تسمح للكائنات بإخفاء تفاصيل تنفيذ معقدة عن كيفية عمل الكائنات. رأينا ذلك في "السمات والتوابع الخاصة"، إذ يقدم كائن BankAccount توابع deposit()‎ و withdraw()‎ لإخفاء تفاصيل كيفية التعامل مع السمة ‎_‎balance. تعمل الدوال مثل صندوق أسود: كيفية حساب الدالة math.sqrt()‎ للجذر التربيعي لأي رقم مخفية، كل ما عليك معرفته هو أن تعيد الدالة الجذر التربيعي للرقم الممرر لها. التعددية الشكلية polymorphism تسمح التعددية الشكلية بمعالجة كائنات من نوع ما على أنها كائنات من نوع آخر، فمثلًا تعيد الدالة ()len طول الوسيط الممرر إليها، ويمكنك تمرير سلسلة نصية إلى هذه الدالة لمعرفة عدد المحارف المكونة منه، وكذلك يمكن قائمة list أو قاموس dictionary لمعرفة عدد العناصر، أو عدد أزواج مفتاح-قيمة key-value على الترتيب. يدعى نموذج التعددية الشكلية هذا باسم الدوال المعممة generic functions أو التعددية الشكلية القياسية parametric polymorphism لأنها تعالج كائنات ذات أنواع مختلفة. يمكنك الاطلاع على مقال مفهوم البرمجة المعممة Generic Programming على أكاديمية حسوب لمزيدٍ من المعلومات عن البرمجة المعممة. يمكن الإشارة إلى التعددية الشكلية بمصطلح التعددية الشكلية الخاصة ad hoc polymorphism أو زيادة تحميل للعامل operator overloading، إذ يمكن أن يأخذ المعامل (مثل + أو *) سلوكًا مختلفًا اعتمادًا على نوع الكائنات التي تُجرى عليها العملية؛ فمثلًا يجري المعامل + عملية الجمع الحسابي عندما تُجرى العملية على عددين صحيحين أو عشريين، لكنها توصل السلاسل النصية في حال كانت العملية على سلسلتين بدلًا من عددين. لماذا لا نستخدم الوراثة؟ من السهل زيادة تعقيد الأصناف باستخدام الوراثة. كما يشير لوتشيانو رامالهو Luciano Ramalho : "وضع الكائنات في هرمية يرضي حس الترتيب لدينا، ولكن المبرمجين يفعلونها للتسلية". ننشئ أصناف وأصناف فرعية وأصناف تحت فرعية عندما تكون الحاجة لصنف واحد أو اثنين في الوحدة لتفي بالغرض، ولكن تذكر حكمة بايثون الأهم التي ناقشناها سابقًا؛ الحل البسيط أفضل من الحل المعقد. يسمح استخدام البرمجة كائنية التوجه OOP بتنظيم الشيفرة الخاصة بك إلى وحدات units (في هذه الحالة أصناف) سهلة التعامل بدلًا من ملف "‎.py" واحد يحتوي مئات التوابع المعرفة بدون ترتيب معين. تفيد الوراثة إذا كان لديك عدة دوال تعمل في نفس القاموس أو هيكل قائمة البيانات؛ ففي هذه الحالة من المفيد ترتيبهم في صنف. هناك بعض الأمثلة لعدم إنشاء الأصناف أو استخدام الوراثة: إذا كان الصنف يتألف من توابع لا تستخدم المعاملين self و cls، احذف الأصناف واستخدم الدوال بدلًا من التوابع. إذا أنشأت أب بصنف ابن واحد ولم تُنشئ كائنات من الصنف الأب، يمكنك جمعهم بصنف واحد. إذا أنشأت أكثر من ثلاثة أو أربعة مستويات من الأصناف الفرعية، ربما تكون قد استخدمت الوراثة على نحوٍ زائد، اجمع هذه الأصناف الفرعية إلى أصناف أقل. كما توضّح سابقًا في نسختي برنامج إكس أو tic-tac-toe (مع برمجة كائنية التوجه وبدونها)، من الممكن الحصول على برنامج يعمل على نحوٍ سليم وبدون أخطاء دون استخدام الأصناف. لست بحاجة لتصميم برنامج مثل شبكة معقدة من الأصناف، إذ أن الحل البسيط أفضل من الحل المعقد الذي لا يعمل. يتحدث جول سبلوسكي Joel Spolsky عن ذلك في منشوره "لا تدع المصممين رواد الفضاء أن يخيفوك" الموجود على الرابط joelonsoftware.com/2001/04/21/dont-let-architecture-astronauts-scare-you. يجب أن تعرف الآن كيفية عمل مفاهيم البرمجة كائنية التوجه مثل الوراثة، لأنها تساعدك على تنظيم الشيفرة الخاصة بك وجعل التطوير ومعالجة الأخطاء أسهل. تتمتع لغة بايثون بالمرونة، فهي تقدم لك ميزات برمجة كائنية التوجه، لكنها أبضًا لا تطلب منك استخدامها عندما لا تناسب احتياجات البرنامج الخاص بك. الوراثة المتعددة في العديد من لغات البرمجة يكون الصنف أب واحد فقط، ولكن بايثون تدعم آباء متعددين عن طريق تقديم ميزة تدعى الوراثة المتعددة multiple inheritance. مثلًا، يمكننا الحصول على صنف Airplane مع تابع flyInTheAir()‎ وصنف Ship مع تابع floatOnWater()‎، ويمكننا إنشاء صنف FlyingBoat يرث كلًا من Airplane و Ship عن طريق تحديدهما في تعليمة class مفصولين بفواصل. افتح ملف جديد في محرر النصوص واحفظ التالي flayingboat.py: class Airplane: def flyInTheAir(self): print('Flying...') class Ship: def floatOnWater(self): print('Floating...') class FlyingBoat(Airplane, Ship): pass سيرث الكائن المُنشأ التابعين flyInTheAir()‎ و floatOnWater()‎ كما سنرى في الصدفة التفاعلية: >>> from flyingboat import * >>> seaDuck = FlyingBoat() >>> seaDuck.flyInTheAir() Flying... >>> seaDuck.floatOnWater() Floating... الوراثة المتعددة مفهوم بسيط طالما كانت أسماء توابع الأصناف مميزة ولا تتقاطع، وتسمى هذه الأصناف mixins (هذا مصطلح عام لهذا النوع من الأصناف، إذ لا يوجد في بايثون كلمة mixin مفتاحية)، ولكن ماذا سيحصل إذا ورثنا عدة أصناف معقدة تتشارك بأسماء التوابع؟ مثلًا تذكر أصناف لوحة إكس أو ‏‎MiniBoard‏ و HintTTTBoard سابقًا، ماذا لو أردنا صنف يظهر لوحة إكس أو مصغرة مع تقديم بعض النصائح؟ يمكننا إعادة استخدام هذه الأصناف الموجودة باستخدام الوراثة المتعددة. ضِف التالي إلى نهاية ملف tictactoe_oop.py ولكن قبل تعليمة if التي تستدعي الدالة main‎()‎: class HybridBoard(HintBoard, MiniBoard): pass لا يوجد شيء في هذا الصنف، إذ يُعيد استخدام الشيفرة عن طريق وراثة HintBoard و MiniBoard. عدّل الشيفرة في الدالة main()‎ لتُنشئ كائن HybridBoard: gameBoard = HybridBoard() # إنشاء كائن‫ TTT للّوحة لدى كلا الصنفين الأب MiniBoard و HintBoard تابع اسمه getBoardStr()‎ فما الذي ترثه HybridBoard؟ عندما تنفذ البرنامج سيظهر الخرج لوحة إكس أو مصغرة تحتوي على بعض التلميحات: --snip-- X.. 123 .O. 456 X.. 789 X can win in one more move. يبدو أن بايثون دمجت سحريًا تابع getBoardStr()‎ الخاص بصنف MiniBoard و getBoardStr()‎ الخاص بصنف HintBoard، وهذا ممكن لأننا كتبنا التابعين بشكل يمكّنهما العمل مع بعضهما. إذا بدلت ترتيب الأصناف في تعليمة class في صنف HybridBoard لتصبح على النحو التالي: class HybridBoard(MiniBoard, HintBoard): فستخسر التلميحات كليًا: --snip-- X.. 123 .O. 456 X.. 789 لتفهم لماذا حصل ذلك يجب عليك فهم ترتيب استبيان التوابع method resolution order -أو اختصارًا MRO- الخاص ببايثون وكيفية عمل دالة super()‎. ترتيب استبيان التابع لدى برنامج إكس أو الخاص بنا أربعة أصناف لتمثيل الألواح، ثلاثة معرفة بتابع getBoardStr()‎ وواحدة بتابع getBoardStr()‎ موروث كما في الشكل 2 [الشكل 2: الأصناف الأربعة في برنامج لوحات إكس أو] عندما نستدعي getBoardStr()‎ على الكائن HybridBoard، يعرف بايثون أن الصنف HybridBoard ليس لديه تابع بذلك الاسم لذا تفحص أصناف الأب، ولكن لدى الصنف هذا صنفين أب وكلاهما لديه تابع getBoardStr()‎، أي منها يُستدعى؟ يمكننا معرفة ذلك من التحقق من ترتيب استبيان التابع MRO الخاص بصنف HybridBoard وهي القائمة المرتبة من الأصناف التي يتحقق منها بايثون عند وراثة التوابع، أو عندما يستدعي التابع دالة super()‎. يمكنك رؤية ترتيب استبيان التابع للصنف HybridBoard عن طريق استدعاء mro()‎ في الصدفة التفاعلية: >>> from tictactoe_oop import * >>> HybridBoard.mro() [<class 'tictactoe_oop.HybridBoard'>, <class 'tictactoe_oop.HintBoard'>, <class 'tictactoe_oop.MiniBoard'>, <class 'tictactoe_oop.TTTBoard'>, <class 'object'>] يمكنك من خلال القيمة المُعادة رؤية أنه عندما يُستدعى التابع على HybridBoard، يتحقق بايثون من صنف HybridBoard؛ فإذا لم يكن موجودًا، يتحقق بايثون من صنف HintBoard وبعدها من صنف MiniBoard وأخيرًا من صنف TTTBoard. في آخر كل قائمة ترتيب استبيان الدوال MRO هناك صنف object مضمّن يمثل الصنف الأب لكل الأصناف في بايثون. معرفة ترتيب استبيان الدوال MRO من أجل وراثة واحدة أمر سهل؛ فقط اصنع سلسلة chain من أصناف الأب، أما بالنسبة للوراثة المتعددة سيكون الأمر أصعب. يتبع ترتيب استبيان الدوال MRO الخاص ببايثون خوارزمية C3 -التي تقع تفاصيل مناقشتها خارج سياق موضوعنا- ولكنك تستطيع تحديد ترتيب استبيان الدوال MRO بتذكر قاعدتين: يتحقق بايثون من الأصناف الابن قبل أصناف الأب. يتحقق من الأصناف الموروثة في القائمة من اليسار إلى اليمين في تعليمة class. إذا استدعينا getBoardStr()‎ على كائن HybridBoard، يتحقق بايثون من الصنف HybridBoard أولًا وبعدها ونظرًا لكون أصناف الأب من اليسار إلى اليمين هي HintBoard و MiniBorad، يتحقق بايثون من HintBoard. لدى الصنف الأب هذا تابع getBoardStr()‎ لذا يرثها HybridBorad ويستدعيها. لا ينتهي الأمر هنا، يستدعي التابع super().getBoardStr()‎، إذ أن كلمة "super" هي كلمة مضللة نوعًا ما لدالة super()‎ الخاصة ببايثون، لأنها لا تعيد الصنف الأب ولكن الصنف الذي يليها في ترتيب استبيان التوابع MRO، وهذا يعني عندما نستدعي getBoardStr()‎ على الكائن HybridBoard، يكون الصنف التالي في ترتيب استبيان التوابع MRO بعد HintBoard هو MiniBoard وليس الصنف الأب TTTBoard، لذا استدعاء super().getBoardStr()‎ يستدعي تابع getBoardStr()‎ لصنف MiniBoard الذي يعيد سلسلة نصية للوحة إكس أو المصغرة. تعلّق الشيفرة المتبقية في getBoardStr()‎ الخاصة بصنف HintBoard بعد استدعاء super()‎ نص التلميح لهذه السلسة النصية. إذا غيرنا تعليمة class في صنف HybridBoard لتضع MiniBoard أولًا و HintBoard ثانيًا، سيضع ترتيب استبيان التوابع MRO الصنف ‏MiniBoard قبل الصنف HintBoard، ما يعني أن HybridBorad ترث getBoardStr()‎ من MiniBoard التي لا تحتوي استدعاء super()‎. هذا الترتيب هو الذي سبّب الخطأ الذي جعل لوحة إكس أو المصغرة تظهر بدون تلميحات؛ فبدون استدعاء super()‎ تابع getBoardStr()‎ الخاص بصنف MiniBoard لا يستدعي تابع getBoardStr()‎ الخاص بصنف HintBoard. تسمح لك الوراثة المتعددة في إنشاء وظائف كثيرة في كمية قليلة من الشيفرة، لكنها تقود إلى شيفرة معقدة وصعبة القراءة. فضّل الوراثة الواحدة أو أصناف mixin أو عدم الوراثة، هذه التقنيات غالبًا ما تكون قادرة على تنفيذ مهام البرنامج الخاص بك. الخلاصة كما تعيد type()‎ نوع الكائن المرر لها، تعيد توابع isinstace()‎ و issubclass()‎ نوع ومعلومات الوراثة عن الكائن الممرر لها. يمكن أن تحتوي الأصناف توابع كائن وسمات، لكنها تحتوي أيضًا توابع صنف وسمات صنف وتوابع ساكنة، على الرغم من أنها نادرة الاستخدام لكن يمكنها أن تسمح بالتقنيات كائنية التوجه التي لا تستطيع المتغيرات العامة والدوال أن تقدمها. بسمح بايثون للأصناف أن ترث من عدة آباء، على الرغم من أن ذلك ينتج شيفرة صعبة الفهم. تستطيع دالة super()‎ وتوابع الصنف اكتشاف كيف سترث التوابع اعتمادًا على ترتيب استبيان التوابع MRO، إذ يمكنك مشاهدة ترتيب استبيان التوابع MRO الخاص بصنف في الصدفة التفاعلية عن طريق استدعاء التابع mro()‎ على الصنف. غطينا في هذا المقال والمقالات السابقة مفاهيمًا عامة في البرمجة كائنية التوجه OOP، وسنتحدث تاليًا عن تقنيات برمجة كائنية التوجه OOP خاصة ببايثون. ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming And Inheritance من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق البرمجة كائنية التوجه Object-Oriented Programming والوراثة Inheritance البرمجة كائنية التوجه (Object Oriented Programming) في بايثون البرمجة كائنية التوجه (Object Oriented Programming) في لغة سي شارب #C الوراثة المتعددة multiple inheritance
  9. يوفر عليك استدعاء وتعريف الدوال من عدة أماكن نسخ ولصق الشيفرة المصدرية، إذ أن عدم تكرار الشيفرة هو ممارسة جيدة لأنه إذا أردت تغيير هذه الشيفرة المكرّرة (إما لحل بعض الأخطاء أو لإضافة ميزات جديدة)، فستحتاج فقط لتغييرها في مكان واحد، ويصبح البرنامج أقصر دون شيفرة مكررة وأسهل للقراءة. الأمر مماثل بالنسبة للدوال، الوراثة inheritance هي تقنية لإعادة استخدام الشيفرة، ويمكن تطبيقها في الأصناف، وهي وسيلة لوضع الأصناف في علاقة أب-ابن، بحيث يرث الصنف الابن نسخةً من توابع الصنف الأب، ويخفف عليك عبء تكرار التوابع في عدة أصناف. يعتقد العديد من المبرمجين أن الوراثة أمرٌ مبالغٌ فيه أو خطر بسبب زيادة تعقيد شبكات وراثة الأصناف المضافة إلى البرنامج. ليست المنشورات المدونة بعنوان "الوراثة خطيرة" خاطئة كليًا، فمن السهل استغلال البرنامج، ولكن الاستخدام المحدود لهذه التقنية يمكن أن يوفر الكثير من الوقت بما يتعلق بتنظيم الشيفرة. كيف تعمل الوراثة نضع اسم الصنف الأب الموجود أساسًا بين قوسين في تعليمة class لإنشاء صنف ابن. لتتدرب على إنشاء صنف ابن، نفتح نافذة محرر الملفات ونكتب الشيفرة التالية ونحفظها في ملف inheritanceExample.py: 1 class ParentClass: 2 def printHello(self): print('Hello, world!') 3 class ChildClass(ParentClass): def someNewMethod(self): print('ParentClass objects don't have this method.') 4 class GrandchildClass(ChildClass): def anotherNewMethod(self): print('Only GrandchildClass objects have this method.') print('Create a ParentClass object and call its methods:') parent = ParentClass() parent.printHello() print('Create a ChildClass object and call its methods:') child = ChildClass() child.printHello() child.someNewMethod() print('Create a GrandchildClass object and call its methods:') grandchild = GrandchildClass() grandchild.printHello() grandchild.someNewMethod() grandchild.anotherNewMethod() print('An error:') parent.someNewMethod() عندما ننفذ البرنامج، يكون الخرج على النحو التالي: Create a ParentClass object and call its methods: Hello, world! Create a ChildClass object and call its methods: Hello, world! ParentClass objects don't have this method. Create a GrandchildClass object and call its methods: Hello, world! ParentClass objects don't have this method. Only GrandchildClass objects have this method. An error: Traceback (most recent call last): File "inheritanceExample.py", line 35, in <module> parent.someNewMethod() # ParentClass objects don't have this method. AttributeError: 'ParentClass' object has no attribute 'someNewMethod' أنشأنا ثلاثة أصناف ‎‎ParentClass‎ في السطر (1) و ‎ChildClass‎ في السطر (3) و ‎GrandchildClass‎ في السطر (4). الصنف ChildClass هو صنف فرعي للصنف ParentClass، يعني أن ChildClass لديها توابع ParentClass نفسها، ونقول أن ChildClass يرث التوابع من ParentClass، وأيضًا GrandchildClass هو صنف فرعي من ChildClass وبالتالي لديه كل توابع ChildClass وأبيها ParentClass. نسخنا ولصقنا الشيفرة من التابع printHello()‎ باستخدام هذه الطريقة إلى الصنفين ChildClass و GrandChild. أي تغيير للشيفرة في PrintHello()‎ لا يحدث فقط في ParentClass بل في ChildClass و GrandchildClass. هذا نفس تغيير الشيفرة في دالة تُحدّث كل استدعاءات الدالة الخاصة بها. يمكنك رؤية هذه العلاقة في الشكل 1. لاحظ في مخططات الأصناف أن السهم ينطلق من الصنف الفرعي ويشير إلى الصنف الأساس. هذا يعكس أن كل صنف يعرف دائمًا الصنف الأساس الخاص به ولكنه لا يعرف أصنافه الفرعية. [الشكل 1: مخطط هرمي (يسار) ومخطط فين Venn (يمين) يبينان العلاقات بين الأصناف الثلاثة والتوابع التي يستخدموها] تمثّل الأصناف أب- ابن عادةً علاقات "is a"، إذ أن كائن ChildClass هو كائن ParentClass لأن لديه نفس التوابع التي لدى كائن ParentClass، إضافةً إلى بعض التوابع الإضافية التي يعرّفها. هذه هي علاقة باتجاه واحد: ليس كائن ParentClass هو كائن ChildClass. إذا حاول كائن استدعاء someNewMethod()‎ الموجود فقط لكائنات ChildClass (وأصناف ChildClass الفرعية) يعطي بايثون Python خطأ AttributeError. يمكنك الاطلاع على مقال مخططات الفئات (Class Diagram) في لغة النمذجة الموحدة UML على أكاديمية حسوب لمزيدٍ من المعلومات على علاقات "is a" وغيرها في مخططات الأصناف. يعتقد المبرمجون أن الأصناف المتعلقة ببعضها تندرج تحت هرمية علاقات "is a" واقعية، فغالبًا ما ترى في تدريبات البرمجة كائنية التوجه OOP أصناف أب وابن وحفيد: Vehicle▶FourWheelVehicle▶Car أو Animal▶Bird▶Sparrow أو Shape▶Rectangle▶Square لكن تذكر أن السبب الأساسي للوراثة هو إعادة استعمال الشيفرة. تسمح لك الوراثة بتفادي نسخ ولصق الشيفرة إذا كان البرنامج الخاص بك يحتاج إلى صنف بمجموعة من التوابع التي هي مجموعة كبرى من توابع صنف أُخر. نسمي أحيانًا الصنف الابن بالصنف الفرعي subclass أو الصنف المُشتق derived class ونسمي الصنف الأب الصنف الأعلى super class أو الصنف الأساس base class، ويمكنك الاطلاع على مقال الوراثة والتعددية الشكلية Polymorphism والأصناف المجردة Abstract Classes في جافا على أكاديمية حسوب لمزيدٍ من المعلومات. إعادة تعريف التوابع ترث الأصناف الفرعية كل توابع الأصناف الأب، ويمكن لصنف ابن إعادة تعريف تابع موروث عن طريق تقديم التابع والشيفرة الخاصين بها. يكون لدى التابع الذي يعيد التعريف اسم تابع الصنف الأب ذاته. لتوضيح هذا المفهوم لنعد إلى لعبة إكس أو Tic-Tac-Toe التي أنشأناها سابقًا، ولكن هذه المرة سننشئ صنفًا جديدًا MiniBoard وهو صنف فرعي من TTTBoard يعيد تعريف getBoardStr()‎ لرسم لوحة إكس أو أصغر. سيسأل البرنامج أي نوع لوح سيستخدم ولا نحتاج إلى نسخ ولصق باقي توابع TTTBoard لأن MiniBoard سيرثهم. ضِف التالي في نهاية ملف ticktactoe_oop.py لإنشاء صنف ابن لصنف TTTBoard الأصلي، ثم أعد كتابة تابع getBoardStr()‎: class MiniBoard(TTTBoard): def getBoardStr(self): """Return a tiny text-representation of the board.""" # Change blank spaces to a '.' for space in ALL_SPACES: if self._spaces[space] == BLANK: self._spaces[space] = '.' boardStr = f''' {self._spaces['1']}{self._spaces['2']}{self._spaces['3']} 123 {self._spaces['4']}{self._spaces['5']}{self._spaces['6']} 456 {self._spaces['7']}{self._spaces['8']}{self._spaces['9']} 789''' # Change '.' back to blank spaces. for space in ALL_SPACES: if self._spaces[space] == '.': self._spaces[space] = BLANK return boardStr كما في التابع ()getBoardStr الخاص بصنف TTTBoard سينشئ التابع getBoardStr()‎ الخاص بـ MiniBoard لإظهار سلسلة نصية متعددة الأسطر من لوحة إكس أو عندما تمرر إلى دالة print()‎ ولكن هذه السلسلة النصية هي أقصر وتتجاهل الأسطر بين X و O وتستخدم الفواصل للدلالة على الأماكن الفارغة. غيّر السطر في main()‎ ليستنسخ كائن MiniBoard بدلًا من كائن TTTBoard: if input('Use mini board? Y/N: ').lower().startswith('y'): gameBoard = MiniBoard() # Create a MiniBoard object. else: gameBoard = TTTBoard() # Create a TTTBoard object. يعمل البرنامج كما في السابق ما عدا تغيير هذا السطر الواحد في main()‎ وعندما تنفذ البرنامج الآن سيصبح الخرج على النحو التالي: Welcome to Tic-Tac-Toe! Use mini board? Y/N: y ... 123 ... 456 ... 789 What is X's move? (1-9) 1 X.. 123 ... 456 ... 789 What is O's move? (1-9) --snip-- XXX 123 .OO 456 O.X 789 X has won the game! Thanks for playing! يستطيع البرنامج الآن بسهولة الحصول على تنفيذي صنفي لوح إكس أو، وإذا أردت فقط النسخة المصغرة من اللوحة يمكنك ببساطة استبدال الشيفرة في تابع getBoardStr()‎ في TTTBoard. ولكن إذا أردت الاثنين فالوراثة تسمح لك بسهولة إنشاء صنفين عن طريق إعادة استخدام الشيفرة المشتركة بينهما. يمكننا إضافة سمة attribute جديدة إلى TTTBoard اسمها useMiniBoard إذا لم نستخدم الوراثة، ووضع تعليمة if-else داخل getBoardStr()‎ لتقرر متى تُظهِر اللوحة العادية أو اللوحة المصغرة، سيعمل هذا جيدًا لأن التغيير بسيط، ولكن ماذا لو كان الصنف الفرعي MiniBoard يحتاج لإعادة تعريف تابعين أو ثلاثة توابع أو حتى 100 تابع؟ ماذا لو أردنا إنشاء عدة أصناف فرعية من TTTBoard؟ سيتسبّب عدم استخدام الوراثة بسيل من تعليمات if-else داخل التابع الخاص بنا وزيادة كبيرة في تعقيد الشيفرة. يُمكّننا استخدام الأصناف الفرعية وإعادة تعريف التوابع من ترتيب الشيفرة الخاصة بنا ضمن أصناف منفصلة للتعامل مع حالات استخدام مماثلة. دالة super()‎ يشابه تابع الصنف المعاد تعريفه overridden تابع الصنف الأب؛ فحتى لو كانت الوراثة هي تقنية لإعادة استخدام الشيفرة، قد يتطلب إعادة تعريف التابع إعادة كتابة نفس الشيفرة من تابع الصنف الأب بمثابة جزء من تابع شيفرة الابن. لمنع تكرار الشيفرة: تسمح دالة super()‎ للتابع المعاد تعريفه استدعاء التابع الأصلي في الصنف الأب. مثلًا، لنُنشئ صنفًا جديدًا اسمه HintBoard ليكون صنفًا فرعيًا من TTTBoard، بحيث يعيد هذا الصنف تعريف getBoardStr()‎، ويضيف بعد رسم لوحة إكس أو تلميحًا hint فيما إذا كان X أو O قد يربح في الخطوة التالية. هذا يعني أن تابع getBoardStr()‎ الخاص بصنف HintBoard سينجز نفس مهام تابع getBoardStr()‎ الخاص بصنف TTTBoard لرسم لوحة إكس أو. بدلًا من تكرار الشيفرة لإنجاز ذلك، يمكننا استخدام super()‎ لاستدعاء تابع getBoardStr الخاص بصنف TTTBoard من تابع getBoardStr()‎ الخاص بصنف HintBoard. ضِف التالي لنهاية ملف tictactoe_oop.ps: class HintBoard(TTTBoard): def getBoardStr(self): """Return a text-representation of the board with hints.""" 1 boardStr = super().getBoardStr() # Call getBoardStr() in TTTBoard. xCanWin = False oCanWin = False 2 originalSpaces = self._spaces # Backup _spaces. for space in ALL_SPACES: # Check each space: # Simulate X moving on this space: self._spaces = copy.copy(originalSpaces) if self._spaces[space] == BLANK: self._spaces[space] = X if self.isWinner(X): xCanWin = True # Simulate O moving on this space: 3 self._spaces = copy.copy(originalSpaces) if self._spaces[space] == BLANK: self._spaces[space] = O if self.isWinner(O): oCanWin = True if xCanWin: boardStr += '\nX can win in one more move.' if oCanWin: boardStr += '\nO can win in one more move.' self._spaces = originalSpaces return boardStr أولًا، تنفذ التعليمة ‎‏super().getBoardStr()‎ في السطر ذو الرقم 1 الشيفرة داخل الصنف getBoardStr()‎ الخاص بصنف TTTBoard، والتي تعيد سلسلةً نصيةً على شكل لوحة إكس أو. نحفظ حاليًا هذه السلسلة في متغير اسمه boardStr. تعالج الشيفرة الباقية إنشاء التلميح بعد إنشاء لوحة السلسلة النصية عن طريق إعادة استخدام getBoardStr()‎ الخاص بصنف TTTBoard. يعيّن تابع getBoardStr()‎ قيمة المتغيرين xCanWin و oCanWin إلى False، وينسخ احتياطيًا القاموس self._spaces إلى المتغير ‎originalSpaces‎ (السطر ذو الرقم 2)، ثم تُنفَّذ حلقة for على كل أماكن اللوحة من 1 إلى 9. تُضبط سمة self._spaces لنسخ المكتبة originalSpaces، وإذا كانت الخلية فارغة تُوضع X مكانها، إذ يحفز هذا تحريك X إلى الفراغ التالي. سيحدد استدعاء self.isWinner()‎ إذا كانت هذه هي الحركة الرابحة؛ فإذا كانت كذلك تصبح xCanWin هي True. تُكرر هذه الخطوات من أجل O لمعرفة ما إذا كان O يربح بالتحرك إلى هذا المكان (السطر ذو الرقم 3). يستخدم هذا التابع وحدة copy لنسخ القاموس في self._spaces لذا نضيف السطر التالي لأول ملف tictactoe.py. import copy نغير بعدها السطر في main()‎ لنستنسخ كائن HintBoard بدلًا من TTTBoard: gameBoard = HintBoard() # Create a TTT board object. يعمل البرنامج كما كان عدا تغيير السطر الوحيد في main()‎ وعندما ينفذ البرنامج سيكون الخرج على النحو التالي: Welcome to Tic-Tac-Toe! --snip-- X| | 1 2 3 -+-+- | |O 4 5 6 -+-+- | |X 7 8 9 X can win in one more move. What is O's move? (1-9) 5 X| | 1 2 3 -+-+- |O|O 4 5 6 -+-+- | |X 7 8 9 O can win in one more move. --snip-- The game is a tie! Thanks for playing! في نهاية التابع: إذا كانت قيمة xCanWin و oCanWin هي True، تُضاف رسالةٌ إضافية تشير إلى ذلك إلى السلسلة النصية boardStr، وأخيرًا تُعاد القيمة boardStr. لا يحتاج كل تابع معاد تعريفه لاستخدام super()‎؛ فإذا كان يعمل التابع -الذي يعيد التعريف- شيئًا مختلفًا تمامًا عن التابع المُعاد تعريفه في الصنف الأب، لا توجد حاجة لاستدعاء التابع المعاد تعريفه باستخدام super()‎. تفيد الدالة super()‎ على نحوٍ خاص عندما يكون للصنف أكثر من تابع أب كما موضح في الفقرة "الوراثة المتعددة" لاحقًا. فضل التكون Composition على الوراثة الوراثةهي تقنية جيدة لإعادة استخدام الشيفرة، وقد تفكر باستخدامها فورًا في الأصناف الخاصة بك، ولكن ربما لا تريد دومًا أن يكون الأساس والأصناف الفرعية مرتبطة جدًا، فإنشاء مستويات متعددة من الوراثة لا يرتب الشيفرة الخاصة لك أكثر ما يضيف بيروقراطية. على الرغم من أنه بإمكانك استخدام الوراثة للأصناف ذات العلاقات " is a" (بمعنى آخر، عندما يكون الصنف الابن هو نوع من أنواع الصنف الأب)، من المفضل استخدام تقنية تدعى التكوّن composition للأصناف ذات العلاقات "لديه has a". التكوّن هو تقنية تصميم لضم الكائنات في الأصناف الخاصة بك بدلًا من توارث أصناف تلك الكائنات. هذا ما نفعله عندما نضيف خاصيّات إلى الأصناف الخاصة بنا. عند تصميم الأصناف الخاصة بك باستخدام الوراثة فضّل التكوّن على الوراثة، هذا ما كنا نفعله في كل الأمثلة الحالية والسابقة كما يلي: كائن WizCoin "لديه has a" كمية من النقود من أنواع galleon و sickle و knut. كائن TTTBoard "لديه has a" مصفوفة بتسع فراغات. كائن MiniBoard "هو is a" كائن TTTBoard لذا "لديه has a" مصفوفة من تسعة فراغات. كائن HintBoard "هو is a" كائن TTTBoard لذا "لديه has a" مصفوفة من تسعة فراغات. لنعد إلى صنف WizCoin الذي أنشأناه سابقًا. إذا أنشأنا صنف WizardCustomer لتمثل الزبائن في العالم السحري، يجب على هؤلاء الزبائن حمل كمية من المال، الذي نعبّر عنه بصنف WizCoin ولكن لا توجد علاقة "is a" بين الصنفين؛ فكائن WizardCustomer ليس من نوع كائن WizCoin. إذا استخدمنا الوراثة، سنحصل على شيفرة برمجية غير مُعتادة: import wizcoin 1 class WizardCustomer(wizcoin.WizCoin): def __init__(self, name): self.name = name super().__init__(0, 0, 0) wizard = WizardCustomer('Alice') print(f'{wizard.name} has {wizard.value()} knuts worth of money.') print(f'{wizard.name}\'s coins weigh {wizard.weightInGrams()} grams.') في هذا المثال، يرث WizardCustomer توابع الكائن ‎‏WizCoin مثل value()‎ و weightInGrams()‎. تقنيًا يمكن للصنف WizardCustomer الذي ورث من WizCoin أن ينجز بجميع المهام التي ينجزها WizardCustomer، والتي تضم كائن WizCoin، كما تفعل السمة، ولكن اسمَي التابعين wizard.value()‎ و wizard.weightInGrams()‎ مضللة؛ إذ يبدو أنها تُعيد قيمة ووزن الساحر بدلًا من قيمة ووزن نقود الساحر. إضافةً إلى ذلك، إذا أردنا لاحقًا إضافة تابع weightInGrams()‎ لوزن الساحر، سيكون هذا الاسم مأخوذًا مسبقًا. من الأسهل أن يكون الكائن WizCoin سمةً لأن الزبون الساحر "لديه" كميةً من نقود الساحر. import wizcoin class WizardCustomer: def __init__(self, name): self.name = name 1 self.purse = wizcoin.WizCoin(0, 0, 0) wizard = WizardCustomer('Alice') print(f'{wizard.name} has {wizard.purse.value()} knuts worth of money.') print(f'{wizard.name}\'s coins weigh {wizard.purse.weightInGrams()} grams.') بدلًا من جعل الصنف WizardCutomer يرث التوابع من WizCoin، نعطي للصنف WizardCutomer سمة ‎‏purse التي تحتوي كائن WizCoin. أي تغييرات لتوابع الصنف WizCoin عند استخدام التكوّن لن تغير توابع الصنف WizardCustomer. تمنحك هذه الطريقة مرونةً أكبر في تغيير التصميمات المستقبلية لكلا الصنفين وتؤدي إلى شيفرة سهلة الصيانة. مساوئ الوراثة السيئة الأساسية في الوراثة هي أنه أي تغيير مستقبلي يحصل على الأصناف الأب سترثه كل الأصناف الابن. في بعض الحالات هذا الربط الشديد هو ما تحتاجه ولكن في بعض الحالات لا يفي نموذج الوراثة بمتطلبات الشيفرة. مثلًا، لنقل أنه لدينا الأصناف Car و Motorcycle و LunarRover في برنامج محاكاة عربات، ستحتاج هذه الأصناف إلى توابع متماثلة، مثل startIgnition()‎ و changeTire()‎. بدلًا من نسخ ولصق الشيفرة إلى كل صنف، يمكننا إنشاء صنف أب Vehicle ونجعل Car و Motorcycle و LunarRover يرثونها. الآن نريد إصلاح خطأ في تابع changeTire()‎، وسنجري التغيير في مكان واحد. هذا مفيد جدًا، إذ لدينا العديد من أصناف العربات التي ترث من Vehicle. ستكون شيفرة هذه الأصناف على النحو التالي: class Vehicle: def __init__(self): print('Vehicle created.') def startIgnition(self): pass # Ignition starting code goes here. def changeTire(self): pass # Tire changing code goes here. class Car(Vehicle): def __init__(self): print('Car created.') class Motorcycle(Vehicle): def __init__(self): print('Motorcycle created.') class LunarRover(Vehicle): def __init__(self): print('LunarRover created.') لكن كل التغييرات المستقبلية على Vehicle ستؤثر على هذه الأصناف الفرعية أيضًا. ماذا سيحدث لو أردنا تابع changeSparkPlug()‎؟ لدى السيارات والدراجات النارية محركات احتراق بشمعات احتراق ولكن العربات القمرية lunar rovers ليس لديها ذلك. يمكننا -بتفضيل التكوّن على الوراثة- إنشاء صنفي CombustionEngine و ElectricEngine، ثم تصميم صنف Vehicle ليكون "لديه has a" سمة محرك إما CombustionEngine أو ElectricEngine مع التوابع الموافقة. class CombustionEngine: def __init__(self): print('Combustion engine created.') def changeSparkPlug(self): pass # هنا الشيفرة البرمجية التي تعدّل على شمعة الاحتراق class ElectricEngine: def __init__(self): print('Electric engine created.') class Vehicle: def __init__(self): print('Vehicle created.') self.engine = CombustionEngine() # استخدم هذا المحرك افتراضيًا --snip-- class LunarRover(Vehicle): def __init__(self): print('LunarRover created.') self.engine = ElectricEngine() يتطلب هذا إعادة كتابة كمية كبيرة من الشيفرة خصوصًا إذا كان لدينا عدة أصناف ترث من الصنف Vehicle الموجود مسبقًا. كل استدعاءات vehicleObj.changeSparkPlug()‎ ستكون بحاجة لتصبح vehicleObj.engine.changeSparkPlug()‎ لكل كائن في الصنف Vehicle أو أصنافها الفرعية لأن كل تغيير كبير سيحدث أخطاءً ربما تجعل من التابع changeSparkPlug()‎ الخاص بالصنف LunarVehicle لا يفعل شيئًا. تتمثل الطريقة الخاصة ببايثون في هذه الحالة بضبط قيمة changeSparkPlug إلى None في صنف LunarVehicle: class LunarRover(Vehicle): changeSparkPlug = None def __init__(self): print('LunarRover created.') يتبع السطر: changeSparkPlug = None الصياغة المعرفة في "سمات الصنف" التي سنناقشها لاحقًا، وهذا يعيد تعريف التابع changeSparkPlug()‎ الموروث من Vehicle، لذا يسبب استدعاؤه باستخدام كائن LunarRover خطأ: >>> myVehicle = LunarRover() LunarRover created. >>> myVehicle.changeSparkPlug() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'NoneType' object is not callable يؤدي هذا الخطأ إلى فشل البرنامج وتوقفه سريعًا، ويمكننا مباشرةً ملاحظة المشكلة عند استدعاء التابع غير المناسب باستخدام كائن LunarRover. يرث أيضًا كل صنف ابن للصنف LunarRover القيمة None للتابع changeSparkPlug()‎. تخبرنا رسالة الخطأ التالية بأن مبرمج الصنف LunarRover تعمّد ضبط قيمة التابع changSprakPlug()‎ إلى None: TypeError: 'NoneType' object is not callable إذا لم يكن هناك بالأساس تابع، سنحصل على رسالة الخطأ التالية: NameError: name 'changeSparkPlug' is not defined تخلق الوراثة أصنافًا فيها تعقيدات وتناقضات لذا يُفضل استخدام التكوّن بدلًا عنها. الخلاصة الوراثة هي تقنية لإعادة استخدام الشيفرة، تسمح لك إنشاء أصناف ابن التي ترث توابع أصناف الأب، يمكنك إعادة تعريف التوابع لتقدم شيفرةً جديدةً لهم واستخدام super()‎ لاستدعاء التابع الأصلي من الصنف الأب. لدى الأصناف الابن علاقة "is a" مع الصنف الأب الخاصة بها، لأن كائن من الصنف الابن هو كائن للصنف الأب. استخدام الأصناف والوراثة في بايثون اختياري، إذ يرى بعض المبرمجين أن التعقيد المرافق للاستخدام الكثير للوراثة لا يبرر فائدته. من المرونة أكثر استخدام التكوّن بدلًا من الوراثة لأنها تنفذ علاقة "has a" مع كائن من أحد الأصناف وكان من أصناف أخرى بدلًا من وراثة التوابع مباشرةً من هذه الأصناف، فمثلًا قد يحتوي كائن Customer على سمة birthday المسندة إلى كائن Date بدلًا من أن يكون هناك أصناف فرعية من صنف Customer للكائن Date. ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming And Inheritance من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق مقارنة ما بين برامج بايثون Python الاعتيادية وبرامج بايثون كائنية التوجه البرمجة كائنية التوجه (Object Oriented Programming) في بايثون
  10. بدأنا في مقال الجزء الأول ببناء مشروع عملي بلغة رست وهو عبارة عن خادم ويب متعدد مهام المعالجة، إذ بنينا الخادم الأساسي وكان أحادي خيط المعالجة، وعملنا في مقال الجزء الثاني على تحويله إلى خادم متعدد خيوط المعالجة، وسننهي في هذا المقال بناء الخادم ليصبح جاهزًا، فإذا لم تكن قرأت المقالات السابقة، فارجع لها قبل قراءة هذا المقال. الإغلاق الرشيق وتحرير الذاكرة تستجيب الشيفرة 20 للطلبات بصورةٍ غير متزامنة عبر استخدام مجمع خيط كما نريد، إذ نحصل على بعض التحذيرات من حقول workers و id و thread التي لن نستخدمها مباشرةً وتذكرنا أننا لم نحرر أي شيء من الذاكرة. عندما نستخدم الحل البدائي الذي هو استخدام مفتاحي "ctrl-c" لإيقاف الخيط الرئيسي، تتوقف الخيوط مباشرةً حتى لو كانوا يخدّمون طلبًا. سننفّذ سمة Drop لاستدعاء join على كل خيط في المجمع لكي ننهي الطلبات التي تعمل قبل الإغلاق، ثم سننفّذ طريقةً لإخبار الخيوط ألا تقبل طلبات جديدة قبل الإغلاق. لرؤية عمل هذا الكود سنعدّل الخادم ليقبل طلبين فقط قبل أن يغلق مجمع الخيط thread pool. تنفيذ سمة Drop على مجمع خيط لنبدأ بتنفيذ Drop على مجمع الخيط الخاص بنا. عندما يُسقط المجمع يجب أن تجتمع كل الخيوط للتأكد من أن عملهم قد انتهى. تظهر الشيفرة 22 المحاولة الأولى لتطبيق Drop، إذ لن تعمل الشيفرة حاليًا. اسم الملف: src/lib.rs impl Drop for ThreadPool { fn drop(&mut self) { for worker in &mut self.workers { println!("Shutting down worker {}", worker.id); worker.thread.join().unwrap(); } } } [الشيفرة 22: ضم كل خيط عندما يخرج المجمع خارج النطاق] أولًا، نمرّ على كل workers في مجمع الخيط، واستُخدمت ‎&mut هنا لأن self هو مرجع متغيّر، ونريد أيضًا تغيير worker. نطبع لكل عامل رسالةً تقول أن هذا العامل سيُغلق، ثم نستدعي join على خيط العمال. إذا فشل استدعاء join نستخدم unwrap لجعل رست تهلع وتذهب إلى إغلاق غير رشيق. سنحصل على هذا الخطأ عند تصريف هذه الشيفرة: $ cargo check Checking hello v0.1.0 (file:///projects/hello) error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference --> src/lib.rs:52:13 | 52 | worker.thread.join().unwrap(); | ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call | | | move occurs because `worker.thread` has type `JoinHandle<()>`, which does not implement the `Copy` trait | note: this function takes ownership of the receiver `self`, which moves `worker.thread` --> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std/src/thread/mod.rs:1581:17 For more information about this error, try `rustc --explain E0507`. error: could not compile `hello` due to previous error يوضّح الخطأ أننا لا يمكن أن نستدعي join لأنه لدينا استعارة متغيرة على كل worker وتأخذ join ملكية وسطائها، ولمعالجة هذه المشكلة نحن بحاجة لنقل الخيط خارج نسخة Worker التي تملك thread حتى تستطيع join استهلاك الخيط، وقد فعلنا ذلك في الشيفرة 15 من المقال تنفيذ نمط تصميمي Design Pattern كائني التوجه Object-Oriented في لغة رست. إذا احتفظ Worker بـ Option<thread::JoinHandle<()>>‎، يمكننا استدعاء تابع take على Option لنقل القيمة خارج المتغاير Some وإبقاء المتغاير None في مكانه، بمعنى آخر سيحتوي Worker عامل على متغاير Some في Thread الخاص به وعندما نريد تحرير ذاكرة Worker نستبدل Some بالقيمة None حتى لا يوجد لدى Worker أي خيط لينفذه. لذا نحن نعرف أننا نريد تحديث تعريف Worker على النحو التالي. اسم الملف: src/lib.rs struct Worker { id: usize, thread: Option<thread::JoinHandle<()>>, } الآن لنتابع المصرّف لنجد أية أماكن أُخرى تحتاج تغيير، وبالتحقق من الشيفرة نجد خطأين: $ cargo check Checking hello v0.1.0 (file:///projects/hello) error[E0599]: no method named `join` found for enum `Option` in the current scope --> src/lib.rs:52:27 | 52 | worker.thread.join().unwrap(); | ^^^^ method not found in `Option<JoinHandle<()>>` | note: the method `join` exists on the type `JoinHandle<()>` --> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std/src/thread/mod.rs:1581:5 help: consider using `Option::expect` to unwrap the `JoinHandle<()>` value, panicking if the value is an `Option::None` | 52 | worker.thread.expect("REASON").join().unwrap(); | +++++++++++++++++ error[E0308]: mismatched types --> src/lib.rs:72:22 | 72 | Worker { id, thread } | ^^^^^^ expected enum `Option`, found struct `JoinHandle` | = note: expected enum `Option<JoinHandle<()>>` found struct `JoinHandle<_>` help: try wrapping the expression in `Some` | 72 | Worker { id, thread: Some(thread) } | +++++++++++++ + Some errors have detailed explanations: E0308, E0599. For more information about an error, try `rustc --explain E0308`. error: could not compile `hello` due to 2 previous errors لنعالج الخطأ الثاني الذي يشير إلى الشيفرة في نهاية Worker::new، إذ نريد تغليف قيمة thread في Some عندما ننشئ Worker جديد. أجرِ الخطوات التالية لتصحيح هذا الخطأ: اسم الملف: src/lib.rs impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { // --snip-- Worker { id, thread: Some(thread), } } } الخطأ الأول هو في تنفيذ Drop، وذكرنا سابقًا أننا أردنا استدعاء take على قيمة Option لنقل thread خارج worker. أجرِ التغييرات التالية لتصحيح هذا الخطأ: اسم الملف: src/lib.rs impl Drop for ThreadPool { fn drop(&mut self) { for worker in &mut self.workers { println!("Shutting down worker {}", worker.id); if let Some(thread) = worker.thread.take() { thread.join().unwrap(); } } } } كما تحدثنا سابقًا في المقال البرمجة كائنية التوجه OOP في لغة رست، يأخذ التابع take على Option المتغاير Some خارجًا ويبقي None بدلًا عنه. استخدمنا if let لتفكيك Some والحصول على الخيط، ثم استدعينا join على الخيط. إذا كان خيط العامل هو أساسًا None نعرف أن العامل قد حرًر ذاكرته ولا يحصل شيء في هذه الحالة. الإشارة للخيط ليتوقف عن الاستماع إلى الوظائف تُصرّف الشيفرة بدون تحذيرات بعد كل التغييرات التي أجريناها، ولكن الخبر السيء أنها لا تعمل كما أردنا. النقطة المهمة هي في منطق المغلفات المنفذة بواسطة خيوط نسخ Worker، إذ نستدعي حتى اللحظة join لكن لا تُغلق الخيوط لأننها تعمل في loop للأبد بحثًا عن وظائف. إذا أسقطنا Threadpool بتنفيذنا الحالي للسمة drop، سيُمنع الخيط الأساسي للأبد بانتظار الخيط الأول حتى ينتهي، ولحل هذه المشكلة نحتاج لتغيير تنفيذ drop في ThreadPool، ثم إجراء تغيير في حلقة Worker. أولًا، سنغير تنفيذ drop في ThreadPool ليسقِط صراحةً sender قبل انتظار الخيوط لتنتهي. تظهر الشيفرة 23 التغييرات في ThreadPool لتسقط صراحةً sender. استخدمنا نفس تقنياتOption و take كما فعلنا مع الخيط لكي يستطيع نقل sender خارج ThreadPool. اسم الملف: src/lib.rs: pub struct ThreadPool { workers: Vec<Worker>, sender: Option<mpsc::Sender<Job>>, } // --snip-- impl ThreadPool { pub fn new(size: usize) -> ThreadPool { // --snip-- ThreadPool { workers, sender: Some(sender), } } pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { let job = Box::new(f); self.sender.as_ref().unwrap().send(job).unwrap(); } } impl Drop for ThreadPool { fn drop(&mut self) { drop(self.sender.take()); for worker in &mut self.workers { println!("Shutting down worker {}", worker.id); if let Some(thread) = worker.thread.take() { thread.join().unwrap(); } } } } [الشيفرة 23: إسقاط sender صراحةً قبل جمع الخيوط الفعالة] يغلق إسقاط sender القناة، وهذا يشير بدوره إلى عدم إرسال أي رسائل إضافية، وعندما نفعل ذلك تعيد كل الاستدعاءات إلى recv التي تجريها الخيوط الفعالة في الحلقة اللانهائية خطأً. نغير حلقة Worker في الشيفرة 24 لتخرج من الحلقة برشاقة في تلك الحالة، يعني أن الخيوط ستنتهي عندما يستدعي join عليهم في تنفيذ drop في ThreadPool. اسم الملف: src/lib.rs impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { let thread = thread::spawn(move || loop { let message = receiver.lock().unwrap().recv(); match message { Ok(job) => { println!("Worker {id} got a job; executing."); job(); } Err(_) => { println!("Worker {id} disconnected; shutting down."); break; } } }); Worker { id, thread: Some(thread), } } } [الشيفرة 24: الخروج صراحةً من الحلقة عندما تعيد recv خطأ] لرؤية عمل هذه الشيفرة: سنعدل main لتقبل فقط طلبين قبل أن تُغلق الخادم برشاقة كما تظهر الشيفرة 25. اسم الملف: src/main.rs fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); let pool = ThreadPool::new(4); for stream in listener.incoming().take(2) { let stream = stream.unwrap(); pool.execute(|| { handle_connection(stream); }); } println!("Shutting down."); } [الشيفرة 25: إغلاق الخادم بعد خدمة طلبين عن طريق الخروج من الحلقة] لا نريد أن يتوقف خادم حقيقي بعد خدمة طلبين فقط، وتبين هذه الشيفرة أن الإغلاق الرشيق وتحرير الذاكرة يعملان بصورةٍ نظامية. تُعرّف دالة take في سمة Iterator وتحدد التكرار إلى أول عنصرين بالحد الأقصى. سيخرج ThreadPool خارج النطاق في نهاية main وستُطبَّق سمة drop. شغّل الخادم باستخدام cargo run وأرسل ثلاثة طلبات. سيعطي الطلب الثالث خطأ وسترى الخرج في الطرفية على النحو التالي: $ cargo run Compiling hello v0.1.0 (file:///projects/hello) Finished dev [unoptimized + debuginfo] target(s) in 1.0s Running `target/debug/hello` Worker 0 got a job; executing. Shutting down. Shutting down worker 0 Worker 3 got a job; executing. Worker 1 disconnected; shutting down. Worker 2 disconnected; shutting down. Worker 3 disconnected; shutting down. Worker 0 disconnected; shutting down. Shutting down worker 1 Shutting down worker 2 Shutting down worker 3 يمكن أن ترى ترتيبًا مختلفًا للخيوط الفعالة والرسائل المطبوعة. تعمل الشيفرة وفقًا لهذه الرسائل كما يلي: أخذ العاملان 0 و 3 الطلبين الأولين وتوقف الخادم عن قبول الاتصالات بعد ثاني اتصال، وبدأ تنفيذ Drop في العمل على ThreadPool قبل أخذ العامل 3 وظيفته. يفصل إسقاط sender كل العمال ويخبرهم أن يُغلقوا، ويطبع كل عامل رسالةً عندما يُغلقوا ويستدعي مجمع الخيط join لانتظار كل خيط عامل لينتهي. لاحظ ميّزة مهمة في هذا التنفيذ، إذ قام ThreadPool بإسقاط sender وجرّبنا ضم العامل 0 قبل أن يستقبل أي عامل خطأ. لم يتلق العامل 0 أي خطأ من recv بعد، لذا تنتظر كتلة الخيط الأساسية أن ينتهي العامل 0. في تلك الأثناء استقبل العامل 3 وظيفة ثم استقبلت كل الخيوط خطأ. ينتظر الخيط الأساسي باقي العمال لينتهوا عندما ينتهي العامل 0. وبحلول هذه النقطة يخرج كل عامل من حلقته ويتوقف. تهانينا، فقد أنهينا المشروع ولدينا الآن خادم ويب بسيط يستخدم مجمع خيط للاستجابة بصورةٍ غير متزامنة، ونستطيع إجراء إغلاق رشيق للخادم الذي يحرر من الذاكرة كل الخيوط في المجمع. هذه هي الشيفرة الكاملة بمثابة مرجع. اسم الملف: src/main.rs use hello::ThreadPool; use std::fs; use std::io::prelude::*; use std::net::TcpListener; use std::net::TcpStream; use std::thread; use std::time::Duration; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); let pool = ThreadPool::new(4); for stream in listener.incoming().take(2) { let stream = stream.unwrap(); pool.execute(|| { handle_connection(stream); }); } println!("Shutting down."); } fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; stream.read(&mut buffer).unwrap(); let get = b"GET / HTTP/1.1\r\n"; let sleep = b"GET /sleep HTTP/1.1\r\n"; let (status_line, filename) = if buffer.starts_with(get) { ("HTTP/1.1 200 OK", "hello.html") } else if buffer.starts_with(sleep) { thread::sleep(Duration::from_secs(5)); ("HTTP/1.1 200 OK", "hello.html") } else { ("HTTP/1.1 404 NOT FOUND", "404.html") }; let contents = fs::read_to_string(filename).unwrap(); let response = format!( "{}\r\nContent-Length: {}\r\n\r\n{}", status_line, contents.len(), contents ); stream.write_all(response.as_bytes()).unwrap(); stream.flush().unwrap(); } اسم الملف: src/lib.rs use std::{ sync::{mpsc, Arc, Mutex}, thread, }; pub struct ThreadPool { workers: Vec<Worker>, sender: Option<mpsc::Sender<Job>>, } type Job = Box<dyn FnOnce() + Send + 'static>; impl ThreadPool { /// Create a new ThreadPool. /// /// The size is the number of threads in the pool. /// /// # Panics /// /// The `new` function will panic if the size is zero. pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let (sender, receiver) = mpsc::channel(); let receiver = Arc::new(Mutex::new(receiver)); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id, Arc::clone(&receiver))); } ThreadPool { workers, sender: Some(sender), } } pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { let job = Box::new(f); self.sender.as_ref().unwrap().send(job).unwrap(); } } impl Drop for ThreadPool { fn drop(&mut self) { drop(self.sender.take()); for worker in &mut self.workers { println!("Shutting down worker {}", worker.id); if let Some(thread) = worker.thread.take() { thread.join().unwrap(); } } } } struct Worker { id: usize, thread: Option<thread::JoinHandle<()>>, } impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { let thread = thread::spawn(move || loop { let message = receiver.lock().unwrap().recv(); match message { Ok(job) => { println!("Worker {id} got a job; executing."); job(); } Err(_) => { println!("Worker {id} disconnected; shutting down."); break; } } }); Worker { id, thread: Some(thread), } } } يمكننا إجراء المزيد إذا أردنا تحسين المشروع، وإليك بعض الأفكار: أضِف المزيد من التوثيق إلى ThreadPool وتوابعه العامة. أضِف بعض الاختبارات لوظيفة المكتبة. غيّر الاستدعاءات من unwrap إلى معالجة خطأ أكثر متانة. استخدم ThreadPool لتنفيذ أعمال غير خدمة طلبات الويب. ابحث عن وحدة مجمع خيط مصرفة على creats.io ونفذ خادم ويب باستخدام الوحدة المصرفة، ثم قارن واجهة برمجة التطبيقات API والمتانة بينها وبين مجمع الخيط الذي نفذناه. خاتمة عظيم جدًا! فقد وصلنا إلى نهاية سلسلة البرمجة بلغة رست . نريد أن نشكرك لانضمامك إلينا في هذه الجولة في رست. أنت الآن جاهز لتنفيذ مشاريع رست ومساعدة الآخرين في مشاريعهم. تذكر أنه هناك مجتمع مرحب من مستخدمي رست الذين يحبون المساعدة في أي صعوبة يمكن أن تواجهها في استعمالك رست. ترجمة -وبتصرف- لقسم من الفصل Final Project: Building a Multithreaded Web Server من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: بناء خادم ويب متعدد مهام المعالجة بلغة رست - الجزء الثاني تزامن الحالة المشتركة Shared-State Concurrency في لغة رست وتوسيع التزامن مع Send و Sync مقدمة إلى الخيوط Threads في جافا
  11. تعرّفنا في المقال السابق على مفهوم البرمجة كائنية التوجه - أو اختصارًا OOP- وكيفية تعريف الأصناف classes في لغة بايثون، إضافةً إلى بعض التوابع المفيدة بهذا الخصوص. سننظر في هذا المقال على مثال عملي لتطبيق البرمجة كائنية التوجه في لغة بايثون Python ومن ثم سنطّلع على البرنامج ذاته دون استخدام البرمجة كائنية التوجه Non-OOP. أمثلة مع البرمجة كائنية التوجه وبدونها: لعبة إكس أو في البداية، قد يكون من الصعب معرفة كيفية استخدام الأصناف في برامجك. دعنا نلقي نظرةً على مثال قصير للعبة إكس أو لا يستخدم الأصناف، ثم نعيد كتابته باستخدامها. افتح نافذة محرر ملفات جديدة وأدخل البرنامج التالي؛ ثم احفظه بالاسم tictactoe.py: # tictactoe.py, A non-OOP tic-tac-toe game. ALL_SPACES = list('123456789') # The keys for a TTT board dictionary. X, O, BLANK = 'X', 'O', ' ' # Constants for string values. def main(): """Runs a game of tic-tac-toe.""" print('Welcome to tic-tac-toe!') gameBoard = getBlankBoard() # Create a TTT board dictionary. currentPlayer, nextPlayer = X, O # X goes first, O goes next. while True: print(getBoardStr(gameBoard)) # Display the board on the screen. # Keep asking the player until they enter a number 1-9: move = None while not isValidSpace(gameBoard, move): print(f'What is {currentPlayer}\'s move? (1-9)') move = input() updateBoard(gameBoard, move, currentPlayer) # Make the move. # Check if the game is over: if isWinner(gameBoard, currentPlayer): # First check for victory. print(getBoardStr(gameBoard)) print(currentPlayer + ' has won the game!') break elif isBoardFull(gameBoard): # Next check for a tie. print(getBoardStr(gameBoard)) print('The game is a tie!') break currentPlayer, nextPlayer = nextPlayer, currentPlayer # Swap turns. print('Thanks for playing!') def getBlankBoard(): """Create a new, blank tic-tac-toe board.""" board = {} # The board is represented as a Python dictionary. for space in ALL_SPACES: board[space] = BLANK # All spaces start as blank. return board def getBoardStr(board): """Return a text-representation of the board.""" return f''' {board['1']}|{board['2']}|{board['3']} 1 2 3 -+-+- {board['4']}|{board['5']}|{board['6']} 4 5 6 -+-+- {board['7']}|{board['8']}|{board['9']} 7 8 9''' def isValidSpace(board, space): """Returns True if the space on the board is a valid space number and the space is blank.""" return space in ALL_SPACES and board[space] == BLANK def isWinner(board, player): """Return True if player is a winner on this TTTBoard.""" b, p = board, player # Shorter names as "syntactic sugar". # Check for 3 marks across the 3 rows, 3 columns, and 2 diagonals. return ((b['1'] == b['2'] == b['3'] == p) or # Across the top (b['4'] == b['5'] == b['6'] == p) or # Across the middle (b['7'] == b['8'] == b['9'] == p) or # Across the bottom (b['1'] == b['4'] == b['7'] == p) or # Down the left (b['2'] == b['5'] == b['8'] == p) or # Down the middle (b['3'] == b['6'] == b['9'] == p) or # Down the right (b['3'] == b['5'] == b['7'] == p) or # Diagonal (b['1'] == b['5'] == b['9'] == p)) # Diagonal def isBoardFull(board): """Return True if every space on the board has been taken.""" for space in ALL_SPACES: if board[space] == BLANK: return False # If a single space is blank, return False. return True # No spaces are blank, so return True. def updateBoard(board, space, mark): """Sets the space on the board to mark.""" board[space] = mark if __name__ == '__main__': main() # Call main() if this module is run, but not when imported. عند تنفيذ هذا البرنامج، سيبدو الخرج كما يلي: Welcome to tic-tac-toe! | | 1 2 3 -+-+- | | 4 5 6 -+-+- | | 7 8 9 What is X's move? (1-9) 1 X| | 1 2 3 -+-+- | | 4 5 6 -+-+- | | 7 8 9 What is O's move? (1-9) --snip-- X| |O 1 2 3 -+-+- |O| 4 5 6 -+-+- X|O|X 7 8 9 What is X's move? (1-9) 4 X| |O 1 2 3 -+-+- X|O| 4 5 6 -+-+- X|O|X 7 8 9 X has won the game! Thanks for playing! باختصار، يعمل هذا البرنامج باستخدام كائنات القاموس لتُمثل المساحات التسع على لوحة إكس أو. مفاتيح القاموس هي السلاسل من "1" إلى "9"، وقيمها هي السلاسل "X" أو "O" أو " ". المسافات المرقمة بنفس ترتيب لوحة مفاتيح الهاتف. تتمثل وظيفة الدوال في tictactoe.py بما يلي: تحتوي الدالة main()‎‎ على الشيفرة التي تُنشئ بنية بيانات لوحة جديدة (مخزنة في متغير gameBoard) وتستدعي دوالًا أخرى في البرنامج. تُعيد الدالة getBlankBoard()‎‎ قاموسًا به تسع مسافات مضبوطة على " " للوحة فارغة. تقبل الدالة getBoardStr()‎‎ قاموسًا يمثل اللوحة وتعيد تمثيلًا لسلسلة متعددة الأسطر للوحة يمكن طباعتها على الشاشة، وتصيّر هذه الدالة نص لوحة إكس أو tic-tac-toe الذي تعرضه اللعبة. تُعيد الدالة isValidSpace()‎‎ القيمة True إذا مُرّر رقم مسافة صالح وكانت تلك المسافة فارغة. تقبل معاملات دالة isWinner()‎‎ قاموس لوحة إما "X" أو "O" لتحديد ما إذا كان هذا اللاعب لديه ثلاث علامات متتالية على اللوحة. تحدد دالة isBoardFull()‎‎ ما إذا كانت اللوحة لا تحتوي على مسافات فارغة، ما يعني أن اللعبة قد انتهت. تقبل معاملات دالة updateBoard()‎‎ قاموس لوحة ومساحة وعلامة X أو O للاعب وتحدّث القاموس. لاحظ أن العديد من الدوال تقبل اللوحة المتغيرة في معاملها الأول، وهذا يعني أن هذه الدوال مرتبطة ببعضها بعضًا من حيث أنها تعمل جميعها على بنية بيانات مشتركة. عندما تعمل العديد من الدوال في الشيفرة على بنية البيانات ذاتها، فمن الأفضل عادةً تجميعها معًا على أنها توابع وسمات للصنف. دعنا نعيد تصميم هذا في برنامج tictactoe.py لاستخدام صنف TTTBoard الذي سيخزن قاموس board في سمة تسمى spaces. ستصبح الدوال التي كان لها board مثل معامل توابع لصنف TTTBoard الخاصة بنا وستستخدم المعامل self بدلًا من معامل board. افتح نافذة محرر ملفات جديدة، وأدخل الشيفرة التالي، واحفظه باسم tictactoe_oop.py: # tictactoe_oop.py, an object-oriented tic-tac-toe game. ALL_SPACES = list('123456789') # The keys for a TTT board. X, O, BLANK = 'X', 'O', ' ' # Constants for string values. def main(): """Runs a game of tic-tac-toe.""" print('Welcome to tic-tac-toe!') gameBoard = TTTBoard() # Create a TTT board object. currentPlayer, nextPlayer = X, O # X goes first, O goes next. while True: print(gameBoard.getBoardStr()) # Display the board on the screen. # Keep asking the player until they enter a number 1-9: move = None while not gameBoard.isValidSpace(move): print(f'What is {currentPlayer}\'s move? (1-9)') move = input() gameBoard.updateBoard(move, currentPlayer) # Make the move. # Check if the game is over: if gameBoard.isWinner(currentPlayer): # First check for victory. print(gameBoard.getBoardStr()) print(currentPlayer + ' has won the game!') break elif gameBoard.isBoardFull(): # Next check for a tie. print(gameBoard.getBoardStr()) print('The game is a tie!') break currentPlayer, nextPlayer = nextPlayer, currentPlayer # Swap turns. print('Thanks for playing!') class TTTBoard: def __init__(self, usePrettyBoard=False, useLogging=False): """Create a new, blank tic tac toe board.""" self._spaces = {} # The board is represented as a Python dictionary. for space in ALL_SPACES: self._spaces[space] = BLANK # All spaces start as blank. def getBoardStr(self): """Return a text-representation of the board.""" return f''' {self._spaces['1']}|{self._spaces['2']}|{self._spaces['3']} 1 2 3 -+-+- {self._spaces['4']}|{self._spaces['5']}|{self._spaces['6']} 4 5 6 -+-+- {self._spaces['7']}|{self._spaces['8']}|{self._spaces['9']} 7 8 9''' def isValidSpace(self, space): """Returns True if the space on the board is a valid space number and the space is blank.""" return space in ALL_SPACES and self._spaces[space] == BLANK def isWinner(self, player): """Return True if player is a winner on this TTTBoard.""" s, p = self._spaces, player # Shorter names as "syntactic sugar". # Check for 3 marks across the 3 rows, 3 columns, and 2 diagonals. return ((s['1'] == s['2'] == s['3'] == p) or # Across the top (s['4'] == s['5'] == s['6'] == p) or # Across the middle (s['7'] == s['8'] == s['9'] == p) or # Across the bottom (s['1'] == s['4'] == s['7'] == p) or # Down the left (s['2'] == s['5'] == s['8'] == p) or # Down the middle (s['3'] == s['6'] == s['9'] == p) or # Down the right (s['3'] == s['5'] == s['7'] == p) or # Diagonal (s['1'] == s['5'] == s['9'] == p)) # Diagonal def isBoardFull(self): """Return True if every space on the board has been taken.""" for space in ALL_SPACES: if self._spaces[space] == BLANK: return False # If a single space is blank, return False. return True # No spaces are blank, so return True. def updateBoard(self, space, player): """Sets the space on the board to player.""" self._spaces[space] = player if __name__ == '__main__': main() # Call main() if this module is run, but not when imported. يقدّم هذا البرنامج عمل برنامج إكس أو tic-tac-toe السابق ذاته دون استخدام البرمجة كائنية التوجه. يبدو الخرج متطابقًا تمامًا. نقلنا الشيفرة التي كانت في getBlankBoard()‎‎ إلى تابع ‎__init __()‎‎ لصنف TTTBoard، لأنها تؤدي المهمة ذاتها لإعداد بنية بيانات اللوحة. حوّلنا الدوال الأخرى إلى توابع، مع استبدال المعامل board القديم بمعامل self، لأنها تخدم أيضًا غرضًا مشابهًا؛ إذ أن كلاهما كتلتان من الشيفرة البرمجية التي تعمل على بنية بيانات لوحة إكس أو. عندما تحتاج الشيفرة البرمجية في هذه التوابع إلى تغيير القاموس المخزن في السمة ‎_spaces، تستخدم الشيفرة self._spaces، وعندما تحتاج الشيفرة في هذا التابع إلى استدعاء توابع أخرى، فإن الاستدعاء يسبقه self وفترة زمنية period. هذا مشابه لكيفية احتواء coinJars.values()‎‎ في قسم "إنشاء صنف بسيط" على كائن في متغير coinJars. في هذا المثال، الكائن الذي يحتوي على طريقة استدعاء موجود في متغير self. لاحظ أيضًا أن السمة ‎_spaces تبدأ بشرطة سفلية، ما يعني أن الشيفرة البرمجية الموجودة داخل توابع TTTBoard هي فقط التي يجب أن تصل إليها أو تعدلها. يجب أن تكون الشيفرة البرمجية خارج الصنف قادرةً فقط على تعديل المسافات بصورة غير مباشرة عن طريق استدعاء التوابع التي تعدّلها. قد يكون من المفيد مقارنة الشيفرة المصدرية لبرنامجي إكس أو، إذ يمكنك مقارنة الشيفرة وعرض مقارنة جنبًا إلى جنب من خلال الرابط https://autbor.com/compareoop. لعبة إكس أو هي برنامج صغير، لذا لا يتطلب فهمه الكثير من الجهد، ولكن ماذا لو كان هذا البرنامج يتكون من عشرات الآلاف من السطور بمئات الدوال المختلفة؟ قد يكون فهم البرنامج الذي يحتوي على بضع عشرات من الأصناف أسهل في الفهم من البرنامج الذي يحتوي على عدة مئات من الدوال المتباينة. تُقسّم البرمجة كائنية التوجه البرنامج المعقد إلى أجزاء يسهل فهمها. تصميم أصناف في العالم الحقيقي أمر صعب يبدو تصميم الصنف، تمامًا مثل تصميم الاستمارة الورقية paper form، فهو أمرٌ واضحٌ وبسيط. الاستمارات والأصناف، بحكم طبيعتها، هي تبسيطات لكائنات العالم الحقيقي التي تمثلها. السؤال هو كيف نبسط هذه الأشياء؟ على سبيل المثال، إذا كنا بصدد إنشاء صنف Customer فيجب أن يكون للعميل سمة firstName و lastName، أليس كذلك؟ لكن في الواقع، قد يكون إنشاء أصناف لنمذجة كائنات من العالم الحقيقي أمرًا صعبًا؛ ففي معظم البلدان الغربية، يكون الاسم الأخير للشخص هو اسم عائلته، ولكن في الصين، يكون اسم العائلة أولًا. إذا كنا لا نريد استبعاد أكثر من مليار عميل محتمل، فكيف يجب أن نغير صنف Customer لدينا؟ هل يجب تغيير firstName و lastName إلى givenName و familyName؟ لكن بعض الثقافات لا تستخدم أسماء العائلة. على سبيل المثال، الأمين العام السابق للأمم المتحدة يو ثانت U Thant، وهو بورمي، ليس له اسم عائلة: ثانت Thant هو اسمه الأول ويو U هو اختصار لاسم والده. قد نرغب في تسجيل عمر العميل، ولكن سرعان ما ستصبح سمة age قديمة؛ وبدلًا من ذلك، من الأفضل حساب العمر في كل مرة تحتاج إليها باستخدام سمة birthdate. العالم الحقيقي معقد، ومن الصعب تصميم الاستمارات والأصناف لتسجيل هذه الأمور المعقدة في بنية موحدة يمكن لبرامجنا العمل عليها؛ إذ تختلف تنسيقات أرقام الهاتف بين البلدان؛ ولا تنطبق الرموز البريدية على العناوين خارج الولايات المتحدة؛ كما قد يكون تعيين الحد الأقصى لعدد الأحرف لأسماء المدن مشكلةً بالنسبة إلى قرية SchmedeswMorewesterdeich الألمانية. في أستراليا ونيوزيلندا، يمكن أن يكون جنسك المعترف به قانونًا هو X. خلد الماء هو أحد الثدييات التي تبيض. لا ينتمي الفول السوداني للمكسرات. الهوت دوج قد تكون شطيرة أو قد لا تكون، اعتمادًا على من تسأل. بصفتك مبرمجًا يكتب برامج لاستخدامها في العالم الحقيقي، سيتعين عليك تجاوز هذا التعقيد. ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming And Classes من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق البرمجة كائنية التوجه Object-Oriented Programming والأصناف Classes في لغة بايثون تعلم كتابة أكواد بايثون من خلال الأمثلة العملية كيف تكتب أول برنامج لك في بايثون
  12. سنكمل في هذا المقال ما تحدثنا عنه في المقال السابق الجزء الأول عملية بناء خادم ويب متعدد مهام المعالجة، فإذا لم تكن قد قرأت المقال السابق، فاقرأه قبل قراءة هذا المقال. تحويل خادم ويب ذو خيط وحيد إلى خادم متعدد المهام يعالج الآن خادم الويب كل طلب بدوره، يعني أنه لن يعالج اتصال ثاني حتى ينتهي من معالجة الطلب الأول. سيصبح التنفيذ التسلسلي أقل كفاءةً كلما زادت الطلبات على الخادم؛ فإذا استقبل الخادم طلبًا يتطلب وقتًا طويلًا لمعالجته ستنتظر الطلبات التالية وقتًا أطول حتى ينتهي الطلب الطويل حتى لو كانت الطلبات التالية تُنفذ بسرعة. يجب حل هذه المشكلة ولكن أولًا لنلاحظها أثناء العمل. محاكاة طلب بطيء في تنفيذ الخادم الحالي لنلاحظ كيف يؤثر طلب بطيء المعالجة على الطلبات الأخرى المقدمة إلى تنفيذ الخادم الحالي. تنفذ الشيفرة 10 طلب معالجة إلى ‎/sleep بمحاكاة استجابة بطيئة التي تسبب سكون الخادم لخمس ثوانٍ قبل الاستجابة. اسم الملف: src/main.rs use std::{ fs, io::{prelude::*, BufReader}, net::{TcpListener, TcpStream}, thread, time::Duration, }; // --snip-- fn handle_connection(mut stream: TcpStream) { // --snip-- let (status_line, filename) = match &request_line[..] { "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"), "GET /sleep HTTP/1.1" => { thread::sleep(Duration::from_secs(5)); ("HTTP/1.1 200 OK", "hello.html") } _ => ("HTTP/1.1 404 NOT FOUND", "404.html"), }; // --snip-- } [الشيفرة 10: محاكاة استجابة بطيئة عن طريق سكون الخادم لخمس ثوان] بدلنا من if إلى match إذ لدينا ثلاث حالات. يجب أن نطابق صراحةً مع قطعة من request_line لمطابقة النمط مع قيم السلسلة النصية المجردة. لا تُسند match ولا تُحصل تلقائيًا كما تفعل توابع المساواة. تكون الذراع الأولى هي نفس كتلة if من الشيفرة 9، وتطابق الذراع الثانية الطلب إلى ‎/sleep ويسكن الخادم لخمس ثوان عندما يُستقبل الطلب قبل تصيير صفحة HTML الناجحة، والذراع الثالثة هي نفس كتلة else من الشيفرة 9. يمكن ملاحظة أن الخادم بدائي، لكن تعالج المكاتب الحقيقية طلبات متعددة بطريقة مختصرة أكثر. شغل الخادم باستخدام cargo run، ثم افتح نافذتي متصفح واحدة من أجل "/http://127.0.0.1:7878" وأُخرى من أجل "http://127.0.0.1:7878/sleep". إذا أدخلت ‎/ URI عدة مرات كما سابقًا سترى أنه يستجيب بسرعة، لكن إذا أدخلت ‎/sleep ومن ثم حمّلت "/" سترى أن "/" ينتظر حتى يسكن sleep خمس ثوان كاملة قبل أن يُحمّل. هناك تقنيات متعددة لتفادي التراكم خلف طلب بطيء، والطريقة التي سنتبعها هي مجمع خيط thread pool. تحسن الإنتاجية باستخدام مجمع خيط مجمع خيط thread pool هو مجموعة من الخيوط المُنشأة التي تنتظر معالجة مهمة. عندما يستقبل البرنامج مهمةً، يُعيّن واحد من الخيوط في المجمع لأداء المهمة ومعالجتها وتبقى باقي الخيوط في المجمع متاحةً لمعالجة أي مهمة تأتي أثناء معالجة الخيط الأول للمهمة، وعندما ينتهي الخيط من معالجة المهمة يعود إلى مجمع الخيوط الخاملة جاهزًا لمعالجة أي مهمة جديدة. يسمح مجمع خيط معالجة الاتصالات بصورةٍ متزامنة ويزيد إنتاجية الخادم. سنحدد عدد الخيوط في المجمع برقم صغير لحمايتنا من هجوم حجب الخدمة Denial of Service ‎ -أو اختصارًا DoS. إذا طلبنا من البرنامج إنشاء خيط لكل طلب قادم، يمكن لشخص إنشاء 10 مليون طلب مستهلكًا كل الموارد المتاحة وموقفًا معالجة الطلبات نهائيًا. بدلًا من إنشاء عدد لا نهائي من الخيوط، سننشئ عددًا محددًا من الخيوط في المجمع، وستذهب الطلبات المرسلة إلى المجمع للمعالجة. يحافظ المجمع على ترتيب الطلبات القادمة في رتل، كل خيط يأخذ طلبًا من الرتل يعالجه ثم يطلب من الرتل طلبًا آخر. يمكن معالجة N طلب بهذه الطريقة، إذ تمثّل N عدد الخيوط. إذا كان كل خيط يعالج طلبًا طويل التنفيذ يمكن أن تتراكم الطلبات في الرتل ولكننا بذلك نكون قد زدنا عدد الطلبات طويلة التنفيذ التي يمكن معالجتها قبل أن نصل إلى تلك المرحلة. هذه إحدى طرق زيادة إنتاجية خادم ويب، ويمكن استكشاف طرق أُخرى مثل نموذج اشتقاق/جمع fork/join أو نموذج الدخل والخرج للخيط الواحد غير المتزامن single-threaded async I/O أو نموذج الدخل والخرج للخيوط المتعددة غير المتزامن multi-threaded async I/O model. يمكنك قراءة وتنفيذ هذه الحلول إذا كنت مهتمًا بهذا الموضوع وكل هذه الخيارات ممكنة مع لغة برمجية ذات مستوى منخفض مثل رست. قبل البدء بتنفيذ مجمع خيط، لنتحدث كيف يجب أن يكون استخدام المجمع. عند بداية تصميم الشيفرة، تساعدك كتابة واجهة المستخدم في التصميم. اكتب واجهة برمجة التطبيق API للشيفرة بهيكلية تشبه طريقة استدعائها، ثم نفذ الوظيفة داخل الهيكل بدلًا من تنفيذ الوظيفة ثم بناء واجهة برمجة التطبيق العامة. سنستخدم طريقة تطوير مُقادة بالمصرف compiler-driven، هذه طريقة مشابهة لاستخدامنا التطوير المُقاد بالاختبار test-driven كما فعلنا في مشروع سابق في الفصل 12. سنكتب الشيفرة التي تستدعي الدالة المُرادة، ومن ثم ننظر إلى الأخطاء من المصرّف لتحديد ماذا يجب أن نغير حتى تعمل الشيفرة. يجب الحديث بدايةً عن التقنيات التي لن نستعملها. إنشاء خيط لكل طلب لنلاحظ بدايةً كيف ستكون الشيفرة في حال أنشأنا خيطًا لكل طلب. كما ذكرنا سابقًا، لن تكون هذه خطتنا النهائية وذلك لمشكلة إنشاء عدد لا نهائي من الخيوط ولكنها نقطة بداية لإنشاء خادم متعدد الخيوط قادر على العمل، ثم سنضيف مجمع الخيط مثل تحسين وستكون مقارنة الحلين أسهل. تظهر الشيفرة 11 التغييرات لجعل main تُنشئ خيط جديد لمعالجة كل مجرى داخل حلقة for. fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); thread::spawn(|| { handle_connection(stream); }); } } [الشيفرة 11: إنشاء خيط جديد لكل مجرى] كما تعلمنا سابقًا في مقال سابق استخدام الخيوط Threads لتنفيذ شيفرات رست بصورة متزامنة آنيًا، يُنشئ thread::spawn خيطًا جديدًا وينفذ الشيفرة في المُغلف في الخيط الجديد. إذا نفذت الشيفرة وحملت "‎/sleep" في المتصفح ومن ثم "/" في نافذتي متصفح أُخريين ستلاحظ أن الطلبات إلى "/" لا تنتظر "‎/sleep‎" لينتهي ولكن كما ذكرنا سابقًا سيطغى هذا على النظام لأننا ننشئ خيوطًا دون حد. إنشاء عدد محدد من الخيوط نريد من مجمع الخيط أن يعمل بطريقة مشابهة ومألوفة حتى لا يحتاج التبديل من الخيوط لمجمع خيط أي تعديلات كبيرة للشيفرة التي تستخدمها واجهة برمجة التطبيق. تظهر الشيفرة 12 واجهة افتراضية لهيكل ThreadPool الذي نريد استخدامه بدلًا عن thread::spawn. اسم الملف: src/main.rs fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); let pool = ThreadPool::new(4); for stream in listener.incoming() { let stream = stream.unwrap(); pool.execute(|| { handle_connection(stream); }); } } [الشيفرة 12: واجهة ThreadPool المثالية] استخدمنا ThreadPool::new لإنشاء مجمع خيط جديد بعدد خيوط يمكن تعديله وفي حالتنا أربعة. لدى pool.execute واجهة مماثلة للدالة thread:spawn في حلقة for إذ تأخذ مغلفًا يجب أن ينفذه المجمع لكل مجرى. نحتاج لتنفيذ pool.execute أن تأخذ مغلفًا وتعطيه لخيط في المجمع لينفذه. لن تُصرّف هذه الشيفرة ولكن سنجربها كي يدلنا المصرف عن كيفية إصلاحها. إنشاء مجمع خيط باستخدام التطوير المقاد بالمصرف أجرِ التغييرات في الشيفرة 12 على الملف src/main.rs واستخدم أخطاء المصرّف من cargo check لقيادة التطوير. هذه أول خطأ نحصل عليه: $ cargo check Checking hello v0.1.0 (file:///projects/hello) error[E0433]: failed to resolve: use of undeclared type `ThreadPool` --> src/main.rs:11:16 | 11 | let pool = ThreadPool::new(4); | ^^^^^^^^^^ use of undeclared type `ThreadPool` For more information about this error, try `rustc --explain E0433`. error: could not compile `hello` due to previous error عظيم، يبين هذ الخطأ أننا بحاجة إلى نوع أو وحدة ThreadPool. سيكون تنفيذ Threadpool الخاص بنا مستقل عن عمل خادم الويب، لذا لنبدّل الوحدة المصرفة hello من وحدة ثنائية مصرفة إلى وحدة مكتبة مصرفة لاحتواء تنفيذ Threadpool. يمكننا بعد ذلك استخدام مكتبة مجمع الخيط المنفصلة لفعل أي عمل نريده باستخدام مجمع خيط وليس فقط لطلبات خادم الويب. أنشئ src/lib.rs الذي يحتوي التالي، وهو أبسط تعريف لهيكل ThreadPool يمكن الحصول عليه. اسم الملف: src/lib.rs pub struct ThreadPool; ثم عدل ملف main.rs لجلب ThreadPool من المكتبة إلى النطاق بإضافة الشيفرة التالية في مقدمة الملف src/main.rs. اسم الملف: src/main.rs use hello::ThreadPool; لن تعمل هذه الشيفرة ولكن لننظر مجددًا إلى الخطأ التالي الذي نريد معالجته: $ cargo check Checking hello v0.1.0 (file:///projects/hello) error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in the current scope --> src/main.rs:12:28 | 12 | let pool = ThreadPool::new(4); | ^^^ function or associated item not found in `ThreadPool` For more information about this error, try `rustc --explain E0599`. error: could not compile `hello` due to previous error يشير هذا الخطأ أننا نحتاج إلى انتاج دالة مرتبطة اسمها new من أجل ThreadPool. يمكننا معرفة أن new تحتاج معامل يقبل 4 مثل وسيط ويجب أن يعيد نسخة ThreadPool. لننفذ أبسط دالة new التي تحتوي هذه الصفات characteristics. اسم الملف: src/lib.rs pub struct ThreadPool; impl ThreadPool { pub fn new(size: usize) -> ThreadPool { ThreadPool } } اخترنا نوع usize للمعامل size لأننا نعرف أن العدد السالب للطلبات غير منطقي ونعرف أيضًا أننا سنستخدم 4 ليمثّل عدد العناصر في مجموعة الخيوط وهذا هو عمل نوع usize كما تحدثنا سابقًا في قسم "أنواع الأعداد الصحيحة" في المقال أنواع البيانات Data Types في لغة رست. لنتحقق من الشيفرة مجددًا: $ cargo check Checking hello v0.1.0 (file:///projects/hello) error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope --> src/main.rs:17:14 | 17 | pool.execute(|| { | ^^^^^^^ method not found in `ThreadPool` For more information about this error, try `rustc --explain E0599`. error: could not compile `hello` due to previous error يحدث الخطأ الآن لأنه ليس لدينا تابع execute على ThreadPool. تذكر من قسم "إنشاء عدد محدد من الخيوط" أننا قررنا أن مجمع الخيط يجب أن يكون له واجهة تشبه thread::spawn، وقررنا أيضّأ أننا سننفذ التابع execute ليأخذ المغلف المعُطى له ويعطيه لخيط خامل في المجمع لينفذه. سنعرّف تابع execute على ThreadPool ليأخذ المغلف مثل معامل. تذكر من القسم "نقل القيم خارج المغلف وسمات Fn" في المقال المغلفات closures في لغة رست أننا بإمكاننا أخذ المغلفات مثل معاملات باستخدام ثلاث سمات هي Fn أو FnMut أو FnOnce. يجب أن نحدد أي نوع مغلف نريد استخدامه هنا، نحن نعرف أننا سنفعل شيئًا يشابه تنفيذ المكتبة القياسية لدالةthread::spawn، لذلك دعنا نرى ما هي القيود الموجودة لبصمة thread::spawn على معاملاتها. تظهر التوثيقات التالي: pub fn spawn<F, T>(f: F) -> JoinHandle<T> where F: FnOnce() -> T, F: Send + 'static, T: Send + 'static, المعامل F هو الذي يهمنا، والمعامل T متعلق بالقيمة المُعادة ولسنا مهتمين بها. يمكننا أن نرى أن spawn تستخدم FnOnce مثل قيد سمة على F، وهذا ما نريده أيضًأ لأننا نريد تمرير الوسيط الذي نأخذه في execute إلى spawn. يمكننا التأكد أيضًا أن FnOnce هي السمة المُراد استخدامها لأن خيط تنفيذ الطلب سينفِّذ فقط طلب المغلف مرةً واحدة، والذي يطابق Once في FnOnce. لدى معامل نوع F أيضًا قيد سمة Send وقيد دورة حياة static' المفيدان في حالتنا؛ فنحن بحاجة Send لنقل المغلف من خيط لآخر، و static' لأننا لا نعرف الوقت اللازم ليُنفذ الخيط. لننشئ تابع execute على ThreadPool التي تأخذ معامل معمم للنوع F مع هذه القيود. اسم الملف: src/lib.rs impl ThreadPool { // --snip-- pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { } } استخدمنا () بعد FnOnce لأن FnOnce تمثل مغلفًا لا يأخذ معاملات ويعيد نوع الوحدة (). يمكن إهمال النوع المُعاد من البصمة كما في تعريفات الدالة، ولكن حتى لو لم يوجد أي معاملات نحن بحاجة الأقواس. هذا هو أبسط تنفيذ لدالة execute، فهي لا تعمل شيئًا، لكننا فقط بحاجة أن تُصرّف شيفرتنا، لنتحقق منها مجددًا. $ cargo check Checking hello v0.1.0 (file:///projects/hello) Finished dev [unoptimized + debuginfo] target(s) in 0.24s إنها تُصرّف، لكن لاحظ إذا جربت cargo run وأجريت طلبًا في المتصفح، سترى الأخطاء في المتصفح نفسها التي رأيناها في بداية الفصل. لم تستدع المكتبة المغلف المُمرر إلى execute حتى الآن. ملاحظة: هناك مقولة عن لغات البرمجة ذات المُصرّفات الحازمة مثل هاسكل Haskell ورست وهي "إذا صُرفت الشيفرة فإنها تعمل" ولكن هذه المقولة ليست صحيحة إجمالًا، إذ يُصرّف مشروعنا، لكنه لا يعمل شيئًا إطلاقًا. إذا كنا نريد إنشاء مشروع حقيقي ومكتمل، الآن هو الوقت المثالي لكتابة وحدات اختبار للتحقق من أن الشيفرة تُصرّف ولها السلوك المرغوب. التحقق من صحة عدد الخيوط في new لن نغيّر شيئًا للمعاملين new و parameter. لننفذ متن الدوال بالسلوك الذي نريده، ولنبدأ بالدالة new. اخترنا سابقًا نوع غير مؤشر للمعامل size لأن مجمع بعدد خيوط سلبي هو غير منطقي، ولكن مجمع بعدد خيوط صفر ليس منطقيًا أيضًا ولكن unsize صالح. سنضيف الشيفرة التي تتحقق من أن size أكبر من الصفر قبل إعادة نسخة من ThreadPool وجعل البرنامج يهلع إذا حصل على قيمة صفر باستخدام ماكرو assert!‎ كما في الشيفرة 13. اسم الملف: src/lib.rs impl ThreadPool { /// Create a new ThreadPool. /// /// The size is the number of threads in the pool. /// /// # Panics /// /// The `new` function will panic if the size is zero. pub fn new(size: usize) -> ThreadPool { assert!(size > 0); ThreadPool } // --snip-- } [الشيفرة 13: تنفيذ Threadpool:new ليهلع إذا كان size صفر] أضفنا بعض التوثيق إلى ThreadPool باستخدام تعليقات doc. لاحظ أننا اتبعنا خطوات التوثيق الجيدة بإضافة قسم يستدعي الحالات التي يمكن للدالة أن تهلع فيها كما تحدثنا في الفصل 14. جرب تنفيذ cargo run --open واضغط على هيكل ThreadPool لرؤية كيف تبدو المستندات المُنشأة للدالة new. يمكننا تغيير new إلى build بدلًا من إضافة ماكرو assert!‎، ونعيد Result كما فعلنا في Config::build في مشروع الدخل والخرج في الشيفرة 9 في المقال كتابة برنامج سطر أوامر بلغة رست: إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء، لكننا قررنا في حالتنا أن إنشاء مجمع خيط بدون أي خيوط هو خطأ لا يمكن استرداده. إذا كنت طموحًا جرب كتابة دالة اسمها build مع البصمة التالية لمقارنته مع الدالة new. pub fn build(size: usize) -> Result<ThreadPool, PoolCreationError> { إنشاء مساحة لتخزين الخيوط لدينا الآن طريقة لمعرفة أنه لدينا عدد صالح من الخيوط لتخزينها في المجمع، إذ يمكننا إنشاء هذه الخيوط وتخزينها في هيكل ThreadPool قبل إرجاعها إلى الهيكل، ولكن كيف نخزن الخيوط؟ لنلاحظ بصمة thread::spawn. pub fn spawn<F, T>(f: F) -> JoinHandle<T> where F: FnOnce() -> T, F: Send + 'static, T: Send + 'static, يُعاد JoinHandle<T>‎ من الدالة spawn، إذ تمثّل T النوع الذي يعيده المغلف. لنستعمل JoinHandle أيضًا لنرى ما سيحدث، إذ سيعالج المغلف الذي نمرره إلى مجمع الخيط الاتصال ولا يعيد أي شيء لذا T ستكون نوع وحدة (). ستُصرّف الشيفرة في الشيفرة 14 ولكن لا تُنشئ أي خيوط. غيّرنا تعريف ThreadPool لتحتوي شعاعًا من نسخة thread::JoinHandle<()>‎ وهيأنا الشعاع بسعة size وضبطنا حلقة for التي تعيد بعض الشيفرة لإنشاء الخيوط وتعيد نسخة ThreadPool تحتويهم. اسم الملف: src/main.rs use std::thread; pub struct ThreadPool { threads: Vec<thread::JoinHandle<()>>, } impl ThreadPool { // --snip-- pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let mut threads = Vec::with_capacity(size); for _ in 0..size { // create some threads and store them in the vector } ThreadPool { threads } } // --snip-- } [الشيفرة 14: إنشاء شعاع للهيكل ThreadPool الذي يحتوي الخيوط] جلبنا std::thread إلى النطاق في وحدة المكتبة المصرفة لأننا نستخدم Thread::JoinHandle بمثابة نوع العنصر في الشعاع في ThreadPool. تُنشئ ThreadPool شعاعًا جديدًا يحتوي عناصر size عندما يُستقبل حجم صالح. تعمل الدالة with_capacity نفس مهام Vec::new ولكن بفرق مهم هو أنها تحجز مسبقًا المساحة في الشعاع لأننا نريد تخزين عناصر size في الشعاع. إجراء هذا الحجز مسبقًا هو أكثر كفاءة من استخدام Vec::new الذي يغير حجمه كلما اُضيفت عناصر. عندما تنفذ cargo check مجدّدًا ينبغي أن تنجح. هيكل عامل Worker Struct مسؤول عن ارسال شيفرة من مجمع الخيط إلى خيط تركنا تعليقًا في حلقة for متعلق بإنشاء الخيوط في الشيفرة 14. سننظر هنا إلى كيفية إنشاء الخيوط حقيقةً، إذ تؤمن المكتبة القياسية thread::spawn بمثابة طريقة لإنشاء الخيوط ويتوقع thread::spawn الحصول على بعض الشيفرة لكي ينفذها الخيط بعد إنشائه فورًا، ولكن نريد في حالتنا إنشاء خيوط وجعلهم ينتظرون شيفرةً سنرسلها لاحقًا. لا يقدم تنفيذ المكتبة القياسية للخيوط أي طريقة لعمل ذلك، إذ يجب تنفيذها يدويًا. سننفذ هذا السلوك عن طريق إضافة هيكلية بيانات جديدة بين ThreadPool والخيوط التي ستدير هذه السلوك الجديد، وسندعو هيكل البيانات هذا "العامل Worker" وهو مصطلح عام في تنفيذات مجمّع الخيوط. يأخذ العامل الشيفرة التي بحاجة لتنفيذ وينفّذها في خيط العامل. فكر كيف يعمل الناس في مطبخ المطعم، إذ ينتظر العاملون طلبات الزبائن ويكونوا مسؤولين عن أخذ هذه الطلبات وتنفيذها. بدلًا من تخزين شعاع نسخة JoinHandle<()>‎ في مجمع الخيط، نخزن نسخًا من هيكل Worker. يخزن كل Worker نسخة JoinHandle<()>‎ واحدة، ثم ننفذ تابع على Worker الذي يأخذ مغلف شيفرة لينفذه ويرسله إلى خيط يعمل حاليًا لينفذه. سنعطي كل عامل رقمًا معرّفًا id للتمييز بين العمال المختلفين في المجمع عند التسجيل أو تنقيح الأخطاء. هكذا ستكون العملية الجديدة عند إنشاء ThreadPool. سننفذ الشيفرة التي ترسل المغلف إلى الخيط بعد إعداد Worker بهذه الطريقة: عرّف هيكل Worker الذي يحتوي id و JoinHandle<()>‎. عدّل ThreadPool لتحتوي شعاع من نسخ Worker. عرّف دالة Worker::new التي تأخذ رقم id وتعيد نسخة Worker التي تحتوي id وخيط مُنشأ بمغلف فارغ. استخدم عداد حلقة for لإنشاء id وإنشاء Worker جديد مع ذلك الرقم id وخرن العامل في الشعاع. إذا كنت جاهزًا للتحدي، جرّب تنفيذ هذه التغييرات بنفسك قبل النظر إلى الشيفرة في الشيفرة 15. جاهز؟ يوجد في الشيفرة 15 إحدى طرق عمل التعديلات السابقة. اسم الملف:src/lib.rs use std::thread; pub struct ThreadPool { workers: Vec<Worker>, } impl ThreadPool { // --snip-- pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id)); } ThreadPool { workers } } // --snip-- } struct Worker { id: usize, thread: thread::JoinHandle<()>, } impl Worker { fn new(id: usize) -> Worker { let thread = thread::spawn(|| {}); Worker { id, thread } } } [الشيفرة 15: تعديل ThreadPool بحيث تحتوي نسخة Worker بدلًا من احتواء الخيط مباشرةً] عدّلنا اسم حقل ThreadPool من threads إلى workers لأنه يحتوي نسخ Worker بدلًا من نسخ JoinHandle<()>‎. استخدمنا العداد في حلقة for مثل وسيط لدالة Worker::new وخزّنا كل Worker جديد في شعاع اسمه workers. لا تحتاج الشيفرة الخارجية (كما في الخادم في src/main.rs) أن تعرف تفاصيل التنفيذ بما يتعلق باستخدام هيكل Worker داخل ThreadPool، لذا نجعل كل من هيكل Worker ودالة new خاصين private. تستخدم الدالة Worker::new المعرّف id المُعطى وتخزن نسخة JoinHandle<()>‎ المُنشأة عن طريق إنشاء خيط جديد باستخدام مغلف فارغ. ملاحظة: سيهلع thread::spawn إذا كان نظام التشغيل لا يستطيع إنشاء خيط بسبب عدم توفر موارد كافية، وسيؤدي هذا إلى هلع كامل الخادم حتى لو كان إنشاء بعض الخيوط ممكنًا. للتبسيط يمكن قبول هذا السلوك ولكن في تنفيذ مجمع خيط مُنتج ينبغي استخدام std::thread::Builder ودالة spawn الخاصة به التي تعيد Result. تُصرّف هذه الشيفرة وتخزن عددًا من نسخ Worker الذي حددناه مثل وسيط إلى ThreadPool::new. لكننا لم نعالج المغلف الذي نحصل عليه في execute. لنتعرف على كيفية عمل ذلك تاليًا. إرسال طلبات إلى الخيوط عن طريق القنوات المشكلة التالية التي سنتعامل معها هي أن المغلفات المُعطاة إلى thread::spawn لا تفعل شيئًا إطلاقًا، وسنحصل حاليًا على المغلف الذي نريد تنفيذه في تابع execute، لكن نحن بحاجة لإعطاء thread::spawn مغلفًا لينفذه عندما ننشئ كل Worker خلال إنشاء ThreadPool. نريد تشغيل هياكل Worker التي أنشأناها للبحث عن شيفرة من الرتل في ThreadPool وأن ترسل تلك الشيفرة إلى خيطها لينفّذها. ستكون القنوات التي تعلمناها سابقًا في المقال استخدام ميزة تمرير الرسائل Message Passing لنقل البيانات بين الخيوط Threads في لغة رست -والتي تُعد طريقة بسيطة للتواصل بين خيطين- طريقةً ممتازةً لحالتنا، إذ سنستعمل قناةً لتعمل مثل رتل للوظائف، وترسل execute وظيفة من ThreadPool إلى نسخة Worker التي ترسل بدورها الوظيفة إلى خيطها. ستكون الخطة على النحو التالي: يُنشئ ThreadPool قناة ويحتفظ بالمرسل. يحتفظ كل Worker بالمستقبل. ننشئ هيكل Job جديد يحتفظ بالمغلف الذي نريد إرساله عبر القناة. يرسل تابع execute الوظيفة المراد تنفيذها عبر المرسل. سيتكرر مرور Worker على المستقبل وينفذ المغلف لأي وظيفة يستقبلها في الخيط. لنحاول إنشاء قناة في ThreadPool::new والاحتفاظ بالمرسل في نسخة ThreadPool كما في الشيفرة 16. لا يحتوي هيكل Job أي شيء الآن، لكنه سيكون نوع العنصر المُرسل عبر القناة. اسم الملف: src/lib.rs use std::{sync::mpsc, thread}; pub struct ThreadPool { workers: Vec<Worker>, sender: mpsc::Sender<Job>, } struct Job; impl ThreadPool { // --snip-- pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let (sender, receiver) = mpsc::channel(); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id)); } ThreadPool { workers, sender } } // --snip-- } [الشيفرة 16: تعديل ThreadPool لتخزين مرسل القناة التي ترسل نسخ Job] أنشأنا القناة الجديدة في ThreadPool:new وجعلنا المجمع يحتفظ بالمرسل. ستصرّف هذه الشيفرة بنجاح. لنجرب تمرير مستقبل القناة إلى كل عامل عندما ينشئ مجمع الخيط القناة. نعرف أننا نريد استخدام المستقبل في الخيط الذي أنشأه العامل، لذا سنشير إلى معامل receiver في المغلف بمرجع reference. لن تُصرّف الشيفرة 17. اسم الملف: src/lib.rs impl ThreadPool { // --snip-- pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let (sender, receiver) = mpsc::channel(); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id, receiver)); } ThreadPool { workers, sender } } // --snip-- } // --snip-- impl Worker { fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker { let thread = thread::spawn(|| { receiver; }); Worker { id, thread } } } [الشيفرة 17: تمرير المستقبل إلى العمال workers] أجرينا بعض التغييرات الصغيرة والمباشرة، إذ مررنا المستقبل إلى Worker::new واستخدمناه داخل المغلف. عندما نتحقق من الشيفرة سنحصل على هذا الخطأ: $ cargo check Checking hello v0.1.0 (file:///projects/hello) error[E0382]: use of moved value: `receiver` --> src/lib.rs:26:42 | 21 | let (sender, receiver) = mpsc::channel(); | -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait ... 26 | workers.push(Worker::new(id, receiver)); | ^^^^^^^^ value moved here, in previous iteration of loop For more information about this error, try `rustc --explain E0382`. error: could not compile `hello` due to previous error تحاول الشيفرة تمرير receiver لنسخ متعددة من Worker ولكن هذا لن يعمل كما تتذكر سابقًا من المقال استخدام ميزة تمرير الرسائل Message Passing لنقل البيانات بين الخيوط Threads في لغة رست، إذ أن تنفيذ القناة المقدم من رست هو مُنتجين producer متعددين ومستهلك consumer واحد، وهذا يعني أنه لا يمكن نسخ الطرف المستهلك من القناة لإصلاح هذا الخطأ ولا نريد أيضًا إرسال رسائل متعددة لمستهلكين متعددين، بل نحتاج قائمة رسائل واحدة مع عمال متعددين لكي تعالج كل رسالة مرةً واحدةً فقط. إضافةً إلى ذلك، يتطلب أخذ وظيفة من رتل القناة تغيير receiver، لذا تحتاج الخيوط طريقةً آمنةً لتشارك وتعدل receiver، وإلا نحصل على حالات سباق (كما تحدثنا في الفصل السابق المشار إليه). تذكر المؤشرات الذكية الآمنة للخيوط التي تحدثنا عنها سابقًا في المقال تزامن الحالة المشتركة Shared-State Concurrency في لغة رست وتوسيع التزامن مع Send و Sync؛ فنحن بحاجة لاستخدام Arc<Mutex<T>>‎ لمشاركة الملكية لعدد من الخيوط والسماح للخيوط بتغيير القيمة. يسمح نوع Arc لعدد من العمال من مُلك المستقبل وتضمن Mutex حصول عامل واحد على الوظيفة من المستقبل. تظهر الشيفرة 18 التغييرات التي يجب عملها. اسم الملف: src/lib.rs use std::{ sync::{mpsc, Arc, Mutex}, thread, }; // --snip-- impl ThreadPool { // --snip-- pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let (sender, receiver) = mpsc::channel(); let receiver = Arc::new(Mutex::new(receiver)); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id, Arc::clone(&receiver))); } ThreadPool { workers, sender } } // --snip-- } // --snip-- impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { // --snip-- } } [الشيفرة 18: مشاركة المستقبل بين العمال باستخدام Arc و Mutex] نضع المستقبل فيThreadPool::new في Arc و Mutex، وننسخ Arc لكل عامل لتزيد عدّ المرجع ليستطيع العمال مشاركة ملكية المستقبل. تُصرّف الشيفرة بنجاح مع هذه التغييرات، لقد اقتربنا من تحقيق هدفنا. تنفيذ تابع التنفيذ execute لننفذ أخيرًا تابع execute على ThreadPool، إذ سغيّر أيضًا Job من هيكل إلى نوع اسم بديل لكائن السمة الذي يحتوي نوع المغلف الذي يستقبله execute. كما تحدثنا في قسم "إنشاء مرادفات للنوع بواسطة أسماء النوع البديلة" في المقال الأنواع والدوال المتقدمة في لغة رست، يسمح لنا نوع الاسم البديل بتقصير الأنواع الطويلة لسهولة الاستخدام كما في الشفرة 19. اسم الملف: src/lib.rs // --snip-- type Job = Box<dyn FnOnce() + Send + 'static>; impl ThreadPool { // --snip-- pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { let job = Box::new(f); self.sender.send(job).unwrap(); } } // --snip-- [الشيفرة 19: إنشاء نوع اسم بديل Job لـ Box يحتوي كل مغلف وارسال العمل عبر القناة] بعد إنشاء نسخة Job جديدة باستخدام المغلف نحصل على execute ونرسل الوظيفة عبر الطرف المرسل للقناة. نستدعي unwrap على send في حال فشل الإرسال؛ إذ يمكن حصول ذلك إذا أوقفنا كل الخيوط من التنفيذ، وهذا يعني توقُف الطرف المستقبل عن استقبال أي رسائل جديدة. لا يمكننا الآن إيقاف الخيوط من التنفيذ، إذ تستمر خيوطنا بالتنفيذ طالما المجمع موجود. سبب استخدام unwrap هو أننا نعرف أن حالة الفشل هذه لن تحصل ولكن المصرّف لا يعرف ذلك. لم ننتهي كليًا بعد، فالمغلف المُمرر إلى thread::spawn يسند الطرف المستقبل من القناة فقط، لكن نريد بدلًا من ذلك أن يتكرر المغلف للأبد ويسأل الطرف المستقبل من القناة عن وظيفة وينفذ الوظيفة عندما يحصل عليها. دعنا نجري التغييرات الموضحة في الشيفرة 20 للدالة Worker::new. اسم الملف: src/lib.rs // --snip-- impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { let thread = thread::spawn(move || loop { let job = receiver.lock().unwrap().recv().unwrap(); println!("Worker {id} got a job; executing."); job(); }); Worker { id, thread } } } [الشيفرة 20: استقبال وتنفيذ الوظائف في خيط العامل] نستدعي أولًا lock على receiver للحصول على mutex، ونستدعي unwrap ليهلع على أي خطأ. قد يفشل الحصول على قفل إذا كان mutex في حالة مسمومة poisoned، والتي تحصل إذا هلع أحد الخيوط عند احتفاظه بالقفل بدلًا من ترك القفل، وسيكون استدعاء unwrap في هذه الحالة هو العمل الأفضل. غيّر unwrap إلى expect على راحتك لتظهر رسالة خطأ ذات معنى. إذا حصلنا على القفل على mutex، نستدعي recv لاستقبال Job من القناة. يتخطى استدعاء unwrap الأخير أي أخطاء أيضًا والتي ربما قد تحصل إذا اُغلق، على نحوٍ مشابه لكيفية إعادة Err من قِبل تابع send إذا أُغلق المستقبل. إذا لم توجد أي وظيفة في استدعاء كتل recv، سينتظر الخيط حتى تتوفر وظيفة. يضمن Mutex<T>‎ أن يكون هناك خيط Worker واحد يطلب وظيفة. يعمل مجمع الخيط الآن، جرب cargo run وأرسل بعض الطلبات. $ cargo run Compiling hello v0.1.0 (file:///projects/hello) warning: field is never read: `workers` --> src/lib.rs:7:5 | 7 | workers: Vec<Worker>, | ^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(dead_code)]` on by default warning: field is never read: `id` --> src/lib.rs:48:5 | 48 | id: usize, | ^^^^^^^^^ warning: field is never read: `thread` --> src/lib.rs:49:5 | 49 | thread: thread::JoinHandle<()>, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: `hello` (lib) generated 3 warnings Finished dev [unoptimized + debuginfo] target(s) in 1.40s Running `target/debug/hello` Worker 0 got a job; executing. Worker 2 got a job; executing. Worker 1 got a job; executing. Worker 3 got a job; executing. Worker 0 got a job; executing. Worker 2 got a job; executing. Worker 1 got a job; executing. Worker 3 got a job; executing. Worker 0 got a job; executing. Worker 2 got a job; executing. لقد نجحنا، ولدينا الآن مجمع خيط ينفذ الاتصالات على نحوٍ غير متزامن. لا يُنشئ أكثر من أربعة خيوط حتى لا يُحمَل النظام بصورةٍ زائدة إذا استقبل الخادم طلبات كثيرة. إذا أرسلنا طلبًا إلى ‎/‎sleep سيكون الخادم قادرًا على خدمة طلبات أُخرى بجعل خيط آخر ينفذهم. ملاحظة: إذا فتحنا ‎/sleep في نوافذ متعددة في المتصفح بنفس الوقت، ستُحمل واحدةٌ تلو الأُخرى بفواصل زمنية مدتها 5 ثواني لأن بعض المتصفحات تنفذ النسخ المتعددة لنفس الطلب بالترتيب لأسباب التخزين المؤقت. ليس الخادم هو سبب هذا التقصير. بعد أن تعلمنا عن حلقة while let في المقال الأنماط Patterns واستخداماتها وقابليتها للدحض Refutability في لغة رست، ربما تتساءل لماذا لم نكتب شيفرة الخيط العامل كما في الشيفرة 21. اسم الملف: src/lib.rs // --snip-- impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { let thread = thread::spawn(move || { while let Ok(job) = receiver.lock().unwrap().recv() { println!("Worker {id} got a job; executing."); job(); } }); Worker { id, thread } } } [الشيفرة 21: طريقة تنفيذ مختلفة لدالة Worker::new باستخدام while let] تُصرّف الشيفرة وتُنفذ ولكن لا تعطي نتيجة عمل الخيوط المرغوبة، إذ يسبب الطلب البطيء انتظار باقي الطلبات لتُعالج، والسبب بسيطٌ إلى حد ما؛ فليس لدى هيكل Mutex دالة unlock عامة لأن ملكية القفل مبينةٌ على دورة حياة MutexGuard<T>‎ داخل LockResult<MutexGuard<T>>‎ التي يعيدها التابع lock. يطبق متحقق الاستعارة قاعدة أن المورد المحمي بهيكل Mutex لا يمكن الوصول له إلا إذا احتفظنا بالقفل وقت التصريف، ولكن بهذا التنفيذ يمكن أن يبقى القفل مُحتفظًا به أكثر من اللازم إذا لم نكن منتبهين إلى دورة حياة MutexGuard<T>‎. تعمل الشيفرة في الشيفرة 20 التي تستخدم let job = receiver.lock().unwrap().recv().unwrap();‎ إذ تُسقط أي قيمة مؤقتة مُستخدمة في التعبير على الطرف اليمين من إشارة المساواة "=" مع letعندما تنتهي تعليمة let، ولكن لا تُسقط while let (وأيضًا if let و match) القيم المؤقتة حتى نهاية الكتلة المرتبطة بها. يبقى القفل مُحتفظًا به حتى نهاية فترة استدعاء job()‎ يعني أن العمال الباقين لا يمكن أن يستقبلوا وظائف. ترجمة -وبتصرف- لقسم من الفصل Final Project: Building a Multithreaded Web Server من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: بناء خادم ويب متعدد مهام المعالجة بلغة رست - الجزء الأول مدخل إلى برمجة مواقع الويب من طرف الخادم أفضل 5 خوادم ويب مفتوحة المصدر
  13. البرمجة كائنية التوجه - أو اختصارًا OOP- هي ميزة للغة البرمجة تسمح لك بجمع الدوال functions والمتغيرات variables معًا في أنواع بيانات data type جديدة، تسمى الأصناف classes، والتي يمكنك من خلالها إنشاء كائنات objects. يمكنك تقسيم البرنامج المترابط إلى أجزاء أصغر يسهل فهمها وتنقيح أخطائها عن طريق تنظيم الشيفرة البرمجية الخاصة بك إلى أصناف. لا تضيف البرمجة كائنية التوجه تنظيمًا بالنسبة للبرامج الصغيرة، بل تقّدم تعقيدًا لا داعٍ له في بعض الحالات، وعلى الرغم من أن بعض اللغات، مثل جافا، تتطلب منك تنظيم كل الشيفرات البرمجية في أصناف، إلا أن ميزات البرمجة كائنية التوجه في بايثون اختيارية، إذ يمكن للمبرمجين الاستفادة من الأصناف إذا كانوا بحاجة إليها أو تجاهلها. يشير حديث مُبرمج بايثون Jack Diederich في PyCon 2012، "توقف عن كتابة الأصناف"، إلى العديد من الحالات التي يستخدم فيها المبرمجون الأصناف عندما تفي دالة بسيطة بالغرض، ولكن بصفتك مبرمجًا، يجب أن تكون على دراية بأساسيات الأصناف وكيف تعمل، لذلك سنتعرف في هذا المقال على ماهية الأصناف، ولماذا تُستخدَم في البرامج، ومفاهيم البرمجة القائمة عليها. البرمجة كائنية التوجه موضوع واسع، وهذا المقال مجرّد مقدمة. تشبيه من العالم الحقيقي: تعبئة استمارة لعلّك ملأت الاستمارات الورقية والإلكترونية عدة مرات في حياتك؛ منها لزيارات الطبيب أو لعمليات الشراء عبر الإنترنت أو للرد على دعوة لحضور حفل زفاف. تُستخدم الاستمارة بمثابة طريقة رسمية من قبل شخص آخر أو منظمة أخرى لجمع المعلومات التي يحتاجونها عنك. تسأل الاستمارات المختلفة أنواعًا متنوعة من الأسئلة، فمثلًا يجب عليك شرح حالتك الطبية الحساسة في استمارة الطبيب، بينما عليك الإبلاغ عن أي ضيوف تحضرهم معك إلى حفل الزفاف في استمارة الحفل. في بايثون، يكون للصنف والنوع ونوع البيانات المعنى ذاته، ومثال عن ذلك النموذج الورقي أو الإلكتروني؛ فالصنف هو مخطط لكائنات بايثون (وتسمى أيضًا النُسَخ instances)، والتي تحتوي على البيانات الممثّلة لاسم، ويمكن أن يكون هذا الاسم هو مريض الطبيب، أو عملية شراء إلكترونية، أو ضيف حفل زفاف. تشبه الأصناف قالب استمارة فارغة، وتشبه الكائنات التي تُنشأ من ذلك الصنف الاستمارة المملوءة التي تحتوي على بيانات فعلية حول نوع الشيء الذي يمثله النموذج. على سبيل المثال، تُشبه استمارة استجابة دعوة حفل الزفاف RSVP الصنف، في حين تشبه دعوة حفل الزفاف RSVP المملوء الكائن في الشكل 1. [الشكل 1: تُشبه قوالب استمارة دعوة الزفاف الأصناف، في حين تُشبه الاستمارات المعبأة الكائنات] يمكنك أيضًا النظر إلى الأصناف والكائنات على أنها جداول بيانات، كما في الشكل 2. [الشكل 2: جدول بيانات لجميع بيانات دعوات حفل الزفاف] ستشكل ترويسات الأعمدة الأصناف، وتشكل الصفوف rows الفردية كائنًا. يأتي غالبًا ذكر الأصناف والكائنات على أنها نماذج بيانات للعناصر في العالم الحقيقي، ولكن لا تخلط بين الخريطة map والمنطقة؛ فما تحتويه الأصناف يعتمد على ما يحتاج البرنامج لفعله. يوضح الشكل 3 بعض الكائنات من أصناف مختلفة تمثل جميعها شخصًا، وتخزن معلومات مختلفة تمامًا باختلاف اسم الشخص. [الشكل 3: أربعة كائنات مصنوعة من أصناف مختلفة تمثل شخصًا، اعتمادًا على ما يحتاج التطبيق إلى معرفته عن الشخص] يجب أيضًا أن تعتمد المعلومات الموجودة في أصنافك على احتياجات برنامجك، إذ تستخدم العديد من برامج البرمجة كائنية التوجه التعليمية صنف Car مثالًا أساسيًا دون الإشارة إلى أن ما يوجد في الصنف يعتمد كليًا على نوع البرنامج الذي تكتبه. لا يوجد هناك ما يدعى صنف Car العام الذي من الواضح أنه يحتوي على تابع honkHorn()‎‎‎‎ أو سمة numberOfCupholders لمجرد أنها خصائص تمتلكها سيارات العالم الحقيقي؛ فقد يكون برنامجك لتطبيق ويب لبيع السيارات أو للعبة فيديو لسباق السيارات أو لمحاكاة حركة المرور على الطرق؛ وقد يكون لصنف السيارات الخاصة بتطبيق الويب لبيع السيارات على الويب سمات milesPerGallon أو manufacturersSuggestedRetailPrice (تمامًا كما قد تستخدم جداول بيانات وكالة السيارات هذه كعمود)، لكن لن تحتوي لعبة الفيديو ومحاكاة حركة المرور على الطرق على هذه الأصناف، لأن هذه المعلومات ليست ذات صلة بهما. قد يحتوي صنف السيارات الخاصة بلعبة الفيديو على explodeWithLargeFireball()‎‎، ولكن لن يحتوي تطبيق المحاكاة أو بيع السيارات على الصنف ذاته. إنشاء كائنات من الأصناف سبق لك استخدام الأصناف والكائنات في بايثون، حتى لو لم تُنشئ الأصناف بنفسك. تذكّر وحدة datetime، التي تحتوي على صنف باسم date، إذ تُمثل كائنات صنف datetime.date (تسمى أيضًا ببساطة كائنات اdatetime.date أو كائنات date) تاريخًا محددًا. أدخل ما يلي في الصدفة التفاعلية Interactive Shell لإنشاء كائن من صنف datetime.date: >>> import datetime >>> birthday = datetime.date(1999, 10, 31) # مرّر قيمة السنة والشهر واليوم >>> birthday.year 1999 >>> birthday.month 10 >>> birthday.day 31 >>> birthday.weekday()‎‎ # يمثّل‫ weekday() تابعًا؛ لاحظ القوسين 6 السمات Attributes -أو يطلق عليها أحيانًا الخاصيات- هي متغيرات مرتبطة بالكائنات. يؤدي استدعاء datetime.date()‎‎ إلى إنشاء كائن date جديد، جرت تهيئته باستخدام الوسطاء 1999, 10, 31، بحيث يمثل الكائن تاريخ 31 أكتوبر 1999. نعيّن هذه الوسطاء على أنها سمات للصنف date، وهي year و month و day، التي تحتوي على جميع كائنات date. يمكن -باستخدام هذه المعلومات- لتابع الصنف weekday()‎‎ حساب يوم الأسبوع. في هذا المثال، تُعاد القيمة 6 ليوم الأحد، لأنه وفقًا لتوثيق بايثون عبر الإنترنت، القيمة المُعادة من weekday()‎‎ هي عدد صحيح يبدأ من 0 ليوم الاثنين وينتهي بالعدد 6 ليوم الأحد. يسرد توثيق بايثون العديد من التوابع الأخرى التي تمتلكها كائنات صنف date. على الرغم من أن كائن date يحتوي على سمات وتوابع متعددة، لكنه لا يزال كائنًا واحدًا يمكنك تخزينه في متغير، مثل birthday في هذا المثال. إنشاء صنف بسيط- WizCion دعنا ننشئ صنف WizCoin الذي يمثل عددًا من العملات في عالم سحري خيالي. فئات هذه العملة هي: knuts، و sickles (بقيمة 29 knuts)، و galleons (بقيمة 17 sickles أو 493 knuts). ضع في حساباتك أن العناصر الموجودة في صنف WizCoin تمثل كميةً من العملات، وليس مبلغًا من المال. على سبيل المثال، ستُخبرك أنك تمتلك خمسة أرباع سنت وعشرة سنتات بدلًا من 1.35 دولار. في ملف جديد باسم wizcoin.py، ضِف الشيفرة التالي لإنشاء صنف WizCoin. لاحظ أن اسم دالة __init__ له شرطتان سفليتان قبل وبعد init (سنناقش __init__ في " التابعين ‎ ‎__init__()‎ والمعامل self" لاحقًا): 1 class WizCoin: 2 def ‎__init__(self, galleons, sickles, knuts): ‫ """‫إنشاء كائن WizCoin جديد باستخدام galleons و sickles و knuts""" self.galleons = galleons self.sickles = sickles self.knuts = knuts # ‫ملاحظة: لا يوجد لتوابع ()__init‎__ قيمة مُعادة إطلاقًا 3 def value(self): ‫ """‎‫حساب القيمة بفئة knuts في غرض WizCoin لكل العملات""" return (self.galleons * 17 * 29) + (self.sickles * 29) + (self.knuts) 4 def weightInGrams(self): """حساب وزن العملات بالجرام""" return (self.galleons * 31.103) + (self.sickles * 11.34) + (self.knuts * 5.0) يعرّف هذا البرنامج صنفًا جديدًا يدعى WizCoin باستخدام التعليمة الأولى class، ويؤدي إنشاء صنف إلى إنشاء نوع جديد من الكائنات، إذ أن استخدام عبارة class لتعريف صنف يشبه عبارات def التي تعرّف دوالًا جديدة. توجد تعريفات لثلاثة توابع داخل كتلة التعليمات البرمجية التي تلي تعليمة class، هي: ‎__init __()‎‎ (اختصارًا للتهيئة)، و value()‎‎، و weightInGrams()‎‎. لاحظ أن جميع التوابع لها معامل أوّل يدعى self الذي سنكتشفه في القسم التالي. تكون أسماء الوحدات، مثل wizcoin في ملف wizcoin.py بأحرف صغيرة عادةً، بينما تبدأ أسماء الأصناف، مثل WizCoin بحرف كبير، ولكن للأسف، لا تتبع بعض الأصناف في مكتبة بايثون هذا الاصطلاح مثل صنف date. للتدرب على إنشاء كائنات جديدة لصنف WizCoin، ضِف الشيفرة المصدرية التالية في نافذة محرر ملفات منفصلة واحفظ الملف باسم wcexample1.py في المجلد wizcoin.py: import wizcoin 1 purse = wizcoin.WizCoin(2, 5, 99) # تُمرّر الأعداد الصحيحة إلى التابع‫ ()__init‎__ print(purse) print('G:', purse.galleons, 'S:', purse.sickles, 'K:', purse.knuts) print('Total value:', purse.value()‎‎) print('Weight:', purse.weightInGrams()‎‎, 'grams') print()‎‎ 2 coinJar = wizcoin.WizCoin(13, 0, 0) # تُمرّر الأعداد الصحيحة إلى التابع‫ ()__init‎__ print(coinJar) print('G:', coinJar.galleons, 'S:', coinJar.sickles, 'K:', coinJar.knuts) print('Total value:', coinJar.value()‎‎) print('Weight:', coinJar.weightInGrams()‎‎, 'grams') تُنشئ استدعاءات WizCoin()‎‎ كائن WizCoin وتُنفّذ الشيفرة في دالة‎‎__init __()‎‎. نمرّر هنا ثلاثة أعداد صحيحة مثل وسطاء إلى WizCoin()‎‎ ثم يُعاد توجيه تلك الوسطاء إلى معاملات ‎‎__init __()‎‎، تُعيَّن الوسطاء إلى سمات كائنات self.galleons و self.sickles و self.knuts. يجب علينا استيراد wizcoin ووضع .wizcoin قبل اسم دالة WizCoin()‎‎ تمامًا كما تتطلب دالة time.sleep()‎‎ أن تستورد وحدة time ووضع .time قبل اسم الدالة أولًا. عند تنفيذ البرنامج، سيبدو الخرج كما يلي: <wizcoin.WizCoin object at 0x000002136F138080> G: 2 S: 5 K: 99 Total value: 1230 Weight: 613.906 grams <wizcoin.WizCoin object at 0x000002136F138128> G: 13 S: 0 K: 0 Total value: 6409 Weight: 404.339 grams إذا تلقيت رسالة خطأ، مثل: ModuleNotFoundError: No module named 'wizcoin' تحقق أن الملف يحمل اسم wizcoin.py وأنه موجود في المجلد wcexample1.py ذاته. لا تمتلك كائنات WizCoin توضيحات نصية مفيدة، لذلك تعرض طباعة purse و coinJar عنوان تخزين بين قوسين (ستتعلم كيفية تغيير لاحقًا). يمكننا استدعاء التابعين value()‎‎ و weightInGrams()‎‎ على كائنات WizCoin التي خصصناها لمتغيري purse و coinJar، كما يمكننا استدعاء تابع السلسلة lower()‎‎ على كائن سلسلة نصية. تحسب هذه التوابع القيم بناءً على سمات كائنات galleons و sickles و knuts. يُفيد استخدام الأصناف classes والبرمجة كائنية التوجه في إنتاج شيفرات برمجية أكثر قابلية للصيانة؛ أي شيفرة يسهل قراءتها وتعديلها وتوسيعها مستقبلًا. التابع ‎‎ __init __()‎‎والمعامل self التوابع هي دوال مرتبطة بكائنات من صنف معين. تذكر أن low()‎‎ هو تابع لسلسلة، ما يعني أن استدعائه يكون على كائنات سلسلة نصية string. يمكنك استدعاء lower()‎‎ من سلسلة، مثل ‎'Hello'.lower()‎‎ ولكن لا يمكنك استدعائها على قائمة مثل: ‎['dog', 'cat'].lower()‎‎. لاحظ أيضًا أن التوابع تأتي بعد الكائن، والشيفرة الصحيحة هي ‎'Hello'.lower()‎‎، وليست lower('Hello'‎)‎. على عكس تابع مثل lower()‎‎، لا ترتبط دالة مثل len()‎‎ بنوع بيانات واحد؛ إذ يمكنك تمرير سلاسل وقوائم وقواميس وأنواع أخرى كثيرة من الكائنات إلى الدالة len()‎‎. نُنشئ كائنات عن طريق استدعاء اسم الصنف مثل دالة كما رأيت سابقًا، ويُشار إلى هذه الدالة على أنها دالة بانية constructor (أو باني، أو تُختصر باسم ctor، وتُنطق "see-tore") لأنها تُنشئ كائنًا جديدًا. نقول أيضًا أن الباني يبني نسخةً جديدةً للصنف. يؤدي استدعاء الباني إلى إنشاء كائن جديد ثم تنفيذ تابع ‎‎__init __()‎‎، ولا يُطلب من الأصناف أن يكون لديها تابع ‎‎__init __()‎‎، لكنها تملك هذا دائمًا تقريبًا. تابع ‎‎__init __()‎‎ هو المكان الذي تُعيّن فيه القيم الأولية للسمات عادةً. على سبيل المثال، تذكر أن تابع ‎‎__init __()‎‎ الخاص بالصنف WizCoin يبدو كما يلي: def ‎__init__(self, galleons, sickles, knuts): ‫ """‫إنشاء كائن WizCoin جديد باستخدام galleons و sickles و knuts""" self.galleons = galleons self.sickles = sickles self.knuts = knuts # ‫ملاحظة: لا يوجد لتوابع ()__init‎__ قيمة مُعادة إطلاقًا عندما يستدعي برنامج wcexample1.py ما يلي: WizCoin (2, 5, 99)‎، يبني بايثون كائن WizCoin جديد، ثم يمرر ثلاثة وسطاء (2 و 5 و 99) إلى استدعاء ‎‎__init __()‎‎، لكن للتابع ‎‎__init __()‎‎ أربعة معاملات، هي: self و galleons و sickles و knuts، والسبب هو أن جميع التوابع لها معامل أول يدعى self. عندما يُستدعى تابع ما على كائن، يُمرّر الكائن تلقائيًا لمعامل self، وتُعيَّن بقية الوسطاء للمعاملات بصورة طبيعية. إذا رأيت رسالة خطأ، مثل: TypeError: ‎__init__()‎‎ takes 3 positional arguments but 4 were given ربما تكون قد نسيت إضافة معامل self إلى تعليمة def الخاصة بالتابع. لا يتعين عليك تسمية المعامل الأول للتابع بالاسم self، إذ يمكنك تسميته بأي شيء آخر، لكن استخدام self أمر تقليدي، واختيار اسم مختلف سيجعل الشيفرة الخاصة بك أقل قابلية للقراءة لمبرمجي بايثون الآخرين. عندما تقرأ الشيفرة، فإن وجود self مثل معامل أول هو أسرع طريقة يمكنك من خلالها تمييز التوابع عن الدوال، وبالمثل، إذا كانت شفرة تابعك لا تحتاج أبدًا إلى استخدام معامل self، فهذه علامة على أن تابعك يجب أن يكون مجرد دالة. لا تُعيَّن الوسطاء 2 و 5 و 99 في WizCoin (2, 5, 99)‎ تلقائيًا إلى سمات الكائن الجديد؛ إذ نحتاج إلى عبارات الإسناد الثلاث في ‎‎__init __()‎‎ لإجراء ذلك. تُسمّى معاملات ‎‎__init __()‎‎ غالبًا باسم السمات ذاته، لكن يشير وجود self في self.galleons إلى أنها سمة من سمات الكائن، بينما يُعد galleons معاملًا. يعد تخزين وسطاء الباني في سمات الكائن مهمةً شائعةً لتابع ‎‎‎__init __()‎‎ للأصناف. نفّذ استدعاء datetime.date()‎‎ في القسم السابق مهمةً مماثلةً باستثناء أن الوسطاء الثلاثة التي مررناها كانت لسمات year و month و day لكائن date الذي أُنشئ حديثًا. لقد سبق لك أن استدعيت الدوال int()‎‎ و str()‎‎ و float()‎‎ و bool()‎‎ للتحويل بين أنواع البيانات، مثل str (3.1415)‎ للحصول على قيمة السلسلة '3.1415' بناءً على القيمة العشرية 3.1415. وصفنا ما سبق عندها على أنها دوال، لكن int و str و float و bool في الواقع أصناف، والدوال int()‎‎ و str()‎‎ و float()‎‎ و bool()‎‎ هي دوال بانية تعيد عددًا صحيحًا جديدًا أو سلسلة أو عدد عشري أو كائنات منطقية. يوصي دليل أسلوب بايثون باستخدام أحرف كبيرة لأسماء أصنافك، مثل WizCoin، على الرغم من أن العديد من أصناف بايثون المضمنة لا تتبع هذا الاصطلاح. يعيد استدعاء دالة الإنشاء WizCoin()‎‎ الكائن WizCoin الجديد، لكن التابع ‎‎__init __()‎‎ لا يحتوي أبدًا على عبارة return بقيمة مُعادة. تؤدي إضافة قيمة إعادة إلى حدوث هذا الخطأ: TypeError: ‎‎__init__()‎‎ should return None.‎ السمات السمات attributes -أو الخاصيات- هي متغيرات مرتبطة بكائن، ويصف توثيق بايثون السمات بأنها "أي اسم يتبع النقطة" على سبيل المثال، لاحظ تعبير birthday.year في القسم السابق، السمة year هي اسم يتبع النقطة. يمتلك كل كائن مجموعة السمات الخاصة به، فعندما أنشأ برنامج wcexample1.py كائنين WizCoin وخزّنهما في متغيرات purse و coinJar كان لسماتهما قيم مختلفة. يمكنك الوصول إلى هذه السمات وتعيينها تمامًا مثل أي متغير. للتدرب على إعداد السمات: افتح نافذة محرر ملفات جديدة وأدخل الشيفرة التالية، واحفظها بالاسم wcexample2.py في مجلد الملف wizcoin.py ذاته: import wizcoin change = wizcoin.WizCoin(9, 7, 20) print(change.sickles) # تطبع 7 change.sickles += 10 print(change.sickles) # تطبع 17 pile = wizcoin.WizCoin(2, 3, 31) print(pile.sickles) # تطبع 3 pile.someNewAttribute = 'a new attr' # إنشاء سمة جديدة print(pile.someNewAttribute) عند تنفيذ هذا البرنامج، يبدو الخرج كما يلي: 7 17 3 a new attr يمكنك التفكير في سمات الكائن بطريقة مشابهة لمفاتيح القاموس، إذ يمكنك قراءة وتعديل القيم المرتبطة بها وتعيين سمات جديدة للكائن، تقنيًا تُعدّ التوابع سمات للأصناف أيضًا. السمات والتوابع الخاصة يمكن تمييز السمات على أنها تتمتع بوصول خاص في لغات مثل C++‎ أو جافا، ما يعني أن المصرِّف compiler أو المُفسر interpreter يسمح فقط للشيفرة الموجودة في توابع الأصناف بالوصول إلى سمات كائنات تلك الصنف فقط أو تعديلها، لكن هذا الأمر غير موجود في بايثون، إذ تمتلك جميع السمات والتوابع وصولًا عامًا public access فعال، ويمكن للشيفرة خارج الصنف الوصول إلى أي سمة وتعديلها في أي كائن من ذلك الصنف. الوصول الخاص مفيد، إذ يمكن مثلًا أن تحتوي كائنات صنف BankAccount على سمة balance التي لا يجب الوصول إليها إلا لتوابع صنف BankAccount. لهذه الأسباب، ينص اصطلاح بايثون على بدء أسماء السمات أو التوابع الخاصة بشرطة سفلية واحدة. تقنيًا، لا يوجد ما يمنع الشيفرة خارج الصنف من الوصول إلى السمات والتوابع الخاصة، ولكن من الممارسات المُثلى تُملي بالسماح لتوابع الصنف فقط بالوصول إليها. افتح نافذة محرر ملفات جديدة، وأدخل الشيفرة التالية، واحفظها باسم privateExample.py. تحتوي كائنات صنف BankAccount في هذه الشيفرة على السمتين ‎_name و ‎_balance الخاصتين والتي يمكن فقط لتابعَي deposit()‎‎ و withdraw()‎‎ الوصول إليهما مباشرةً: class BankAccount: def ‎__init__(self, accountHolder): # ‫يمكن لتوابع ‎ BankAccount الوصول إلى self._balance ولكن الشيفرة خارج هذا الصنف لا يمكنها الوصول 1 self._balance = 0 2 self._name = accountHolder with open(self._name + 'Ledger.txt', 'w') as ledgerFile: ledgerFile.write('Balance is 0\n') def deposit(self, amount): 3 if amount <= 0: return # لا تسمح بقيم سالبة self._balance += amount 4 with open(self._name + 'Ledger.txt', 'a') as ledgerFile: ledgerFile.write('Deposit ' + str(amount) + '\n') ledgerFile.write('Balance is ' + str(self._balance) + '\n') def withdraw(self, amount): 5 if self._balance < amount or amount < 0: return # لا يوجد نقود كافية في الحساب أو أن الرصيد سالب self._balance -= amount 6 with open(self._name + 'Ledger.txt', 'a') as ledgerFile: ledgerFile.write('Withdraw ' + str(amount) + '\n') ledgerFile.write('Balance is ' + str(self._balance) + '\n') acct = BankAccount('Alice') # أنشأنا حساب خاص بأليس acct.deposit(120) # ‫يمكن تعديل السمة ‫‎_balance‎‎ باستخدام deposit()‎ acct.withdraw(40) # ‫يمكن تعديل السمة ‫‎_balance‎‎ باستخدام withdraw()‎ # ‫‎التغيير من ‎_name و ‎_balance أمر غير محبّذ ولكنه ممكن 7 acct._balance = 1000000000 acct.withdraw(1000) 8 acct._name = 'Bob' # ‎‫نستطيع الآن التعديل على سجل Bob! acct.withdraw(1000) # عملية السحب هذه مسجلة في‫ BobLedger.txt! عند تنفيذ privateExample.py، تكون الملفات التي تُنشأ غير دقيقة لأننا عدّلنا على ‎_balance و ‎_name خارج الصنف، مما أدى إلى حالات غير صالحة. يحتوي AliceLedger.txt على الكثير من المال بداخله: Balance is 0 Deposit 120 Balance is 120 Withdraw 40 Balance is 80 Withdraw 1000 Balance is 999999000 يوجد الآن ملف BobLedger.txt برصيد حساب لا يمكن تفسيره، على الرغم من أننا لم ننشئ كائن BankAccount لسجل Bob إطلاقًا: Withdraw 1000 Balance is 999998000 تكون الأصناف المصممة جيدًا في الغالب قائمة بحد ذاتها self-contained، مما يوفر توابع لضبط السمات على القيم الصحيحة. تُميَّز السمتين ‎_balance و ‎_name برقمي السطرين 1 و2، والطريقة الصالحة الوحيدة لتعديل قيمة صنف BankAccount هي من خلال التابعين deposit()‎‎ و withdraw()‎‎؛ إذ يحقق هذان التابعان من تعليمة (3) وتعليمة (5) للتأكد من أن ‎_balance لم توضع في حالة غير صالحة (مثل قيمة عدد صحيح سالب). يسجل هذان التابعان أيضًا كل معاملة لحساب الرصيد الحالي في تعليمة (4) وتعليمة (6). يمكن أن تضع الشيفرة البرمجية التي تعدل هذه السمات وتقع خارج الصنف، مثل تعليمة ‎acct._balance = 1000000000‎ (التعليمة 7) أو تعليمة acct._name = 'Bob'‎ (التعليمة ? ذلك الكائن في حالة غير صالحة ويتسبب بأخطاء وعمليات تدقيق من فاحص البنك. يصبح تصحيح الأخطاء أسهل باتباع اصطلاح بادئة الشرطة السفلية للوصول الخاص، والسبب هو أنك تعرف أن سبب الخطأ سيكون داخل شيفرة الصنف بدلًا من أي مكان في البرنامج بأكمله. لاحظ أنه على عكس جافا واللغات الأخرى، لا تحتاج بايثون إلى توابع getter و setter العامة للسمات الخاصة، وتستخدم بدلًا من ذلك الخاصيات properties، كما هو موضح لاحقًا. دالة type()‎‎ وسمة qualname يخبرنا تمرير كائن إلى دالة type()‎‎ المضمنة بنوع بيانات الكائن من خلال قيمته المُعادة، والكائنات التي تُعاد من دالة type()‎‎ هي أنواع كائنات، وتسمى أيضًا كائنات الصنف. تذكر أن مصطلح النوع ونوع البيانات والصنف لها المعنى ذاته في بايثون. لمعرفة ما تُعيده دالة type()‎‎ للقيم المختلفة، أدخل ما يلي في الصدفة التفاعلية: >>> type(42) # The object 42 has a type of int. <class 'int'> >>> int # int is a type object for the integer data type. <class 'int'> >>> type(42) == int # Type check 42 to see if it is an integer. True >>> type('Hello') == int # Type check 'Hello' against int. False >>> import wizcoin >>> type(42) == wizcoin.WizCoin # Type check 42 against WizCoin. False >>> purse = wizcoin.WizCoin(2, 5, 10) >>> type(purse) == wizcoin.WizCoin # Type check purse against WizCoin. True لاحظ أن int هو نوع كائن وهو نفس نوع الكائن الذي يُعيده type(42)‎، ولكن يمكن أيضًا تسميته بدالة بانية int()‎‎؛ إذ لا تحوّل الدالة int ('42')‎ وسيط السلسلة '42'، وتُعيد بدلًا من ذلك كائن عدد صحيح بناءً على المعطيات. لنفترض أنك بحاجة إلى تسجيل بعض المعلومات حول المتغيرات في برنامجك لمساعدتك على تصحيحها لاحقًا. يمكنك فقط كتابة سلاسل إلى ملف السجل، ولكن تمرير كائن النوع إلى str()‎‎ سيعيد سلسلة تبدو فوضوية إلى حد ما. بدلًا من ذلك، استخدم السمة __qualname__، التي تمتلكها جميع أنواع الكائنات، لكتابة سلسلة أبسط يمكن للبشر قراءتها: >>> str(type(42)) # Passing the type object to str() returns a messy string. "<class 'int'>" >>> type(42).__qualname__ # The __qualname__ attribute is nicer looking. 'int' تُستخدم سمة __qualname__ غالبًا لتجاوز تابع __repr __()‎‎، والتي سنشرحها بمزيد من التفصيل لاحقًا. الخلاصة البرمجة كائنية التوجه هي ميزة مفيدة لتنظيم الشيفرة البرمجية الخاصة بك. تتيح لك الأصناف تجميع البيانات والشيفرات البرمجية معًا في أنواع بيانات جديدة. يمكنك أيضًا إنشاء كائنات من هذه الأصناف عن طريق استدعاء بانيها (اسم الصنف المُستدعى مثل دالة)، والتي بدورها تستدعي تابع ‎__init __()‎‎ الخاص بالصنف. التوابع هي دوال مرتبطة بالكائنات، والسمات هي متغيرات مرتبطة بالكائنات. تحتوي جميع التوابع على معامل أول self، والذي يُعيّن للكائن عند استدعاء التابع. يسمح هذا للتوابع بقراءة سمات الكائن أو تعيينها واستدعاء توابعها. على الرغم من أن بايثون لا تسمح لك بتحديد الوصول الخاص أو العام للسمات، إلا أنها تمتلك اصطلاحًا باستخدام بادئة شرطة سفلية لأي تابع أو سمات يجب استدعاؤها أو الوصول إليها فقط من توابع الصنف الخاصة. يمكنك -باتباع هذه الاتفاقية- تجنب إساءة استخدام الصنف ووضعها في حالة غير صالحة يمكن أن تسبب أخطاء. سيعيد استدعاء type(obj)‎ كائن صنف النوع obj. تحتوي كائنات الصنف على سمة __qualname___ التي تحتوي على سلسلة بشكل يمكن للبشر قراءته من اسم الصنف. في هذه المرحلة، ربما تفكر، لماذا يجب أن نهتم باستخدام الأصناف والسمات والتوابع بينما يمكننا إنجاز المهمة ذاتها مع الدوال؟ تُعد البرمجة كائنية التوجه طريقةً مفيدةً لتنظيم الشيفرات البرمجية الخاصة بك في أكثر من مجرد ملف "‎.py" يحتوي على 100 دالة فيه. من خلال تقسيم البرنامج إلى عدة أصناف مصممة جيدًا، يمكنك التركيز على كل صنف على حدة. البرمجة كائنية التوجه هي نهج يركز على هياكل البيانات وطرق التعامل مع هياكل البيانات تلك. هذا النهج ليس إلزاميًا لكل برنامج، ومن الممكن بالتأكيد الإفراط في استخدام البرمجة كائنية التوجه، لكن البرمجة كائنية التوجه توفر فرصًا لاستخدام العديد من الميزات المتقدمة التي سنستكشفها في الفصلين التاليين. أول هذه الميزات هو الوراثة inheritance التي سنتعمق فيها في الفصل التالي. ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming And Classes من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: برمجة لعبة أربع نقاط في صف واحد Four-in-a-Row باستخدام لغة بايثون مصطلحات شائعة مثيرة للالتباس في بايثون. البرمجة كائنية التوجه كيفية إنشاء الأصناف وتعريف الكائنات في بايثون 3.
  14. بعد رحلة طويلة وصلنا إلى نهاية السلسلة البرمجة بلغة رست. سنبني في هذا القسم مشروعًا لتوضيح بعض المفاهيم التي تحدثنا عنها في المقالات السابقة وتذكر بعض الدروس السابقة. سنبني خادم ويب يعرض "hello" ويشبه الشكل 1 في متصفح الويب. [الشكل1: مشروعنا الأخير المشترك] هذه هي خطة بناء خادم الويب: مقدمة عن TCP و HTTP الاستماع إلى اتصالات TCP على المقبس socket تحليل عدد صغير من طلبات HTTP إنشاء استجابة HTTP مناسبة تطوير خرج الخادم بمجمع خيط thread pool قبل البدء، يجب التنويه على أن هذه الطريقة ليست أفضل طريقة لبناء خادم ويب باستخدام رست، إذ نشر أعضاء المجتمع وحدات مصرفة جاهزة للتطبيق على creats.io، والتي تقدم خوادم ويب أكثر اكتمالًا وتطبيقات لمجمع خيط أفضل من الذي سنبنيه، ولكن هدفنا من هذا الفصل هو مساعدتك على التعلم وليس اختيار الطريق الأسهل. يمكننا اختيار مستوى التجريد الذي نريد العمل معه لأن رست هي لغة برمجية للأنظمة ويمكن الانتقال لمستوى أدنى مما هو ممكن أو عملي في بعض اللغات الأُخرى، لذلك سنكتب خادم HTTP بسيط ومجمع الخيط يدويًا لنتعلم الأفكار والتقنيات العامة الموجودة في الوحدات المصرفة التي يمكن أن تراها في المستقبل. بناء خادم ويب أحادي الخيط سنبدأ بإنشاء خادم ويب أحادي الخيط، ولكن قبل أن نبدأ دعنا نراجع البروتوكولات المستخدمة في إنشاء خوادم الويب. تفاصيل هذه البروتوكولات هي خارج نطاق موضوعنا هنا إلا أن مراجعة سريعة ستمنحك المعلومات الكافية. البروتوكولان الأساسيان المعنيان في خوادم الويب هما بروتوكول نقل النصوص الفائقة Hypertext Transfer Protocol‏ -أو اختصارًا HTTP- وبروتوكول تحكم النقل Transmission Control Protocol‎ -أو اختصارًا TCP، وهما بروتوكولا طلب-استجابة؛ يعني أن العميل يبدأ الطلبات ويسمع الخادم الطلبات ويقدم استجابةً للعميل، ويُعرّف محتوى هذه الطلبات والاستجابات عبر هذه البروتوكولات. يصف بروتوكول TCP تفاصيل انتقال المعلومات من خادم لآخر ولكن لا يحدد نوع المعلومات. يبني HTTP فوق TCP عن طريق تعريف محتوى الطلبات والاستجابات. يمكن تقنيًا استخدام HTTP مع بروتوكولات أُخرى لكن في معظم الحالات يرسل HTTP البيانات على بروتوكول TCP. سنعمل مع البايتات الخام في طلبات واستجابات TCP و HTTP. الاستماع لاتصال TCP يجب أن يستمع خادم الويب إلى اتصال TCP لذا سنعمل على هذا الجزء أولًا. تقدم المكتبة القياسية وحدة std::net التي تسمح لنا بذلك. لننشئ مشروعًا جديدًا بالطريقة الاعتيادية: $ cargo new hello Created binary (application) `hello` project $ cd hello الآن اكتب الشيفرة 1 في الملف src/main.rs لنبدأ. ستسمع هذه الشيفرة إلى العنوان المحلي "127.0.0.1:7878" لمجرى TCP stream القادم، وعندما تستقبل مجرى قادم ستطبع Connection established!‎. اسم الملف: src/main.rs use std::net::TcpListener; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); println!("Connection established!"); } } [الشيفرة 1: الاستماع للمجاري القادمة وطباعة رسالة عند استقبال مجرى] يمكننا الاستماع لاتصال TCP على هذا العنوان "127.0.0.1:7878" باستخدام TcpListner، إذ يمثّل القسم قبل النقطتين عنوان IP الذي يمثل الحاسوب (هذا العنوان هو نفسه لكل الحواسيب وليس لحاسوب المستخدم حصريًا)، ورقم المنفذ هو 7878. اخترنا هذا المنفذ لسببين: لا يُقبل HTTP على هذا المنفذ لذا لا يتعارض الخادم بأي خدمة ويب ربما تحتاجها على جهازك، و 7878 هي كلمة rust مكتوبة على لوحة أرقام الهاتف. تعمل دالة bind في هذا الحالة مثل دالة new التي ترجع نسخة TcpListner جديدة. تسمى الدالة bind لأن الاتصال بمنفذ للاستماع إليه هي عملية تُعرف باسم الربط لمنفذ binding to a port. تعيد الدالة bind القيمة Results<T, E>‎ التي تشير أنه من الممكن أن يفشل الربط. يتطلب الاتصال بالمنفذ 80 امتيازات المسؤول (يستطيع غير المسؤولين فقط الاستماع في المنافذ الأعلى من 1023)، لذا لا يعمل الارتباط إذا حاولت الاتصال بالمنفذ 80 بدون كونك مسؤول، ولا يعمل الارتباط أيضًا إذا نفذنا نسختين من برنامجنا أي لدينا برنامجين يستمعان لنفس المنفذ. لا يلزمنا أن نتعامل مع هكذا أخطاء لأننا نكتب خادم بسيط لأغراض تعليمية فقط. نستعمل unwrap لإيقاف البرنامج إذا حصلت أي أخطاء. يعيد التابع incoming على TcpListner مكرّرًا iterator يعطي سلسلةً من المجاري (مجاري نوع TcpStream تحديدًا). يمثل المجرى الواحد اتصالًا مفتوحًا بين العميل والخادم، والاتصال هو الاسم الكامل لعملية الطلب والاستجابة التي يتصل فيها العميل إلى الخادم، وينشئ الخادم استجابةً ويغلق الاتصال. كذلك، سنقرأ من TcpStream لرؤية ماذا أرسل العميل وكتابة استجابتنا إلى المجرى لإرسال البيانات إلى العميل. ستعالج حلقة for عمومًا كل اتصال بدوره وتضيف سلسلة من المجاري لنتعامل معها. تتألف حتى الآن طريقتنا للتعامل مع المجرى من استدعاء unwrap لينهي البرنامج إذا كان للمجرى أي أخطاء، وإذا لم يكن هناك أخطاء يطبع البرنامج رسالة، وسنضيف وظائفًا إضافية في حالة النجاح في الشيفرة التالية. سبب استقبال أخطاء من تابع incoming عندما يتصل عميل بالخادم هو أننا نكرّر زيادةً عن حد محاولات الاتصال بدلًا من أن نكرّر أعلى من حد الاتصالات؛ فقد تفشل محاولات الاتصال لعدد من الأسباب ويتعلق العديد منها بنظام التشغيل، فمثلًا تحدّد الكثير من أنظمة التشغيل عدد الاتصالات المفتوحة بالوقت الذي تدعمها، وستعطي أي اتصالات جديدة خطأ حتى تُغلق أي اتصالات مفتوحة. لنحاول تنفيذ هذه الشيفرة، استدعِ cargo run في الطرفية وحمّل 127.0.0.1:7878 في متصفح الويب. يجب أن يظهر المتصفح رسالة الخطأ "إعادة ضبط الاتصال" لأن الخادم لا يرسل أي بيانات حاليًا، لكن عندما تنظر إلى الطرفية يجب أن ترى عدد من الرسائل المطبوعة عندما يتصل المتصفح بالخادم. Running `target/debug/hello` Connection established! Connection established! Connection established! سنرى أحيانًا عددًا من الرسائل المطبوعة لطلب متصفح واحد، ويعود سبب ذلك إلى أن المتصفح أنشأ طلبًا الصفحة وكذلك لعدد من الموارد الأخرى مثل أيقونة favicon.ico التي تظهر على صفحة المتصفح. يمكن أن تعني أيضًا أن المتصفح يحاول الاتصال بالخادم مرات متعددة لأنه لا يتجاوب مع أي بيانات. يُغلق الاتصال كجزء من تنفيذ drop عندما تخرج stream عن النطاق وتُسقط في نهاية الحلقة. تتعامل المتصفحات أحيانًا مع الاتصالات المغلقة بإعادة المحاولة لأن هذه المشكلة يمكن أن تكون مؤقتة. العامل المهم أنه حصلنا على مقبض لاتصال TCP. تذكر أن توقف البرنامج بالضغط على المفتاحين "ctrl-c" عندما تنتهي من تنفيذ نسخة معينة من الشيفرة، بعدها أعد تشغيل البرنامج باستدعاء أمر cargo run بعد إجراء أي تعديل على الشيفرة للتأكد من أنك تنفذ أحدث إصدار منها. قراءة الطلب دعنا ننفّذ وظيفةً لقراءة الطلب من المتصفح، إذ سنبدأ بدالة جديدة لمعالجة الاتصالات من أجل الفصل بين الحصول على اتصال وإجراء بعض الأعمال بالاتصال. سنقرأ دالة handle_connection البيانات من مجرى TCP وتطبعها لرؤية البيانات التي أُرسلت من المتصفح. غيّر الشيفرة لتصبح مثل الشيفرة 2. اسم الملف: src/main.rs use std::{ io::{prelude::*, BufReader}, net::{TcpListener, TcpStream}, }; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&mut stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) .take_while(|line| !line.is_empty()) .collect(); println!("Request: {:#?}", http_request); } [الشيفرة 2: القراءة من TcpStream وطباعة البيانات] نضيف std::io::prelude و std::io::BufReader إلى النطاق للحصول على سمات وأنواع تسمح لنا بالقراءة من والكتابة على المجرى. بدلًا من طباعة رسالة تقول أننا اتصلنا، نستدعي الدالة الجديدة handle_connection في حلقة for في الدالة main ونمرّر stream إليها. أنشأنا نسخة BufReader في دالة handle_connection التي تغلف المرجع المتغيّر إلى stream. يضيف BufReader تخزينًا مؤقتًا عن طريق إدارة الاستدعاءات إلى توابع سمة std::io::Read. أنشأنا متغيرًا اسمه http_request لجمع أسطر الطلب التي أرسله المتصفح إلى الخادم، ونشير أننا نريد جمع هذه الأسطر في شعاع عن طريق إضافة توصيف نوع Vec<_>‎. ينفّذ BufReader سمة std::io::BufRead التي تؤمن التابع lines، الذي يعيد مكرّر <Result<String, std::io::Error عن طريق فصل مجرى البيانات أينما ترى بايت سطر جديد. للحصول على كل String، نربط ونزيل تغليف unwarp كل Result. يمكن أن تكون Result خطأ إذا كانت البيانات ليست UTF-8 صالح أو كان هناك مشكلة في القراءة من المجرى. مجددًا، يمكن لبرنامج إنتاجي حل هذه المشكلات بسهولة ولكننا اخترنا إيقاف البرنامج في حالة الخطأ للتبسيط. يشير المتصفح إلى نهاية طلب HTTP عن طريق إرسال محرفي سطر جديد على الترتيب، لذا للحصول على طلب من المجرى نأخذ الأسطر حتى نصل إلى سلسلة نصية فارغة. عندما نجمع الأسطر في الشعاع سنطبعهم باستخدام تنسيقات جذابة pretty لتنقيح الأخطاء لكي ننظر إلى التعليمات التي يرسلها المتصفح إلى الخادم. لنجرب هذه الشيفرة. ابدأ البرنامج واطلب الصفحة في المتصفح مجددًا. لاحظ أنك ستحصل على صفحة خطأ في المتصفح، ولكن خرج البرنامج في الطرفية سيكون مشابهًا للتالي: $ cargo run Compiling hello v0.1.0 (file:///projects/hello) Finished dev [unoptimized + debuginfo] target(s) in 0.42s Running `target/debug/hello` Request: [ "GET / HTTP/1.1", "Host: 127.0.0.1:7878", "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language: en-US,en;q=0.5", "Accept-Encoding: gzip, deflate, br", "DNT: 1", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "Sec-Fetch-Dest: document", "Sec-Fetch-Mode: navigate", "Sec-Fetch-Site: none", "Sec-Fetch-User: ?1", "Cache-Control: max-age=0", ] اعتمادًا على المتصفح يمكن أن تحصل على خرج مختلف قليلًا. نطبع الآن طلبات البيانات ويمكن مشاهدة لماذا نحصل على اتصالات متعددة من طلب واحد من المتصفح بتتبع المسار الذي بعد GET في أول سطر من الطلب. إذا كانت الاتصالات المتعددة كلها تطلب "/"، نعرف أن المتصفح يحاول إيجاد "/" باستمرار لأنه لم يحصل على استجابة من برنامجنا. لنفصّل بيانات الطلب لفهم ما يطلبه المتصفح من برنامجنا. نظرة أقرب على طلب HTTP بروتوكول HTTP أساسه نصي وتكون طلباته على الشكل التالي: Method Request-URI HTTP-Version CRLF headers CRLF message-body السطر الأول هو سطر الطلب الذي يحتوي معلومات عما يطلبه العميل، إذ يدل القسم الأول من سطر الطلب على التابع المستخدم مثل GET أو POST الذي يصف كيفية إجراء العميل لهذا الطلب. استخدم عميلنا طلب GET، وهذا يعني أنه يطلب معلومات؛ بينما يشير القسم الثاني "/" من سطر الطلبات إلى معرّف الموارد الموحد Uniform Resource Identifier‎ -أو اختصارًا URI- الذي يطلبه العميل. يشابه URI محدد الموارد الموحد Uniform Resource Locator -أو URL اختصارًا- ولكن ليس تمامًا، إذ أن الفرق بينهم ليس مهمًا لهذا المشروع، لكن تستخدم مواصفات HTTP المصطلح URI لذا نستبدل هنا URL بالمصطلح URI. القسم الأخير هو نسخة HTTP التي يستخدمها العميل، وينتهي الطلب بسلسلة CRLF (تعني CRLF محرف العودة إلى أول السطر والانتقال سطر للأسفل Carriage Return and Line Feed وهما مصطلحان من أيام الآلة الكاتبة). يمكن كتابة سلسلة CRLF مثل ‎\r\n إذ أن r\ هي محرف العودة إلى أول السطر و n\ هو الانتقال سطر للأسفل. تفصل سلسلة CRLF سطر الطلب من باقي بيانات الطلب. نلاحظ عندما تُطبع CRLF نرى بداية سطر جديد بدل ‎\r\n. عند ملاحظة سطر البيانات الذي استقبلناه من تنفيذ برنامجنا حتى الآن نرى أن التابع هو GET وطلب URI هو / والنسخة هي HTTP/1.1. الأسطر الباقية بدءًا من Host: وبعد هي ترويسات. طلب GET لا يحتوي متن. حاول عمل طلب من متصفح آخر أو طلب عنوان مختلف مثل 127.0.0.1:7878‎/test لترى كيف تتغير بيانات الطلب. بعد أن عرفنا ماذا يريد المتصفح لنرسل بعض البيانات. كتابة استجابة سننفّذ إرسال بيانات مثل استجابة لطلب عميل. لدى الاستجابات التنسيق التالي: HTTP-Version Status-Code Reason-Phrase CRLF headers CRLF message-body يحتوي السطر الأول الذي هو سطر الحالة نسخة HTTP المستخدمة في الاستجابة ورمز حالة status code عددية تلخص نتيجة الطلب وعبارة سبب تقدم شرحًا نصيًا عن رمز الحالة. يوجد بعد سلسلة CRLF ترويسات وسلسلة CRLF أُخرى ومتن الاستجابة. لدينا مثال عن استجابة تستخدم نسخة HTTP 1.1 ولديها رمز حالة 200 وعبارة سبب OK بلا ترويسة أو متن. HTTP/1.1 200 OK\r\n\r\n يُعد رمز الحالة 200 استجابة نجاح قياسية والنص هو استجابة نجاح HTTP صغيرة. لنكتب ذلك إلى المجرى مثل استجابة لطلب ناجح. أزل !println التي كانت تطبع طلب البيانات من الدالة handle_connection واستبدلها بالشيفرة 3. اسم الملف: src/main.rs fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&mut stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) .take_while(|line| !line.is_empty()) .collect(); let response = "HTTP/1.1 200 OK\r\n\r\n"; stream.write_all(response.as_bytes()).unwrap(); } [الشيفرة 3: كتابة استجابة نجاح HTTP صغيرة إلى المجرى] يعرّف أول سطر المتغير response الذي يحتوي بيانات رسالة النجاح، بعدها نستدعي as_bytes على response الخاص بنا لتحويل بيانات السلسلة النصية إلى بايتات. يأخذ تابع write_all على stream النوع ‏‏[u8]‏‏& ويرسل هذه البايتات مباشرةً نحو الاتصال لأن عملية write_all قد تفشل. نستعمل unwrap على أي خطأ ناتج كما فعلنا سابقًا. مُجددًا، يجب أن تتعامل مع الأخطاء في التطبيقات الحقيقية. لننفذ شيفرتنا بعد إجراء التعديلات ونرسل طلبًا. لا نطبع أي بيانات إلى الطرفية لذا لا نرى أي خرج ما عدا خرج Cargo. عند تحميل 127.0.0.1:7878 في متصفح الويب يجب أن يظهر صفحة فارغة بدلًا من خطأ، وبذلك تكون قد شفّرت يدويًا استقبال طلب HTTP وإرسال استجابة. إعادة HTML حقيقي لننفّذ وظيفة إعادة أكثر من صفحة فارغة. أنشئ الملف الجديد hello.html في جذر مسار مشروعك وليس في مسار src. يمكنك إدخال أي HTML تريده، تظهر الشيفرة 4 أحد الاحتمالات. اسم الملف:hello.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello!</title> </head> <body> <h1>Hello!</h1> <p>Hi from Rust</p> </body> </html> [الشيفرة 4: مثال ملف HTML يعيد استجابة] يمثّل هذا وثيقة HTML5 بسيطة مع ترويسة وبعض النصوص. سنعدّل الدالة handle_connection لإعادتها من الخادم عندما يُستقبل الطلب، كما في الشيفرة 5 وذلك لقراءة ملف HTML وإضافة الاستجابة مثل متن وإرساله. اسم الملف: src/main.rs use std::{ fs, io::{prelude::*, BufReader}, net::{TcpListener, TcpStream}, }; // --snip-- fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&mut stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) .take_while(|line| !line.is_empty()) .collect(); let status_line = "HTTP/1.1 200 OK"; let contents = fs::read_to_string("hello.html").unwrap(); let length = contents.len(); let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); stream.write_all(response.as_bytes()).unwrap(); } [الشيفرة 5: إرسال محتوى hello.html مثل متن الاستجابة] أضفنا fs إلى تعليمة use لجلب وحدة نظام ملفات المكتبة القياسية إلى النطاق. يجب أن تكون الشيفرة لقراءة محتوى الملف إلى سلسلةً نصيةً مألوفة، إذ استخدمناها سابقًا في مقال كتابة برنامج سطر أوامر Command Line بلغة رست Rust عندما قرأنا محتوى ملف مشروع I/O في الشيفرة 4. استخدمنا !format لإضافة محتوى الملف على أنه متن استجابة النجاح، أضفنا الترويسة Content-Length التي تحدد حجم متن الاستجابة وفي حالتنا حجم hello.html لضمان استجابة HTTP صالحة لا. نفذ هذه الشيفرة مع cargo run وحمّل 1270.0.1:7878 في المتصفح، يجب أن ترى HTML الخاص بك معروضًا. نتجاهل حاليًا طلب البيانات في http_request ونرسل فقط محتوى ملف HTML دون شروط، هذا يعني إذا جربنا طلب 127.0.0.1:7878‎/something-else في المتصفح سنحصل على نفس استجابة HTML. في هذه اللحظة الخادم محدود ولا يفعل ما يفعله خوادم الويب، ونريد تعديل استجابتنا اعتمادًا على الطلب وإرسال ملف HTML فقط لطلب منسق جيدًا إلى "/". التحقق من صحة الطلب والاستجابة بصورة انتقائية يعيد خادم الويب الخاص بنا ملف HTML مهما كان طلب العميل، لنضف وظيفة التحقق أن المتصفح يطلب "/" قبل إعادة ملف HTML وإعادة خطأ في حال طلب المتصفح شيئًا آخر، لذا نحتاج لتعديل handle_connection كما في الشيفرة 6. تتحقق هذه الشيفرة الجديدة محتوى الطلب المُستقبل مع ما يشبه طلب "/" وتضيف كتل if و else لمعالجة الطلبات على نحوٍ مختلف. اسم الملف: src/main.rs // --snip-- fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&mut stream); let request_line = buf_reader.lines().next().unwrap().unwrap(); if request_line == "GET / HTTP/1.1" { let status_line = "HTTP/1.1 200 OK"; let contents = fs::read_to_string("hello.html").unwrap(); let length = contents.len(); let response = format!( "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}" ); stream.write_all(response.as_bytes()).unwrap(); } else { // some other request } } [الشيفرة 6: معالجة الطلبات إلى / على نحوٍ مختلف عن الطلبات الأُخرى] سننظر فقط إلى السطر الأول من طلب HTTP لذا بدلًا من قراءة كامل الطلب لشعاع، نستدعي next ليأخذ العنصر الأول من المكرّر. تتعامل unwarp الأولى مع Option وتوقف البرنامج إذا لم يكن للمكرّر أي عنصر؛ بينما تتعامل unwarp الثانية مع Result ولها نفس تأثير unwarp التي كان في map المضافة في الشيفرة 2. نتحقق بعد ذلك من request_line لنرى إذا كانت تساوي سطر طلب GET إلى المسار"/"؛ فإذا ساوت تعيد كتلة if محتوى ملف HTML؛ وإذا لم تساوي، يعني ذلك أننا استقبلنا طلب آخر. سنضيف شيفرة إلى كتلة else بعد قليل لاستجابة الطلبات الأخرى. نفذ هذه الشيفرة واطلب 127.0.0.1:7878، يجب أن تحصل على HTML في hello.html. إذا طلبت أي شيء آخر مثل 127.0.0.1:7878‎/something-else ستحصل على خطأ اتصال مثل الذي تراه عند تنفيذ الشيفرة 1 و2. لنضيف الشيفرة في الشيفرة 7 إلى كتلة else لإعادة استجابة مع رمز الحالة 404 التي تشير إلى أن محتوى الطلب ليس موجودًا. سنعيد بعض HTML للصفحة لتصّير في المتصفح مشيرةً إلى جواب للمستخدم النهائي. اسم الملف: src/main.rs // --snip-- } else { let status_line = "HTTP/1.1 404 NOT FOUND"; let contents = fs::read_to_string("404.html").unwrap(); let length = contents.len(); let response = format!( "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}" ); stream.write_all(response.as_bytes()).unwrap(); } [الشيفرة 7: الاستجابة برمز الحالة 404 وصفحة خطأ إذا كان أي شيء عدا / قد طُلب] لدى استجابتنا سطر حالة مع رمز الحالة 404 وعبارة سبب NOT FOUND، يكون متن الاستجابة HTML في الملف ‎404.html. نحن بحاجة انشاء ملف ‎404.html بجانب hello.html لصفحة الخطأ، ويمكنك استخدام أي HTML تريده أو استخدم مثال HTML في الشيفرة 8. اسم الملف: ‎404.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello!</title> </head> <body> <h1>Oops!</h1> <p>Sorry, I don't know what you're asking for.</p> </body> </html> [الشيفرة 8: محتوى معين لصفحة كي تُرسل مع أي استجابة 404] شغّل الخادم مجددًا بعد هذه التغيرات. يجب أن يعيد محتوى hello.html عند طلب 127.0.0.1:7878 ويعيد خطأ HTML من ‎404.html في حال طلب آخر مثل 127.0.0.1:7878‎/foo. القليل من إعادة بناء التعليمات البرمجية في هذه اللحظة لدى كتلتي if و else الكثير من التكرار، فهما تقرأن الملفات وتكتبان محتوى الملفات إلى المجرى. الفرق الوحيد بينهما هو سطر الحالة واسم الملف. لنجعل الشيفرة أدق بسحب هذه الاختلافات إلى سطري if و else منفصلين، ليعينان القيم إلى المتغيرين سطر الحالة واسم الملف. يمكننا استخدام هذه المتغيرات دون قيود في الشيفرة لقراءة الملف وكتابة الاستجابة. تظهر الشيفرة 9 الشيفرة المُنتجة بعد استبدال كتل if و else الكبيرة. اسم الملف: src/main.rs // --snip-- fn handle_connection(mut stream: TcpStream) { // --snip-- let (status_line, filename) = if request_line == "GET / HTTP/1.1" { ("HTTP/1.1 200 OK", "hello.html") } else { ("HTTP/1.1 404 NOT FOUND", "404.html") }; let contents = fs::read_to_string(filename).unwrap(); let length = contents.len(); let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); stream.write_all(response.as_bytes()).unwrap(); } [الشيفرة 9: إعادة بناء التعليمات البرمجية لكتل if و else لتحتوي فقط على الشيفرة المختلفة بين الحالتين] تعيد الآن كتلتا if و else فقط القيم المناسبة لسطر الحالة واسم الملف في الصف. نستخدم بعد ذلك التفكيك لتحديد هذه القيمتين إلى status_line و filename باستخدام الأنماط في تعليمة let كما تحدثنا في الفصل 18. الشيفرة المتكررة الآن هي خارج كتلتي if و else وتستخدم المتغيران status_line و filename. يسهّل هذا مشاهدة الفرق بين الحالتين ولدينا فقط مكان واحد لتعديل الشيفرة إذا أردنا تغيير كيفية قراءة الملفات وكتابة الاستجابة. سيكون سلوك الشيفرة في الشيفرة 9 مثل ماهو في الشيفرة 8. ممتاز، لديك الآن خادم ويب بسيط في حوالي 40 سطر من شيفرة رست الذي يستجيب لطلب واحد مع صفحة محتوى ويستجيب برمز حالة 404 لكل الطلبات الأُخرى. ينفذ الخادم حاليًا خيطًا واحدًا، بمعنى أنه يُخدّم طلبًا واحدًا كل مرة. لنفحص تاليًا كيف يمكن لذلك أن يسبب مشكلةً بمحاكاة بعض الطلبات البطيئة، ثم سنعالج هذه المشكلة لكي يعالج الخادم طلبات متعددة بالوقت ذاته. ترجمة -وبتصرف- لقسم من الفصل Final Project: Building a Multithreaded Web Server من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: الماكرو Macros في لغة رست إنشاء خادم ويب في Node.js باستخدام الوحدة HTTP دليل إعداد خادم ويب محلي خطوة بخطوة
  15. لعبة أربع نقاط في صف واحد Four-in-a-Row هي لعبة للاعبين اثنين، إذ يضع كل منهما حجرًا، ويحاول كل لاعب إنشاء صف مكون من أربعة من حجراته، سواء أفقيًا أو رأسيًا أو قطريًا، وهي مشابهة للعبتَين Connect Four و Four Up. تستخدم اللعبة لوحة قياس 7×6، وتشغل المربعات أدنى مساحة شاغرة في العمود. في لعبتنا، سيلعب لاعبان بشريان، X و O، ضد بعضهما، وليس لاعب بشري واحد ضد الحاسوب. خرج اللعبة سيبدو الخرج كما يلي عند تنفيذ برنامج أربع نقاط في صف واحد: Four-in-a-Row, by Al Sweigart al@inventwithpython.com Two players take turns dropping tiles into one of seven columns, trying to make four in a row horizontally, vertically, or diagonally. 1234567 +-------+ |.......| |.......| |.......| |.......| |.......| |.......| +-------+ Player X, enter 1 to 7 or QUIT: > 1 1234567 +-------+ |.......| |.......| |.......| |.......| |.......| |X......| +-------+ Player O, enter 1 to 7 or QUIT: --snip-- Player O, enter 1 to 7 or QUIT: > 4 1234567 +-------+ |.......| |.......| |...O...| |X.OO...| |X.XO...| |XOXO..X| +-------+ Player O has won! حاول اكتشاف العديد من الاستراتيجيات الدقيقة التي يمكنك استخدامها للحصول على أربعة أحجار متتالية بينما تمنع خصمك من فعل الشيء نفسه. الشيفرة المصدرية افتح ملفًا جديدًا في المحرر أو البيئة التطويرية IDE، وأدخل الشيفرة التالية، واحفظ الملف باسم "fourinarow.py": """Four-in-a-Row, by Al Sweigart al@inventwithpython.com A tile-dropping game to get four-in-a-row, similar to Connect Four.""" import sys # الثوابت المستخدمة لعرض اللوحة EMPTY_SPACE = "." # النقطة أسهل للعدّ والرؤية من المسافة PLAYER_X = "X" PLAYER_O = "O" # ‎‫ملاحظة: عدّل قيمتي BOARD_TEMPLATE و COLUMN_LAVELS إذا تغيّر BOARD_WIDTH BOARD_WIDTH = 7 BOARD_HEIGHT = 6 COLUMN_LABELS = ("1", "2", "3", "4", "5", "6", "7") assert len(COLUMN_LABELS) == BOARD_WIDTH # قالب السلسلة النصية الذي يُستخدم لطباعة اللوحة BOARD_TEMPLATE = """ 1234567 +-------+ |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| +-------+""" def main()‎: """Runs a single game of Four-in-a-Row.""" print( """Four-in-a-Row, by Al Sweigart al@inventwithpython.com Two players take turns dropping tiles into one of seven columns, trying to make Four-in-a-Row horizontally, vertically, or diagonally. """ ) # إعداد لعبة جديدة gameBoard = getNewBoard()‎ playerTurn = PLAYER_X while True: # بدء دور اللاعب # عرض اللوحة قبل الحصول على حركة اللاعب displayBoard(gameBoard) playerMove = getPlayerMove(playerTurn, gameBoard) gameBoard[playerMove]‎ = playerTurn # فحص حالة الفوز أو التعادل if isWinner(playerTurn, gameBoard): displayBoard(gameBoard) # عرض اللوحة لمرة أخيرة print("Player {} has won!".format(playerTurn)) sys.exit()‎ elif isFull(gameBoard): displayBoard(gameBoard) # عرض اللوحة لمرة أخيرة print("There is a tie!") sys.exit()‎ # تبديل الدور للاعب الآخر if playerTurn == PLAYER_X: playerTurn = PLAYER_O elif playerTurn == PLAYER_O: playerTurn = PLAYER_X def getNewBoard()‎: """Returns a dictionary that represents a Four-in-a-Row board. The keys are (columnIndex, rowIndex) tuples of two integers, and the values are one of the "X", "O" or "." (empty space) strings.""" board = {} for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): board[(columnIndex, rowIndex)]‎ = EMPTY_SPACE return board def displayBoard(board): """Display the board and its tiles on the screen.""" # ‫تحضير قائمة لتمريرها إلى تابع format()‎ لقالب اللوحة # تحتوي القائمة على خلايا اللوحة بما في ذلك المسافات الفارغة # من اليسار إلى اليمين ومن الأعلى للأسفل tileChars = []‎ for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): tileChars.append(board[(columnIndex, rowIndex)]‎) # عرض اللوحة print(BOARD_TEMPLATE.format(*tileChars)) def getPlayerMove(playerTile, board): """Let a player select a column on the board to drop a tile into. Returns a tuple of the (column, row) that the tile falls into.""" while True: # استمر بسؤال اللاعب إلى أن يُدخل حركة صالحة print(f"Player {playerTile}, enter 1 to {BOARD_WIDTH} or QUIT:") response = input("> ").upper()‎.strip()‎ if response == "QUIT": print("Thanks for playing!") sys.exit()‎ if response not in COLUMN_LABELS: print(f"Enter a number from 1 to {BOARD_WIDTH}.") continue # اطلب حركة من اللاعب مجددًا columnIndex = int(response) - 1 # نطرح واحد للحصول على فهرس يبدأ من الصفر # إذا كان العمود مليئًا، نطلب من اللاعب حركة مجددًا if board[(columnIndex, 0)]‎ != EMPTY_SPACE: print("That column is full, select another one.") continue # اطلب حركة من اللاعب مجددًا # البدء من الأسفل واختيار أول خلية فارغة for rowIndex in range(BOARD_HEIGHT - 1, -1, -1): if board[(columnIndex, rowIndex)]‎ == EMPTY_SPACE: return (columnIndex, rowIndex) def isFull(board): """Returns True if the `board` has no empty spaces, otherwise returns False.""" for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): if board[(columnIndex, rowIndex)]‎ == EMPTY_SPACE: return False # أعد‫ False إذا عُثر على مسافة فارغة return True # في حال كانت جميع الخلايا ممتلئة def isWinner(playerTile, board): """Returns True if `playerTile` has four tiles in a row on `board`, otherwise returns False.""" # تفقّد اللوحة بكاملها بحثًا عن حالة فوز for columnIndex in range(BOARD_WIDTH - 3): for rowIndex in range(BOARD_HEIGHT): # التحقق من حالة الفوز بالذهاب لليمين tile1 = board[(columnIndex, rowIndex)]‎ tile2 = board[(columnIndex + 1, rowIndex)]‎ tile3 = board[(columnIndex + 2, rowIndex)]‎ tile4 = board[(columnIndex + 3, rowIndex)]‎ if tile1 == tile2 == tile3 == tile4 == playerTile: return True for columnIndex in range(BOARD_WIDTH): for rowIndex in range(BOARD_HEIGHT - 3): # التحقق من حالة فوز بالذهاب للأسفل tile1 = board[(columnIndex, rowIndex)]‎ tile2 = board[(columnIndex, rowIndex + 1)]‎ tile3 = board[(columnIndex, rowIndex + 2)]‎ tile4 = board[(columnIndex, rowIndex + 3)]‎ if tile1 == tile2 == tile3 == tile4 == playerTile: return True for columnIndex in range(BOARD_WIDTH - 3): for rowIndex in range(BOARD_HEIGHT - 3): # التحقق من حالة فوز بالذهاب قطريًا إلى اليمين والأسفل tile1 = board[(columnIndex, rowIndex)]‎ tile2 = board[(columnIndex + 1, rowIndex + 1)]‎ tile3 = board[(columnIndex + 2, rowIndex + 2)]‎ tile4 = board[(columnIndex + 3, rowIndex + 3)]‎ if tile1 == tile2 == tile3 == tile4 == playerTile: return True # التحقق من حالة فوز بالذهاب قطريًا إلى اليسار والأسفل tile1 = board[(columnIndex + 3, rowIndex)]‎ tile2 = board[(columnIndex + 2, rowIndex + 1)]‎ tile3 = board[(columnIndex + 1, rowIndex + 2)]‎ tile4 = board[(columnIndex, rowIndex + 3)]‎ if tile1 == tile2 == tile3 == tile4 == playerTile: return True return False # شغّل اللعبة إذا نُفّذ البرنامج بدلًا من استيراده if __name__ == "__main__": main()‎ شغّل البرنامج السابق والعب بعض الجولات للحصول على فكرة عما يفعله هذا البرنامج قبل قراءة شرح الشيفرة المصدرية. للتحقق من وجود أخطاء كتابية، انسخها والصقها في أداة لكشف الاختلاف عبر الإنترنت. كتابة الشيفرة لنلقي نظرةً على الشيفرة المصدرية للبرنامج، كما فعلنا مع برنامج برج هانوي سابقًا. نسّقنا مرةً أخرى الشيفرة المصدرية باستخدام منسّق السطور Black بحد 75 محرفًا للسطر. نبدأ من الجزء العلوي للبرنامج: """Four-in-a-Row, by Al Sweigart al@inventwithpython.com A tile-dropping game to get four-in-a-row, similar to Connect Four.""" import sys # Constants used for displaying the board: EMPTY_SPACE = "." # A period is easier to count than a space. PLAYER_X = "X" PLAYER_O = "O" نبدأ البرنامج بسلسلة توثيق نصية docstring واستيراد للوحدات module، وتعيين للثوابت. كما فعلنا في برنامج برج هانوي. نعرّف الثابتَين PLAYER_X و PLAYER_O بحيث نبتعد عن استخدام سلاسل "X" و "O" ضمن البرنامج، مما يسهل اكتشاف الأخطاء. على سبيل المثال، سنحصل على استثناء NameError إذا أخطأنا بكتابة اسم الثابت، مثل كتابة PLAYER_XX مما يشير فورًا إلى المشكلة، ولكن إذا ارتكبنا خطأً كتابيًا باستخدام الحرف "X"، مثل "XX" أو "Z"، فقد لا يكون الخطأ الناتج واضحًا فورًا. كما هو موضح في قسم "الأرقام السحرية" من مقال اكتشاف دلالات الأخطاء في شيفرات لغة بايثون، فإن استخدام الثوابت بدلًا من قيمة السلسلة لا يمثل الوصف فحسب، بل يوفر أيضًا تحذير مبكر لأي أخطاء كتابية في الشيفرة المصدرية. ينبغي ألا تتغير الثوابت أثناء تشغيل البرنامج، لكن يمكن للمبرمج تحديث قيمهم في الإصدارات المستقبلية من البرنامج. لهذا السبب، نقدم ملاحظةً تخبر المبرمجين بضرورة تحديث ثابتي BOARD_TEMPLATE و COLUMN_LABELS، إذا غيروا قيمة BOARD_WIDTH: # Note: Update BOARD_TEMPLATE & COLUMN_LABELS if BOARD_WIDTH is changed. BOARD_WIDTH = 7 BOARD_HEIGHT = 6 بعد ذلك، ننشئ ثابت COLUMN_LABELS: COLUMN_LABELS = ("1", "2", "3", "4", "5", "6", "7") assert len(COLUMN_LABELS) == BOARD_WIDTH سنستخدم هذا الثابت لاحقًا للتأكد من أن اللاعب يختار عمودًا صالحًا. لاحظ أنه في حالة تعيين BOARD_WIDTH على أي قيمة أخرى بخلاف 7، فسنضطر إلى إضافة تسميات labels إلى مجموعة tuple تدعى COLUMN_LABELS أو إزالتها منها. كان بإمكاننا تجنب ذلك من خلال إنشاء قيمة COLUMN_LABELS بناءً على BOARD_WIDTH بشيفرة مثل هذه: COLUMN_LABELS = tuple ([str (n) for n in range (1، BOARD_WIDTH + 1)]‎) لكن من غير المرجح أن يتغير COLUMN_LABELS في المستقبل، لأن لعبة أربع نقاط في صف واحد تربح تُلعب على لوحة 7×6، لذلك قررنا كتابة قيمة صريحة للمجموعة. بالتأكيد، تمثّل هذه الشيفرة شيفرة ذات رائحة smell code (وهي نمط شيفرة يشير إلى أخطاء محتملة)، ولكنها أكثر قابلية للقراءة من بديلها. تحذرنا تعليمة assert من تغيير BOARD_WIDTH بدون تحديث COLUMN_LABELS. كما هو الحال مع برج هانوي، يستخدم برنامج أربع في صف واحد تربح محارف آسكي ASCII لرسم لوحة اللعبة. تمثّل الأسطر التالية تعليمة إسناد واحدة بسلسلة نصية متعددة الأسطر: # The template string for displaying the board: BOARD_TEMPLATE = """ 1234567 +-------+ |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| +-------+""" تحتوي هذه السلسلة على أقواس معقوصة braces {} يحل محلها سلسلة باستخدام التابع format()‎. (ستعمل دالة displayBoard()‎، التي ستُشرح لاحقًا، على تحقيق هذا.) نظرًا لأن اللوحة تتكون من سبعة أعمدة وستة صفوف، فإننا نستخدم سبعة أزواج من القوسين {} في كل من الصفوف الستة لتمثيل كل فتحة. لاحظ أنه تمامًا مثل COLUMN_LABELS، فإننا نشفّر من الناحية الفنية اللوحة لإنشاء عدد محدد من الأعمدة والصفوف. إذا غيرنا BOARD_WIDTH أو BOARD_HEIGHT إلى أعداد صحيحة جديدة، فسنضطر أيضًا إلى تحديث السلسلة متعددة الأسطر في BOARD_TEMPLATE. كان بإمكاننا كتابة شيفرة لإنشاء BOARD_TEMPLATE استنادًا إلى الثابتين BOARD_WIDTH و BOARD_HEIGHT، مثل: BOARD_EDGE = " +" + ("-" * BOARD_WIDTH) + "+" BOARD_ROW = " |" + ("{}" * BOARD_WIDTH) + "|\n" BOARD_TEMPLATE = "\n " + "".join(COLUMN_LABELS) + "\n" + BOARD_EDGE + "\n" + (BOARD_ROW * BOARD_HEIGHT) + BOARD_EDGE لكن هذه الشيفرة غير قابلة للقراءة مثل سلسلة بسيطة متعددة الأسطر، ومن غير المرجح أن نغير حجم لوحة اللعبة على أي حال، لذلك سنستخدم السلسلة البسيطة متعددة الأسطر. نبدأ بكتابة الدالة main()‎ التي ستستدعي جميع الدوال الأخرى التي أنشأناها لهذه اللعبة: def main()‎: """Runs a single game of Four-in-a-Row.""" print( """Four-in-a-Row, by Al Sweigart al@inventwithpython.com Two players take turns dropping tiles into one of seven columns, trying to make four-in-a-row horizontally, vertically, or diagonally. """ ) # Set up a new game: gameBoard = getNewBoard()‎ playerTurn = PLAYER_X نعطي الدالة main()‎ سلسلة توثيق نصية، قابلة للعرض viewable باستخدام دالة help()‎ المضمنة. تُعِد الدالة main()‎ أيضًا لوحة اللعبة للعبة جديدة وتختار اللاعب الأول. تحتوي الدالة main()‎ حلقة لا نهائية: while True: # Run a player's turn. # Display the board and get player's move: displayBoard(gameBoard) playerMove = getPlayerMove(playerTurn, gameBoard) gameBoard[playerMove]‎ = playerTurn يمثل كل تكرار لهذه الحلقة دورًا واحدًا. أولًا، نعرض لوحة اللعبة للاعب. ثانيًا، يختار اللاعب عمودًا لإسقاط حجر فيه، وثالثًا، نُحدث بنية بيانات لوحة اللعبة. بعد ذلك، نقيم نتائج حركة اللاعب: # Check for a win or tie: if isWinner(playerTurn, gameBoard): displayBoard(gameBoard) # Display the board one last time. print("Player {} has won!".format(playerTurn)) sys.exit()‎ elif isFull(gameBoard): displayBoard(gameBoard) # Display the board one last time. print("There is a tie!") sys.exit()‎ إذا أدّت حركة اللاعب لفوزه، ستعيد الدالة isWinner()‎ القيمة True وتنتهي اللعبة؛ بينما إذا ملأ اللاعب اللوحة ولم يكن هناك فائز، ستعيد الدالة isFull()‎ القيمة True وتنتهي اللعبة. لاحظ أنه بدلًا من استدعاء sys.exit()‎، كان بإمكاننا استخدام تعليمة break بسيطة. كان من الممكن أن يتسبب هذا في انقطاع التنفيذ عن حلقة while، ولأنه لا يوجد شيفرة برمجية في الدالة main()‎ بعد هذه الحلقة، ستعود الدالة إلى استدعاء main()‎ في الجزء السفلي من البرنامج، مما يتسبب في إنهاء البرنامج، لكننا اخترنا استخدام sys.exit()‎ للتوضيح للمبرمجين الذين يقرؤون الشيفرة أن البرنامج سينتهي فورًا. إذا لم تنته اللعبة، تُعِد الأسطر التالية playerTurn للاعب الآخر: # Switch turns to other player: if playerTurn == PLAYER_X: playerTurn = PLAYER_O elif playerTurn == PLAYER_O: playerTurn = PLAYER_X لاحظ أنه كان بإمكاننا تحويل تعليمة elif إلى تعليمة else بسيطة دون شرط، لكن تذكر أن ممارسات بايثون الفُضلى تنص على "الصراحة أفضل من الضمنية explicit is better than implicit". تنص هذه الشيفرة صراحةً على أنه إذا جاء دور اللاعب O الآن، فسيكون دور اللاعب X هو التالي. ستنص الشيفرة البديلة على أنه إذا لم يكن دور اللاعب X الآن، فسيكون دور اللاعب X التالي. على الرغم من أن دوال if و else تتناسب بصورةٍ طبيعية مع الشروط المنطقية، لا تتطابق قيمتا PLAYER_X و PLAYER_O مع True وقيمة False: not PLAYER_X ليست PLAYER_O. لذلك، من المفيد أن تكون مباشرًا عند التحقق من قيمة playerTurn. بدلًا من ذلك، كان بإمكاننا تنفيذ جميع الإجراءات في سطر واحد: playerTurn = {PLAYER_X: PLAYER_O, PLAYER_O: PLAYER_X}[ playerTurn]‎ يستخدم هذا السطر خدعة القاموس المذكورة في قسم "استخدام القواميس بدلا من العبارة Switch" في مقال الطرق البايثونية في استخدام قواميس بايثون ومتغيراتها وعاملها الثلاثي، ولكن مثل العديد من الأسطر الفردية، فهي غير سهلة القراءة مقارنةً بعبارة if و elif المباشرة. بعد ذلك، نعرّف الدالة getNewBoard()‎: def getNewBoard(): """Returns a dictionary that represents a Four-in-a-Row board. The keys are (columnIndex, rowIndex) tuples of two integers, and the values are one of the "X", "O" or "." (empty space) strings.""" board = {} for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): board[(columnIndex, rowIndex)] = EMPTY_SPACE return board تُعيد هذه الدالة قاموسًا يمثل لوحة أربع نقاط في صف واحد تربح؛ إذ يحتوي هذا القاموس على مجموعات (indexIndex و rowIndex) للمفاتيح (يمثّل العمود indexIndex و rowIndex أعدادًا صحيحة) و "X" أو "O" أو "." حرف الحجر في كل مكان على اللوحة. تُخزَّن هذه السلاسل في PLAYER_X و PLAYER_O و EMPTY_SPACE على التوالي. لعبة أربع نقاط في صف واحد تربح الخاصة بنا بسيطة نوعًا ما، لذا يُعد استخدام قاموس لتمثيل لوحة اللعبة أسلوبًا مناسبًا. ومع ذلك، كان بإمكاننا استخدام نهج كائني التوجه object-oriented بدلًا من ذلك. سنتعرف على البرمجة كائنية التوجه في الفصول القادمة. تأخذ دالة displayBoard()‎ بنية بيانات لوحة اللعبة من أجل الوسيط board وتعرض اللوحة على الشاشة باستخدام ثابت BOARD_TEMPLATE: def displayBoard(board): """Display the board and its tiles on the screen.""" # Prepare a list to pass to the format() string method for the board # template. The list holds all of the board's tiles (and empty # spaces) going left to right, top to bottom: tileChars = [] تذكر أن BOARD_TEMPLATE هي سلسلة متعددة الأسطر بها عدة أزواج من الأقواس. عند استدعاء دالة format()‎ على BOARD_TEMPLATE، ستُستبدل هذه الأقواس بقيم الوسطاء الممرّرة إلى format()‎. سيحتوي المتغير tileChars على قائمة بهذه الوسطاء. نبدأ بتخصيص قائمة فارغة لها، إذ ستحل القيمة الأولى في tileChars محل الزوج الأول من الأقواس في BOARD_TEMPLATE، وستحل القيمة الثانية محل الزوج الثاني، وهكذا. نشكّل قائمةً بالقيم من قاموس board: for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): tileChars.append(board[(columnIndex, rowIndex)]‎) # Display the board: print(BOARD_TEMPLATE.format(*tileChars)) تتكرر حلقات for المتداخلة هذه على كل صف وعمود محتملين على اللوحة، لتلحقهم بالقائمة في tileChars. بمجرد الانتهاء من هذه الحلقات، نمرر القيم الموجودة في قائمة tileChars بصورةٍ مفردة إلى التابع format()‎ باستخدام محرف النجمة * في البادئة. يشرح قسم "استخدام * لإنشاء دوال مرنة" من مقال كتابة دوال فعالة في بايثون كيفية استخدام رمز النجمة للتعامل مع القيم الموجودة في قائمة مثل وسطاء دالة منفصلة، إذ تعادل الشيفرة print(*['cat', 'dog', 'rat']‎)‎ الشيفرة print('cat', 'dog', 'rat')‎. نحتاج إلى النجمة لأن التابع format()‎ يتوقع وسيطًا واحدًا لكل زوج من الأقواس، وليس وسيطًا واحدًا للقائمة الواحدة. بعد ذلك، نكتب دالة getPlayerMove()‎: def getPlayerMove(playerTile, board): """Let a player select a column on the board to drop a tile into. Returns a tuple of the (column, row) that the tile falls into.""" while True: # Keep asking player until they enter a valid move. print(f"Player {playerTile}, enter 1 to {BOARD_WIDTH} or QUIT:") response = input("> ").upper().strip() if response == "QUIT": print("Thanks for playing!") sys.exit()‎ تبدأ الدالة بحلقة لا نهائية تنتظر أن يدخل اللاعب نقلة move صحيحة. تشبه هذه الشيفرة دالة getPlayerMove()‎ في برنامج برج هانوي سابقًا. لاحظ أن استدعاء print()‎ في بداية حلقة while loop يستخدم سلسلة نصية من النوع f، لذا لا يتعين علينا تغيير الرسالة إذا حدثنا BOARD_WIDTH. نتحقق من أن رد اللاعب هو عمود صالح؛ إذا لم يكن كذلك، تنقل دالة continue التنفيذ مرةً أخرى إلى بداية الحلقة لتطلب من اللاعب نقلة صحيحة: if response not in COLUMN_LABELS: print(f"Enter a number from 1 to {BOARD_WIDTH}.") continue # Ask player again for their move. كان من الممكن كتابة شرط التحقق من صحة الإدخال هذا على شكل: not response.isdecimal()‎ or spam < 1 or spam > BOARD_WIDTH ولكن من الأسهل استخدام response not in COLUMN_LABELS. بعد ذلك، نحتاج إلى معرفة الصف الذي ستصل إليه الحجرة التي سقطت في العمود المحدد للاعب: columnIndex = int(response) - 1 # -1 for 0-based column indexes. # If the column is full, ask for a move again: if board[(columnIndex, 0)]‎ != EMPTY_SPACE: print("That column is full, select another one.") continue # Ask player again for their move. تعرض اللوحة تسميات الأعمدة من 1 إلى 7 على الشاشة، بينما تستخدم فهارس (indexIndex، rowIndex) على اللوحة الفهرسة المستندة إلى 0، لذا فهي تتراوح من 0 إلى 6. لحل هذا التناقض، نحوّل قيم السلسلة '1' إلى '7' إلى القيم الصحيحة من 0 إلى 6. تبدأ فهارس الصفوف من 0 في أعلى اللوحة وتزيد إلى 6 في أسفل اللوحة. نتحقق من الصف العلوي في العمود المحدد لمعرفة ما إذا كان مشغولًا؛ وفي حال كان مشغولًا، فهذا العمود ممتلئ تمامًا وستعيد عبارة المتابعة التنفيذ إلى بداية الحلقة لتطلب من اللاعب نقلةً أخرى؛ وإذا لم يكن العمود ممتلئًا، فسنحتاج إلى العثور على أدنى مساحة غير مشغولة لنزول الحجر: # Starting from the bottom, find the first empty space. for rowIndex in range(BOARD_HEIGHT - 1, -1, -1): if board[(columnIndex, rowIndex)]‎ == EMPTY_SPACE: return (columnIndex, rowIndex) تبدأ حلقة for من فهرس الصف السفلي، BOARD_HEIGHT - 1 أو 6، وتتحرك لأعلى حتى تعثر على أول مساحة فارغة. تُعيد الدالة بعد ذلك فهارس أدنى مساحة فارغة. في أي وقت تكون اللوحة ممتلئة، تنتهي اللعبة بالتعادل: def isFull(board): """Returns True if the `board` has no empty spaces, otherwise returns False.""" for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): if board[(columnIndex, rowIndex)] == EMPTY_SPACE: return False # Found an empty space, so return False. return True # All spaces are full. تستخدم الدالة isFull()‎ زوجًا من حلقات for المتداخلة للمرور على كل مكان على اللوحة، وإذا عثرت على مساحة فارغة واحدة، فإن اللوحة ليست ممتلئة، وبالتالي تُعيد الدالة False. إذا نجح التنفيذ في المرور عبر كلتا الحلقتين، فإن الدالة isFull()‎ لم تعثر على مساحة فارغة، لذا فإنها تعيد True. تتحقق دالة isWinner()‎ ما إذا كان اللاعب قد فاز باللعبة أم لا: def isWinner(playerTile, board): """Returns True if `playerTile` has four tiles in a row on `board`, otherwise returns False.""" # Go through the entire board, checking for four-in-a-row: for columnIndex in range(BOARD_WIDTH - 3): for rowIndex in range(BOARD_HEIGHT): # Check for four-in-a-row going across to the right: tile1 = board[(columnIndex, rowIndex)] tile2 = board[(columnIndex + 1, rowIndex)] tile3 = board[(columnIndex + 2, rowIndex)] tile4 = board[(columnIndex + 3, rowIndex)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True تُعيد هذه الدالة True إذا ظهر playerTile أربع مرات على التوالي أفقيًا أو رأسيًا أو قطريًا. لمعرفة استيفاء الشرط، يتعين علينا التحقق من كل مجموعة من أربع مسافات متجاورة على اللوحة، وسنستخدم سلسلةً من حلقات for المتداخلة لذلك. تمثل المجموعة (columnIndex, rowIndex) نقطة البداية، إذ نتحقق من نقطة البداية والمسافات الثلاثة على يمينها لسلسلة playerTile. إذا كانت مساحة البداية هي (columnIndex, rowIndex)، ستكون المسافة الموجودة على يمينها (columnIndex + 1, rowIndex)، وهكذا. سنحفظ المربعات الموجودة في هذه المساحات الأربعة في المتغيرات tile1 و tile2 و tile3 و tile4. إذا كانت كل هذه المتغيرات لها نفس قيمة playerTile، فقد وجدنا أربع نقاط في صف واحد، وتعيد الدالة isWinner()‎ القيمة True. ذكرنا سابقًا في قسم "المتغيرات ذات اللواحق الرقمية" من مقال اكتشاف دلالات الأخطاء في شيفرات لغة بايثون أن الأسماء المتغيرات ذات اللواحق الرقمية المتسلسلة (مثل tile1 إلى tile4 في هذه اللعبة) تشير غالبًا إلى شيفرة ذات رائحة code smell تشير إلى أنه يجب عليك استخدام قائمة واحدة بدلًا من ذلك، لكن في هذا السياق، لا بأس بأسماء المتغيرات هذه؛ إذ لا نحتاج إلى استبدالها بقائمة، لأن برنامج الأربع نقاط في صف واحد سيتطلب دائمًا أربعة متغيرات تحديدًا. تذكر أن رائحة الشيفرة البرمجية لا تشير بالضرورة إلى وجود مشكلة؛ وهذا يعني فقط أننا يجب أن نلقي نظرة ثانية ونتأكد أننا كتبنا الشيفرة الخاصة بنا بطريقة أكثر قابلية للقراءة. قد يؤدي استخدام القائمة إلى جعل الشيفرة أكثر تعقيدًا في هذه الحالة، ولن تضيف أي فائدة، لذلك سنلتزم باستخدام tile1 و tile2 و tile3 و tile4. نستخدم عملية مماثلة للتحقق من وجود أربع أحجار متتالية رأسيًا: for columnIndex in range(BOARD_WIDTH): for rowIndex in range(BOARD_HEIGHT - 3): # Check for four-in-a-row going down: tile1 = board[(columnIndex, rowIndex)]‎ tile2 = board[(columnIndex, rowIndex + 1)]‎ tile3 = board[(columnIndex, rowIndex + 2)]‎ tile4 = board[(columnIndex, rowIndex + 3)]‎ if tile1 == tile2 == tile3 == tile4 == playerTile: return True بعد ذلك، نتحقق من وجود أربعة أحجار متتالية قطريًا للأسفل وإلى اليمين؛ ثم نتحقق من وجود أربعة أحجار متتالية قطريًا للأسفل وإلى اليسار: for columnIndex in range(BOARD_WIDTH - 3): for rowIndex in range(BOARD_HEIGHT - 3): # Check for four-in-a-row going right-down diagonal: tile1 = board[(columnIndex, rowIndex)] tile2 = board[(columnIndex + 1, rowIndex + 1)] tile3 = board[(columnIndex + 2, rowIndex + 2)] tile4 = board[(columnIndex + 3, rowIndex + 3)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True # Check for four-in-a-row going left-down diagonal: tile1 = board[(columnIndex + 3, rowIndex)] tile2 = board[(columnIndex + 2, rowIndex + 1)] tile3 = board[(columnIndex + 1, rowIndex + 2)] tile4 = board[(columnIndex, rowIndex + 3)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True هذه الشيفرة مشابهة لعمليات التحقق الأفقية، لذلك لن نكرر الشرح هنا. إذا فشلت جميع عمليات التحقق الخاصة بالعثور على تتاليات رباعية، تعيد الدالة False للإشارة إلى أن playerTile ليس فائزًا في هذه الحالة: return False الدالة الوحيدة المتبقية هي استدعاء دالة main()‎: # If this program was run (instead of imported), run the game: if __name__ == '__main__': main()‎ نستخدم لغة بايثون الشائعة التي ستستدعي main()‎ في حال تشغيل fourinarow.py مباشرةً، ولكن ليس في حال استيراد fourinarow.py مثل وحدة module. الخلاصة تستخدم لعبة أربع نقاط في صف واحد تربح محارف آسكي ASCII لعرض تمثيل للوحة اللعبة. نعرض هذا باستخدام سلسلة متعددة الأسطر مخزنة في ثابت BOARD_TEMPLATE. تحتوي هذه السلسلة على 42 زوجًا من الأقواس {} لعرض كل مسافة على لوحة بقياس 7×6. نستخدم الأقواس بحيث يمكن لتابع السلسلة format()‎ استبدالها بالحجر الموجود في تلك المساحة. بهذه الطريقة، يصبح الأمر أكثر وضوحًا كيف تنتج سلسلة BOARD_TEMPLATE لوحة اللعبة كما تظهر على الشاشة. ترجمة -وبتصرف- لقسم من الفصل Practice Projects من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: برمجة لغز أبراج هانوي Hanoi Towers باستخدام لغة بايثون كتابة شيفرات بايثون Python: مبادئ بايثون التوجيهية العشرون وسوء استخدام الصيغة الشائع. إنشاء تطبيق ويب باستخدام إطار عمل فلاسك Flask من لغة بايثون بناء لعبة نرد بسيطة بلغة بايثون
  16. استخدمنا الماكرو مثل println!‎ سابقًا ضمن هذه السلسلة البرمجة بلغة رست، إلا أننا لم نتحدث بالكامل عما هو الماكرو وكيفية عمله، إذ تشير كلمة ماكرو إلى مجموعة من الميّزات في رست، ألا وهي الماكرو التصريحية declarative مع macro_rules!‎، إضافةً إلى ثلاثة أنواع من الماكرو الإجرائي procedural: ماكرو [derive]# مخصص يحدد شيفرة مضافة بسمة derive المستخدمة على الهياكل والمعدّدات. ماكرو شبيه بالسمة attribute، الذي يعرف سمات معينة تُستخدم على أية عنصر. ماكرو يشبه الدالة ويشابه استدعاءات الدالة ولكن يعمل على المفاتيح المحددة مثل وسائطها. سنتحدث عن كلِّ مما سبق بدوره ولكن لنتحدث أولًا عن حاجتنا للماكرو بالرغم من وجود الدوال. الفرق بين الماكرو والدوال الماكرو هو طريقة لكتابة شيفرة تكتب شيفرة أُخرى والمعروف بالبرمجة الوصفية metaprogramming، وحدثنا في الملحق "ت" عن سمة derive التي تنشئ تنفيذًا لسمات متعددة، واستخدمنا أيضًا ماكرو ‎‎‎‎‎println‎!‎‎‎ و ‎vec!‎ سابقًا. تتوسع كل هذه الماكرو لتضيف شيفرةً أكثر من الشيفرة التي كُتبت يدويًا. تفيد البرمجة الوصفية في تقليل كمية الشيفرة التي يجب كتابتها والمحافظة عليها وهو أيضًا أحد أدوار الدوال، لكن لدى الماكرو بعض القوى الإضافية غير الموجودة في الدوال. يجب أن تصرّح بصمة الدالة signature على عدد ونوع المعاملات الموجودة في الدالة، أما في حالة الماكرو فيمكن أن يأخذ عدد متغير من المعاملات، إذ يمكننا استدعاء println!("hello")‎ بوسيط واحد أو println!("hello {}", name)‎ بوسيطين. يتوسع أيضًا الماكرو قبل أن يفسر المصرف معنى الشيفرة لذا يمكن للماكرو مثلًا تنفيذ سمة على أي نوع مُعطى ولا يمكن للدالة فعل ذلك لأنها تُستدعى وقت التنفيذ وتحتاج لسمة لتُنفذ وقت التصريف. من مساوئ تنفيذ الماكرو بدلًا من الدالة هو أن تعاريف الماكرو أكثر تعقيدًا من تعاريف الدالة لأننا نكتب شيفرة رست لتكتب شيفرة رست، بالتالي تكون تعاريف الماكرو أكثر تعقيدًا للقراءة والفهم والمحافظة عليها من تعاريف الدالة. هناك فرق آخر مهم بين الماكرو والدوال هو أنه يجب تعريف الماكرو أو جلبه إلى النطاق في ملف قبل استدعائه، على عكس الدوال التي يمكنك تعريفها واستدعائها في كل وقت ومكان. الماكرو التصريحي مع macro_rules!‎ للبرمجة الوصفية العامة أكثر أنواع الماكرو استخدامًا في رست هو الماكرو التصريحي الذي يسمى أحيانًا "ماكرو بالمثال macros by example" أو "ماكرو macro_rules!‎" أو ببساطة "ماكرو". يسمح لك الماكرو التصريحي بكتابة شيء مشابه لتعبير match في رست بداخله. تعابير match -كما تحدثنا في المقال بنية match للتحكم بسير برامج لغة رست- هي هياكل تحكم تقبل تعبيرًا وتقارن القيمة الناتجة من التعبير مع النمط وبعدها تنفذ الشيفرة المرتبطة مع النمط المُطابق. يقارن الماكرو أيضًا قيمةً مع أنماط مرتبطة بشيفرة معينة، وتكون القيمة في هذه الحالة هي الشيفرة المصدرية لرست المُمَررة إلى الماكرو. تُقارن الأنماط مع هيكل الشيفرة المصدرية والشيفرة المرتبطة بكل نمط، وعند حدوث التطابق يستبدل الشيفرة المُمَررة إلى الماكرو، ويحصل كل ذلك وقت التصريف. نستخدم بنية macro_rules!‎ لتعريف الماكرو. دعنا نتحدث عن كيفية استخدام macro_rules!‎ بالنظر إلى كيفية تعريف ماكرو vec!‎، إذ تحدثنا سابقًا في المقال تخزين لائحة من القيم باستخدام الأشعة Vectors في لغة رست عن كيفية استخدام ماكرو vec!‎ من أجل إنشاء شعاع جديد بقيم معينة. ينشئ الماكرو التالي مثلًا شعاع جديد يحتوي على ثلاثة أعداد صحيحة. let v: Vec<u32> = vec![1, 2, 3]; يمكن استخدام الماكرو vec!‎ لإنشاء شعاع بعددين صحيحين أو شعاع بخمس سلاسل شرائح نصية string slice، ولا يمكننا فعل ذلك باستخدام الدوال لأننا لا نعرف عدد أو نوع القيم مسبقًا. تبين الشيفرة 28 تعريفًا مبسطًا لماكرو vec!‎. اسم الملف: src/main.rs #[macro_export] macro_rules! vec { ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec } }; } [الشيفرة 28: نسخة مبسطة من تعريف ماكرو vec!‎] ملاحظة: يتضمن التعريف الفعلي لماكرو vec!‎ في المكتبة القياسية شيفرة للحجز الصحيح للذاكرة مسبقًا، وهذه الشيفرة هي تحسين لم نضفه هنا لجعل المثال أبسط. يشير توصيف ‏[macro_export]#‏‎ إلى أن هذا الماكرو يجب أن يبقى متاحًا عندما يجري إحضار الوحدة المُصرّفة crate المعرّفة داخلها الماكرو إلى النطاق، ولا يمكن إضافة الماكرو إلى النطاق دون هذا التوصيف. عندما نبدأ بتعريف الماكرو مع macro_rules!‎ ويكون اسم الماكرو الذي نعرّفه بدون علامة التعجب، يكون الاسم في هذه الحالة vec متبوعًا بقوسين معقوصين تدل على متن تعريف الماكرو. يشابه الهيكل في متن vec!‎ الهيكل في تعبير match، إذ لدينا هنا ذراع واحد مع النمط ‎( $( $x:expr ),* )‎‏‎‎‎ متبوعةً بالعامل ‎=>‎ وكتلة الشيفرة المرتبطة في النمط، وستُرسل الكتلة المرتبطة إذا تطابق النمط. بما أن هذا هو النمط الوحيد في الماكرو، هناك طريقة وحيدة للمطابقة، وأي أنماط أُخرى ستسبب خطأ، ويكون لدى الماكرو الأكثر تعقيدًا أكثر من ذراع واحدة. تختلف الصيغة الصحيحة في تعاريف الماكرو عن صيغة النمط المذكور سابقًا في المقال صياغة أنماط التصميم الصحيحة Pattern Syntax في لغة رست لأن أنماط الماكرو تُطابق مع هيكل شيفرة رست بدلًا من القيم. لنتحدث عن ماذا تعني أقسام النمط في الشيفرة 28. لقراءة صيغة نمط ماكرو الكاملة راجع مرجع رست. استخدمنا أولًا مجموعة أقواس لتغليف كامل النمط، واستخدمنا علامة الدولار ($) للتصريح عن متغير في نظام الماكرو الذي يحتوي على شيفرة رست مطابقة للنمط، إذ توضح إشارة الدولار أن هذا متغير ماكرو وليس متغير رست عادي. تأتي بعد ذلك مجموعةٌ من الأقواس التي تلتقط القيم التي تطابق النمط داخل القوسين لاستخدامها في الشيفرة المُستبدلة. توجد ‎$x:expr داخل ‎$()‎‎، التي تطابق أي تعبير رست وتعطي التعبير الاسم ‎$x. تشير الفاصلة التي تلي ‎‎‎$()‎ أنه يمكن أن يظهر هناك محرف فاصلة بعد الشيفرة الذي يطابق الشيفرة في ‎$()‎، وتشير * إلى أن هناك نمط يطابق صفر أو أكثر مما يسبق *. عندما نستدعي هذا الماكرو باستخدام vec![1, 2, 3];‎، يُطابق النمط ‎$x‎ ثلاث مرات مع التعابير الثلاث 1 و 2 و 3. لننظر إلى النمط الموجود في متن الشيفرة المرتبطة مع هذا الذراع، إذ تُنشَئ temp_vec.push()‎ داخل ‎$()*‎ لكل جزء يطابق ‎$()‎ في النمط صفر مرة أو أكثر اعتمادًا على كم مرة طابق النمط. تُبَدل ‎$‎x مع كل جزء مطابق، وعندما نستدعي الماكرو باستخدام vec![1, 2, ‎3];‎، ستكون الشيفرة المُنشأة التي تستبدل هذا الماكرو على النحو التالي: { let mut temp_vec = Vec::new(); temp_vec.push(1); temp_vec.push(2); temp_vec.push(3); temp_vec } عرّفنا الماكرو الذي يستطيع أن يأخذ أي عدد من الوسطاء من أي نوع ويستطيع إنشاء شيفرة لإنشاء شعاع يحتوي العناصر المحددة. لتعرف أكثر عن كيفية كتابة الماكرو، راجع وثائق ومصادر أُخرى على الشبكة مثل "الكتاب الصغير لماكرو رست The Little Book of Rust Macros" الذي بدأ فيه دانيل كيب Daniel Keep وتابعه لوكاس ويرث Lukas Wirth. الماكرو الإجرائي لإنشاء شيفرة من السمات الشكل الثاني من الماكرو هو الماكرو الإجرائي الذي يعمل أكثر مثل دالة (وهي نوع من الإجراءات). يقبل الماكرو الإجرائي بعض الشيفرة مثل دخل ويعمل على الشيفرة ويُنتج بعض الشيفرة مثل خرج بدلًا من مطابقة الأنماط وتبديل الشيفرة بشيفرة أُخرى كما يعمل الماكرو التصريحي. أنواع الماكرو الإجرائي الثلاث، هي: مشتقة مخصصة custom derive، أو مشابهة للسمة attribute-like، أو مشابهة للدالة function-like وتعمل كلها بطريقة مشابهة. عند إنشاء ماكرو إجرائي، يجب أن يبقى التعريف داخل الوحدة المصرّفة الخاصة به بنوع وحدة مصرّفة خاص، وذلك لأسباب تقنية معقدة نأمل أن نتخلص من وجودها مستقبلًا، تبين الشيفرة 29 كيفية تعريف الماكرو الإجرائي، إذ أن some_attribute هو عنصر مؤقت لاستخدام نوع ماكرو معين. اسم الملف: src/lib.rs use proc_macro; #[some_attribute] pub fn some_name(input: TokenStream) -> TokenStream { } [الشيفرة 29: مثال لتعريف ماكرو إجرائي] تأخذ الدالة التي تعرّف الماكرو الإجرائي TokenStream مثل دخل وتنتج TokenStream في الخرج. يُعرّف نوع TokenStream بالوحدة المصرّفة proc_macro المتضمنة في رست وتمثّل سلسلة من المفاتيح. هذا هو صلب الماكرو: تكون الشيفرة المصدرية التي يعمل فيها الماكرو هي الدخل TokenStream والشيفرة التي ينتجها الماكرو هي الخرج TokenStream. لدى الدالة سمة مرتبطة بها تحدد أي نوع من الماكرو الإجرائي يجب أن نُنشئ، ويمكن أيضًا الحصول على العديد من الماكرو الإجرائي في الوحدة المصرّفة ذاتها. لنتحدث عن الأشكال المختلفة من الماكرو الإجرائي. سنبدأ بالماكرو المشتق الخاص ونفسر الاختلافات البسيطة التي تجعل باقي الأشكال مختلفة. كيفية كتابة ماكرو derive مخصص لننشئ وحدة مصرّفة اسمها hello_macro التي تعرف سمةً اسمها HelloMacro مع دالة مرتبطة associated اسمها hello_macro، وبدلًا من إجبار المستخدمين على تنفيذ السمة HelloMacro لكل من أنواعهم، سنؤمن ماكرو إجرائي لكي يتمكن المستخدمين من توصيف نوعهم باستخدام ‏‏‏[derive(HelloMacro)‎]‏‏# للحصول على تنفيذ افتراضي للدالة hello_macro. سيطبع النفيذ الافتراضي: Hello, Macro! My name is TypeName!‎ إذ أن TypeName هو اسم النوع المُعرّفة عليه السمة، بمعنى آخر سنكتب وحدة مصرّفة تسمح لمبرمج آخر بكتابة الشيفرة باستخدام حزمتنا المصرفة كما في الشيفرة 30. اسم الملف:src/main.rs use hello_macro::HelloMacro; use hello_macro_derive::HelloMacro; #[derive(HelloMacro)] struct Pancakes; fn main() { Pancakes::hello_macro(); } [الشيفرة 30: الشيفرة التي يستطيع مستخدم الوحدة المصرفة فيها الكتابة عند استخدام الماكرو الإجرائي الخاص بنا] ستطبع الشيفرة عندما تنتهي ما يلي: Hello, Macro! My name is Pancakes!‎ الخطوة الأولى هي إنشاء وحدة مكتبة مصرّفة على النحو التالي: $ cargo new hello_macro --lib بعدها نعرّف سمة HelloMacro والدّالة التابعة لها. اسم الملف: src/lib.rs pub trait HelloMacro { fn hello_macro(); } لدينا السمة ودوالها، ويستطيع هنا مستخدم الوحدة المصرّفة تنفيذ السمة للحصول على الوظيفة المرغوبة على النحو التالي: use hello_macro::HelloMacro; struct Pancakes; impl HelloMacro for Pancakes { fn hello_macro() { println!("Hello, Macro! My name is Pancakes!"); } } fn main() { Pancakes::hello_macro(); } ولكن سيحتاج المستخدم لكتابة كتلة التنفيذ لكل نوع يرغب باستخدامه مع hello_macro، ونريد إعفائهم من ذلك. إضافةً إلى ذلك، لا نستطيع أن نؤمّن للتابع hello_macro التنفيذ الافتراضي الذي سيطبع اسم نوع السمة المُطبقة عليه، إذ ليس لدى رست قدرة على الفهم لذا لا تستطيع البحث عن اسم النوع وقت التنفيذ، وفي هذه الحالة نحن بحاجة لماكرو لإنشاء شيفرة وقت التنفيذ. الخطوة التالية هي تعريف الماكرو الإجرائي. يحتاج الماكرو الإجرائي حتى الآن إلى وحدة مصرّفة خاصة به، ربما سيُرفع هذا التقييد بالنهاية. يأتي اصطلاح الوحدات المصرّفة الهيكلية والوحدات المصرّفة للماكرو على النحو التالي: يسمى الماكرو الإجرائي الخاص المشتق foo_derive لاسم موحدة مصرفة foo. لنبدأ بإنشاء وحدة مصرّفة جديدة اسمها hello_macro_derive داخل المشروع hello_macro. $ cargo new hello_macro_derive --lib الوحدتان المصرّفتان مرتبطتان جدًا، لذلك سننشئ وحدةً مصرّفةً للماكرو الإجرائي داخل مجلد الوحدة المصرّفة hello_macro. يجب علينا تغيير تنفيذ الماكرو الإجرائي في hello_macro_derive إذا غيرنا تعريف السمة في hello_macro أيضًا. تحتاج الوحدتان المصرّفتان أن تُنشَرا بصورةٍ منفصلة ويجب أن يضيف مستخدمو هاتين الوحدتين المصرّفتين مثل اعتماديتين dependencies وجلبهما إلى النطاق. يمكن -بدلًا من ذلك- جعل الحزمة المصرّفة hello_macro تستخدم hello_macro_derive مثل اعتمادية وتعيد تصدير شيفرة الماكرو الإجرائي ولكن الطريقة التي بنينا فيها المشروع تسمح للمبرمجين استخدام hello_macro حتى لو كانوا لا يرغبون باستخدام وظيفة derive. يجب علينا التصريح عن الوحدة المصرفة hello_macro_derive مثل وحدة مصرفة لماكرو إجرائي ونحتاج أيضًا إلى وظائف من الوحدات المصرّفة syn و quote كما سنرى بعد قليل لذا سنحتاج لإضافتهم كاعتماديات. أضِف التالي إلى ملف Cargo.toml من أجل hello_macro_derive: اسم الملف: hello_macro_derive/Cargo.toml [lib] proc-macro = true [dependencies] syn = "1.0" quote = "1.0" لنبدأ بتعريف الماكرو الإجرائي. ضع الشيفرة 31 في ملف src/lib.rs من أجل الوحدة المصرّفة hello_macro-derive. لاحظ أن الشيفرة لن تصرّف حتى نضيف التعريف لدالة impl_hello_macro. اسم الملف: hello_macro_derive/src/lib.rs use proc_macro::TokenStream; use quote::quote; use syn; #[proc_macro_derive(HelloMacro)] pub fn hello_macro_derive(input: TokenStream) -> TokenStream { // إنشاء تمثيل لشيفرة رست مثل شجرة صيغة يمكننا التلاعب بها let ast = syn::parse(input).unwrap(); // بناء تنفيذ السمة impl_hello_macro(&ast) } [الشيفرة 31: الشيفرة التي تتطلبها معظم الوحدات المصرّفة للماكرو الإجرائي لكي تعالج شيفرة رست] لاحظ أننا قسّمنا الشيفرة إلى دالة hello_macro_derive المسؤولة عن تحليل TokenStream، ودالة Impl_hello_macro المسؤولة عن تحويل شجرة الصيغة syntax tree التي تجعل كاتبة الماكرو الإجرائي لتكون أكثر ملائمة. ستكون الشيفرة في الدالة الخارجية (في هذه الحالة hello_macro_derive) هي نفسها لمعظم الوحدات المصرّفة للماكرو الإجرائي الذي تراه أو تنشئه، وستكون الشيفرة التي تحددها في محتوى الدالة الداخلية (في هذه الحالة impl_hello_macro) مختلفة اعتمادًا على غرض الماكرو الإجرائي. أضفنا ثلاث وحدات مصرّفة هي proc_macro و syn و quote. لا نحتاج لإضافة الوحدة المصرفة proc_macro إلى الاعتماديات في Cargo.toml لأنها تأتي مع رست، وهذه الوحدة المصرفة هي واجهة برمجة التطبيق للمصرف التي تسمح بقراءة وتعديل شيفرة رست من شيفرتنا. تحلّل الوحدة المصرّفة syn شيفرة رست من سلسلة نصية إلى هيكل بيانات يمكننا إجراء عمليات عليه. تحوّل الوحدة المصرّفة quote هيكل بيانات syn إلى شيفرة رست. تسهّل هذه الوحدات المصرّفة تحليل أي نوع من شيفرة رست يمكن أن نعمل عليه. تُعد كتابة محلل parser كامل لرست أمرًا صعبًا. تُستدعى دالة hello_macro_derive عندما يحدد مستخدم مكتبتنا [derive(HelloMacro)‎]# على نوع، وهذا ممكن لأننا وصفّنا دالة hello_macro_dervie باستخدام proc_macro_dervie وحددنا اسم HelloMacro الذي يطابق اسم سِمتنا، وهذا هو الاصطلاح الذي يتبعه معظم الماكرو الإجرائي. تحوّل دالة hello_macro_derive أولًا input من TokenStream إلى هيكل بيانات يمكن أن نفسره ونجري عمليات عليه. هنا يأتي دور syn. تأخذ دالة parse في syn القيمة TokenStream وتُعيد هيكل DeriveInput يمثّل شيفرة رست المحلّلة. تظهر الشيفرة 32 الأجزاء المهمة من هيكل DeriveInput التي نحصل عليها من تحليل السلسلة النصية struct Pancakes;‎. DeriveInput { // --snip-- ident: Ident { ident: "Pancakes", span: #0 bytes(95..103) }, data: Struct( DataStruct { struct_token: Struct, fields: Unit, semi_token: Some( Semi ) } ) } [الشيفرة 32: نسخة DeriveInput التي نحصل عليها من تحليل الشيفرة التي فيها سمة الماكرو في الشيفرة 30] تظهر حقول هذا الهيكل بأن شيفرة رست التي حللناها هي هيكل وحدة مع ident (اختصارًا للمعرّف، أي الاسم) الخاصة بالاسم Pancakes. هناك حقول أخرى في هذا الهيكل لوصف كل أنواع شيفرة رست. راجع وثائق syn من أجل DeriveInput لمعلومات أكثر. سنعرِّف قريبًا دالة impl_hello_macro، التي سنبني فيها شيفرة رست الجديدة التي نريد ضمها، لكن قبل ذلك لاحظ أن الخرج من الماكرو المشتق الخاص بنا هو أيضًا TokenStream، إذ تُضاف TokenStream المُعادة إلى الشيفرة التي كتبها مستخدمو حزمتنا المصرّفة، لذلك سيحصلون عند تصريف الوحدة المصرّفة على وظائف إضافية قدمناها في TokenStream المعدلة. ربما لاحظت أننا استدعينا unwrap لتجعل الدالة hello_macro_derive تهلع إذا فشل استدعاء الدالة syn::parse. يجب أن يهلع الماكرو الإجرائي على الأخطاء، لأنه يجب أن تعيد الدالة proc_macro_derive الـقيمة TokenStream بدلًا من Result لتتوافق مع واجهة برمجة التطبيقات للماكرو الإجرائي. بسّطنا هذا المثال باستخدام unwrap، إلا أنه يجب تأمين رسالة خطأ محددة أكثر في شيفرة الإنتاج باستخدام panic!‎ أو expect. الآن لدينا الشيفرة لتحويل شيفرة رست الموصّفة من TokenStream إلى نسخة DeriveInput لننشئ الشيفرة التي تطبّق سمة HelloMacro على النوع الموصّف كما تظهر الشيفرة 33. اسم الملف: hello_macro_derive/src/lib.rs fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream { let name = &ast.ident; let gen = quote! { impl HelloMacro for #name { fn hello_macro() { println!("Hello, Macro! My name is {}!", stringify!(#name)); } } }; gen.into() } [الشيفرة 33: تنفيذ سمة HelloMacro باستخدام شيفرة رست المحلّلة] نحصل على نسخة هيكل Indent يحتوي على الاسم (المُعرّف) على النوع الموصّف باستخدام ast.ident. يظهر الهيكل في الشيفرة 32 أنه عندما نفّذنا دالة impl_hello_macro على الشيفرة 30 سيكون لدى ident التي نحصل عليها حقل ident مع القيمة "Pancakes"، لذلك سيحتوي المتغير name في الشيفرة 33 نسخة هيكل Ident، الذي سيكون سلسلة نصية "Pancakes" عندما يُطبع، وهو اسم الهيكل في الشيفرة 30. يسمح لنا ماكرو quote!‎ بتعريف شيفرة رست التي نريد إعادتها. يتوقع المصرف شيئًا مختلفًا عن النتيجة المباشرة لتنفيذ ماكرو quote!‎، لذا نحتاج لتحويله إلى TokenStream، وذلك عن طريق استدعاء تابع into الذي يستهلك التعبير الوسطي ويعيد القيمة من النوع TokenStream المطلوب. يؤمن ماكرو quote!‎ تقنيات قولبة templating جيدة، إذ يمكننا إدخال ‎#‎‎name ويبدّلها quote!‎ بالقيمة الموجودة في المتغير name، ويمكنك أيضًا إجراء بعض التكرارات بطريقة مشابهة لكيفية عمل الماكرو العادي. راجع توثيق الوحدة المصرّفة quote لتعريف وافي عنها. نريد أن يُنشئ الماكرو الإجرائي تنفيذًا لسمة HelloMacro للنوع الذي يريد توصيفه المستخدم، والذي نحصل عليه باستخدام ‎#‎‎name. يحتوي تنفيذ السمة دالةً واحدةً hello_macro تحتوي على الوظيفة المراد تقديمها ألا وهي طباعة Hello, ‎Macro! My name is وبعدها اسم النوع الموصَّف. الماكرو stringify!‎ المُستخدم هنا موجود داخل رست، إذ يأخذ تعبير رست مثل 1‎ + 2‎ ويحول التعبير إلى سلسلة نصية مجرّدة مثل "‎1 +2". هذا مختلف عن format!‎ و println!‎، الماكرو الذي يقيّم التعبير ويحول القيمة إلى String. هناك احتمال أن يكون الدخل ‎#‎name تعبيرًا للطباعة حرفيًا literally، لذا نستخدم stringify!‎، الذي يوفر مساحةً محجوزةً عن طريق تحويل ‎‎#‎name إلى سلسلة نصية مجرّدة وقت التصريف. الآن، يجب أن ينتهي cargo build بنجاح في كل من hello_macro و hello_macro_derive. لنربط هذه الوحدات المصرّفة مع الشيفرة في الشيفرة 30 لنرى كيفية عمل الماكرو الإجرائي. أنشئ مشروعًا ثنائيًا جديدًا في مجلد المشاريع باستخدام cargo new pancakes. نحتاج لإضافة hello_macro و hello_macro_derive مثل اعتماديات في ملف Cargo.toml الخاص بالوحدة المصرّفة pancakes. إذا نشرت النسخ الخاصة بك من hello_macro و hello_macro_derive إلى crates.io فستكون اعتماديات عادية، وإذا لم يكونوا كذلك فبإمكانك تحديدها مثل اعتماديات path على النحو التالي: hello_macro = { path = "../hello_macro" } hello_macro_derive = { path = "../hello_macro/hello_macro_derive" } ضع الشيفرة 30 في الملف src/main.rs ونفذ cargo run يجب أن تطبع Hello, Macro! My name is Pancakes!‎. كان تنفيذ سمة HelloMacro من الماكرو الإجرائي متضمنًا دون أن تحتاج الوحدة المصرفة pancakes أن تنفّذه. أضاف [‎‎derive(HelloMac‎ro)‎]# تنفيذ السمة. سنتحدث تاليًا عن الاختلافات بين الأنواع الأُخرى من الماكرو الإجرائي من الماكرو المشتق الخاص. الماكرو الشبيه بالسمة يشابه الماكرو الشبيه بالسمة الماكرو المشتق الخاص لكن بدلًا من إنشاء شيفرة لسمة derive يسمح لك بإنشاء سمات جديدة وهي أيضًا أكثر مرونة، تعمل derive فقط مع الهياكل والـتعدادات enums، يمكن أن تطبق السمات attributes على عناصر أُخرى أيضًا مثل الدوال. فيما يلي مثال عن استخدام الماكرو الشبيه بالسمة: لنقل أن لديك سمة اسمها route توصّف الدوال عند استخدام إطار عمل تطبيق ويب: #[route(GET, "/")] fn index() { تُعرَّف سمة ‏‏[route]# بإطار العمل مثل ماكرو إجرائي. ستكون بصمة دالة تعريف الماكرو على النحو التالي: #[proc_macro_attribute] pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { لدينا هنا معاملان من النوع TokenStream، الأول هو من أجل محتوى السمة (جزء GET, "/"‎)، والثاني هو لمتن العنصر الذي ترتبط به السمة والذي هو fn index‎() {}‎ في هذه الحالة والباقي هو متن الدالة. عدا عن ذلك، تعمل الماكرو الشبيهة بالسمة بنفس طريقة الماكرو المشتق الخاص عن طريق إنشاء وحدة مصرفة مع نوع الوحدة المصرّفة proc-macro وتنفذ الدالة التي تنشئ الشيفرة المرغوبة. الماكرو الشبيه بالدالة يعرّف الماكرو الشبيه بالدالة الماكرو ليشبه استدعاءات الدوال، وعلى نحوٍ مشابه لماكرو macro_rules!‎، فهي أكثر مرونة من الدوال؛ إذ يستطيع الماكرو أخذ عدد غير معروف من الوسطاء، ولكن يمكن أن يعرّف ماكرو macro_rules!‎ فقط باستخدام صيغة تشبه المطابقة التي تحدثنا عنها سابقًا في قسم "الماكرو التصريحي مع macro_rules!‎ للبرمجة الوصفية العامة". يأخذ الماكرو الشبيه بالدالة معامل TokenStream ويعدل تعريفها القيمة TokenStream باستخدام شيفرة رست كما يفعل الماكرو الإجرائي السابق. إليك مثالًا عن ماكرو شبيه بالدالة هو ماكرو sql!‎ التي يمكن استدعاؤه على النحو التالي: #[proc_macro_attribute] pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { يحلل هذا الماكرو تعليمة SQL داخله ويتحقق إذا كانت صياغتها صحيحة، وهذه المعالجة أعقد مما يستطيع macro_rules!‎ معالجته ويكون تعريف ماكرو sql!‎ على النحو التالي: #[proc_macro] pub fn sql(input: TokenStream) -> TokenStream { يشابه التعريف بصمة الماكرو المشتق الخاص، إذ أخذنا المفاتيح التي داخل القوسين وأعدنا الشيفرة التي نريد إنشاءها. ترجمة -وبتصرف- لقسم من الفصل Advanced Features من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: الأنواع والدوال المتقدمة في لغة رست الأنماط Patterns واستخداماتها وقابليتها للدحض Refutability في لغة رست الماكرو Macro والمعالج المسبق Preprocessor في لغة سي
  17. يستخدم لغز أبراج هانوي Hanoi towers مكدسًا stack من الأقراص ذات أحجام مختلفة، وتحتوي هذه الأقراص على ثقوب في مراكزها، لذا يمكنك وضعها على أحد الأعمدة الثلاثة (الشكل 1). لحل اللغز، يجب على اللاعب نقل مجموعة الأقراص إلى أحد القطبين الآخرين. هناك ثلاثة قيود، هي: يمكن للاعب تحريك قرص واحد فقط في كل مرة. يمكن للاعب تحريك الأقراص من وإلى قمة البرج فقط. لا يمكن للاعب أبدًا وضع قرص أكبر فوق قرص أصغر. [الشكل 1: لعبة أبراج هانوي] حل هذا اللغز هو مشكلة شائعة في علوم الحاسوب ويُستخدم لتدريس الخوارزميات التعاودية recursive algorithms. لن يحل برنامجنا هذا اللغز، بل سيقدِّم اللغز للاعب بشري لحلها. يمكنك الاطلاع على معلومات إضافية حول خوارزمية أبراج هانوي والمتوفرة على موسوعة حسوب. خرج برنامج أبراج هانوي يعرض برنامج أبراج هانوي الأبراج على شكل محارف آسكي ASCII باستخدام أحرف نصية لتمثيل الأقراص. قد يبدو هذا الأمر بدائيًا مقارنةً بالتطبيقات الحديثة، لكنه يبقي التطبيق بسيطًا، لأننا نحتاج فقط إلى استدعاءات print()‎‎ و input()‎‎ للتفاعل مع المستخدم. عند تشغيل البرنامج، سيبدو الخرج مشابهًا لما يلي: THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com Move the tower of disks, one disk at a time, to another tower. Larger disks cannot rest on top of a smaller disk. More info at https://en.wikipedia.org/wiki/Tower_of_Hanoi || || || @_1@ || || @@_2@@ || || @@@_3@@@ || || @@@@_4@@@@ || || @@@@@_5@@@@@ || || A B C Enter the letters of "from" and "to" towers, or QUIT. (e.g., AB to move a disk from tower A to tower B.) > AC || || || || || || @@_2@@ || || @@@_3@@@ || || @@@@_4@@@@ || || @@@@@_5@@@@@ || @_1@ A B C Enter the letters of "from" and "to" towers, or QUIT. (e.g., AB to move a disk from tower A to tower B.) --snip-- || || || || || @_1@ || || @@_2@@ || || @@@_3@@@ || || @@@@_4@@@@ || || @@@@@_5@@@@@ A B C You have solved the puzzle! Well done! إذا كان عدد الأقراص n، يستغرق الأمر ما لا يقل عن 2n - 1 حركة لحل أبراج هانوي. يتطلب هذا البرج المكون من خمسة أقراص 31 خطوة كما يلي: AC, AB, CB, AC, BA, BC, AC, AB, CB, CA, BA, CB, AC, AB, CB, AC, BA, BC, AC, BA, CB, CA, BA, BC, AC, AB, CB, AC, BA, BC, AC. إذا كنت تريد حل تحدٍ أكبر بنفسك، فيمكنك زيادة متغير TOTAL_DISKS في البرنامج من 5 إلى 6. الشيفرة المصدرية افتح ملفًا جديدًا في المحرر أو البيئة التطويرية IDE، وأدخل الشيفرة التالية، ثم احفظ الملف باسم towerofhanoi.py: """THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com A stack-moving puzzle game.""" import copy import sys TOTAL_DISKS = 5 # إضافة المزيد من الأقراص يجعل اللعبة أصعب # البدء بجميع الأقراص على البرج‫ A SOLVED_TOWER = list(range(TOTAL_DISKS, 0, -1)) def main()‎: """تشغيل لعبة أبراج هانوي واحدة""" print( """THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com Move the tower of disks, one disk at a time, to another tower. Larger disks cannot rest on top of a smaller disk. More info at https://wiki.hsoub.com/Algorithms/Towers_of_Hanoi """ ) """ ‫يحتوي قاموس الأبراج على المفاتيح A و B و C وقيم كل من المفتاح هي قائمة تمثّل الأقراص الموجودة على البرج. تحتوي القائمة على أعداد صحيحة تمثل الأقراص التي تكون من أحجام مختلفة، وبداية القائمة هي أسفل البرج. في حال لعبة تحتوي على 5 أقراص، تمثّل القائمة [5,4,3,2,1] البرج المكتمل بينما تمثل القائمة [] برجًا لا يحتوي على أقراص. القائمة [1,3] تحتوي على قرص كبير في الأعلى وقرص صغير بالأسفل، وبالتالي فهي حالة غير صالحة. القائمة [3,1] هي حالة صالحة بما أن القرص الصغير هو أعلى القرص الكبير‫ """ towers = {"A": copy.copy(SOLVED_TOWER), "B": []‎, "C": []‎} while True: # حلقة لدور واحد لكل تكرار من هذه الحلقة # اعرض الأقراص والأبراج displayTowers(towers) # اطلب من المستخدم أن يُدخل حركة fromTower, toTower = getPlayerMove(towers) # ‫حرّك القرص الكبير من البرج fromTower إلى البرج toTower disk = towers[fromTower]‎.pop()‎ towers[toTower]‎.append(disk) # تحقّق إذا حلّ المستخدم اللعبة if SOLVED_TOWER in (towers["B"]‎, towers["C"]‎): displayTowers(towers) # اعرض الأبراج مرةً أخيرة print("You have solved the puzzle! Well done!") sys.exit()‎ def getPlayerMove(towers): ‫ """‫اطلب من المستخدم أن يُدخل حركة، وأعد القيمتين fromTower وtoTower""" while True: # استمرّ بطلب إدخال حركة من المستخدم إلى أن يُدخل حركة صالحة print('Enter the letters of "from" and "to" towers, or QUIT.') print("(e.g., AB to move a disk from tower A to tower B.)") print()‎ response = input("> ").upper()‎.strip()‎ if response == "QUIT": print("Thanks for playing!") sys.exit()‎ # تأكّد أن المستخدم أدخل أحرف لأبراج موجودة if response not in ("AB", "AC", "BA", "BC", "CA", "CB"): print("Enter one of AB, AC, BA, BC, CA, or CB.") continue # اطلب حركة المستخدم مجددًا # استخدم أسماء متغيرات معبّرة fromTower, toTower = response[0]‎, response[1]‎ if len(towers[fromTower]‎) == 0: # ‫ لا يجب أن يكون برج "from" فارغًا print("You selected a tower with no disks.") continue # اطلب حركة من المستخدم مجددًا elif len(towers[toTower]‎) == 0: # يمكن لأي قرص أن يُحرّك إلى برج فارغ return fromTower, toTower elif towers[toTower]‎[-1]‎ < towers[fromTower]‎[-1]‎: print("Can't put larger disks on top of smaller ones.") continue # اطلب حركة المستخدم مجددًا else: # حالة حركة صالحة؛ أعِد الأبراج المختارة return fromTower, toTower def displayTowers(towers): """اطبع الأبراج الثلاث مع أقراصها""" # اطبع الأبراج الثلاثة for level in range(TOTAL_DISKS, -1, -1): for tower in (towers["A"]‎, towers["B"]‎, towers["C"]‎): if level >= len(tower): displayDisk(0) # اعرض العمود الفارغ دون أقراص else: displayDisk(tower[level]‎) # اعرض القرص print()‎ # اعرض تسميات الأبراج emptySpace = " " * (TOTAL_DISKS) print("{0} A{0}{0} B{0}{0} C\n".format(emptySpace)) def displayDisk(width): """أظهِر القرص مع العرض المحدّد، العرض بقيمة 0 يعني عدم وجود قرص""" emptySpace = " " * (TOTAL_DISKS - width) if width == 0: # اطبع قسم العمود الذي لا يحتوي على قرص print(f"{emptySpace}||{emptySpace}", end="") else: # اطبع القرص disk = "@" * width numLabel = str(width).rjust(2, "_") print(f"{emptySpace}{disk}{numLabel}{disk}{emptySpace}", end="") # اذا نُفّذ البرنامج (عوضًا عن استيراده)، ابدأ اللعبة if __name__ == "__main__": main()‎ نفّذ هذا البرنامج والعب عدّة جولات للحصول على فكرة عما يفعله هذا البرنامج قبل قراءة شرح الشيفرة المصدرية. للتحقق من عدم وجود أخطاء كتابية، انسخها والصقها إلى أداة لكشف الاختلاف عبر الإنترنت. كتابة الشيفرة دعنا نلقي نظرةً على الشيفرة المصدرية لنرى كيف تتبع أفضل الممارسات والأنماط التي وضحناها سابقًا. نبدأ بالجزء العلوي من البرنامج: """THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com A stack-moving puzzle game.""" يبدأ البرنامج بتعليق متعدد الأسطر يعمل مثل سلسلة توثيق نصية docstring للوحدة module المسماة towerofhanoi. ستستخدم دالة help()‎‎ المضمنة هذه المعلومات لوصف الوحدة: >>> import towerofhanoi >>> help(towerofhanoi) Help on module towerofhanoi: NAME towerofhanoi DESCRIPTION THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com A stack-moving puzzle game. FUNCTIONS displayDisk(width) Display a single disk of the given width. --snip-- يمكنك إضافة المزيد من الكلمات، وحتى فقرات المعلومات، إلى سلسلة التوثيق النصية الخاصة بالوحدة إذا كنت بحاجة إلى ذلك. كتبنا هنا جزءًا صغيرًا فقط لأن البرنامج بسيط جدًا. تأتي تعليمات import بعد سلسلة توثيق الوحدة: import copy import sys يعمل منسّق السطور Black على تنسيق هذه التعليمات مثل سطور منفصلة بدلًا من سطر واحد مثل import copy, sys، وهذا يجعل إضافة أو إزالة الوحدات النمطية المستوردة أسهل عند التعامل مع أنظمة التحكم في الإصدار، مثل غيت Git، التي تتعقب التغييرات التي يجريها المبرمجون. بعد ذلك، نعرّف الثوابت constants التي سيحتاجها هذا البرنامج: TOTAL_DISKS = 5 # More disks means a more difficult puzzle. # Start with all disks on tower A: SOLVED_TOWER = list(range(TOTAL_DISKS, 0, -1)) نعرّف هذه الثوابت بالقرب من أعلى الملف لتجميعها معًا وجعلها متغيرات عامة. كتبنا أسماء الثوابت بأحرف كبيرة وبتنسيق نمط الثعبان snake_case لتمييزهم على أنهم ثوابت. يشير الثابت TOTAL_DISKS إلى عدد الأقراص التي يحتوي عليها اللغز، أما المتغير SOLVED_TOWER فهو مثال عن قائمة تحتوي على برج محلول؛ إذ تحتوي على كل قرص بحيث يكون أكبر قرص في الأسفل وأصغر قرص في الأعلى. نولّد هذه القيمة من قيمة TOTAL_DISKS، أما بالنسبة للأقراص الخمسة فهي [1, 2, 3, 4, 5]‎. لاحظ عدم وجود تلميحات حول الكتابة في هذا الملف، والسبب هو أنه يمكننا استنتاج أنواع جميع المتغيرات والمعاملات والقيم المُعادة من الشيفرة. على سبيل المثال، عيَّنا قيمة العدد الصحيح 5 للثابت TOTAL_DISKS، وبناءً على ذلك سيستنتج المدقق الكتابي، مثل Mypy، أن TOTAL_DISKS يجب أن يحتوي على أعداد صحيحة فقط. نعرّف الدالة main()‎‎ التي يستدعيها البرنامج بالقرب من أسفل الملف: def main(): """Runs a single game of The Tower of Hanoi.""" print( """THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com Move the tower of disks, one disk at a time, to another tower. Larger disks cannot rest on top of a smaller disk. More info at https://wiki.hsoub.com/Algorithms/Towers_of_Hanoi """ ) يمكن أن تحتوي الدوال على سلاسل توثيق نصية أيضًا. لاحظ سلاسل التوثيق للدالة main()‎‎ أسفل تعليمة def. يمكنك عرض هذا السلاسل عن طريق تنفيذ importofhanoi و help (towerofhanoi.main)‎ من الصدفة التفاعلية. بعد ذلك، نكتب تعليقًا يصف مفصّلًا هيكل البيانات الذي نستخدمه لتمثيل البرج، لأنه يشكّل جوهر عمل هذا البرنامج: """The towers dictionary has keys "A", "B", and "C" and values that are lists representing a tower of disks. The list contains integers representing disks of different sizes, and the start of the list is the bottom of the tower. For a game with 5 disks, the list [5, 4, 3, 2, 1]‎ represents a completed tower. The blank list []‎ represents a tower of no disks. The list [1, 3]‎ has a larger disk on top of a smaller disk and is an invalid configuration. The list [3, 1]‎ is allowed since smaller disks can go on top of larger ones.""" towers = {"A": copy.copy(SOLVED_TOWER), "B": []‎, "C": []‎} نستخدم قائمة SOLVED_TOWER مثل مكدس stack وهو أحد أبسط هياكل البيانات في تطوير البرمجيات؛ فالمكدس هو قائمة مرتبة من القيم التي تتغير فقط من خلال إضافة (تسمى أيضًا الدفع Pushing) أو إزالة (تسمى أيضًا السحب Popping) القيم من أعلى المكدس. يمثل هيكل البيانات البرج في برنامجنا. يمكننا تحويل قائمة بايثون إلى مكدس إذا استخدمنا تابع append()‎‎ للدفع وتابع pop()‎‎ للسحب، وتجنبنا تغيير القائمة بأي طريقة أخرى. سنتعامل مع نهاية القائمة على أنها أعلى المكدس. يمثل كل عدد صحيح في قائمة الأبراج قرصًا واحدًا بحجم معين. على سبيل المثال، في لعبة تحتوي على خمسة أقراص، تمثل القائمة [1, 2, 3, 4, 5]‎ مجموعةً كاملةً من الأقراص من الأكبر ( رقم 5) في الأسفل وصولًا إلى الأصغر (رقم 1) في الأعلى. لاحظ أن تعليقنا يقدم أيضًا أمثلةً على كومة برج صالحة وغير صالحة. نكتب داخل الدالة main()‎‎ حلقةً لا نهائية تُنفّذ لكل دور من جولة لعبة الألغاز الخاصة بنا: while True: # Run a single turn on each iteration of this loop. # Display the towers and disks: displayTowers(towers) # Ask the user for a move: fromTower, toTower = getPlayerMove(towers) # Move the top disk from fromTower to toTower: disk = towers[fromTower]‎.pop()‎ towers[toTower]‎.append(disk) يرى اللاعب في كل دور حالة الأبراج ومن ثمّ يحدد الحركة التالية، ثم يحدّث البرنامج بعد ذلك بنية بيانات الأبراج. أخفينا تفاصيل هذه المهام في دالتي displayTowers()‎‎ و getPlayerMove()‎‎. يسمح اسما الدالتين السابقتين الوصفيّين للدالة main()‎‎ بتقديم نظرة عامة على ما يفعله البرنامج. تتحقق الأسطر التالية مما إذا كان اللاعب قد حل اللغز من خلال مقارنة البرج الكامل في SOLVED_TOWER بالقيمتين towers["B"‎]‎‎ و towers["C"]‎‎: # Check if the user has solved the puzzle: if SOLVED_TOWER in (towers["B"]‎, towers["C"]‎): displayTowers(towers) # Display the towers one last time. print("You have solved the puzzle! Well done!") sys.exit()‎ لا نقارن القيمة مع towers["A"]‎، لأن هذا العمود يبدأ ببرج مكتمل فعلًا؛ ويحتاج اللاعب إلى تشكيل البرج على العمودين B أو C لحل اللغز. لاحظ أننا نعيد استخدام SOLVED_TOWER لإنشاء أبراج البداية والتحقق فيما إذا كان اللاعب قد حل اللغز. نظرًا لأن SOLVED_TOWER ثابت، يمكننا الوثوق في أنه سيحظى دائمًا بالقيمة التي خصصناها له في بداية الشيفرة المصدرية. الشرط الذي نستخدمه يعادل الصياغة التالية ولكنه أقصر منها: SOLVED_TOWER == towers["B"]‎ or SOLVED_TOWER == towers["C"]‎ وهي بأسلوب بايثون الذي ناقشناه سابقًا. إذا كان هذا الشرط صحيحًا، فقد حل اللاعب اللغز، وننهي البرنامج، وإلا فإننا ننفّذ تكرارًا آخر من الحلقة. تطلب دالة getPlayerMove()‎ من اللاعب نقل القرص والتحقق من صحة هذه الخطوة بحسب قواعد اللعبة: def getPlayerMove(towers): """Asks the player for a move. Returns (fromTower, toTower).""" while True: # Keep asking player until they enter a valid move. print('Enter the letters of "from" and "to" towers, or QUIT.') print("(e.g., AB to move a disk from tower A to tower B.)") print() response = input("> ").upper().strip()‎ نبدأ حلقة لا نهائية تستمر في التكرار حتى تتسبب تعليمةreturn في أن يترك التنفيذ الحلقة والدالة، أو ينهي استدعاء sys.exit()‎ البرنامج. يطلب الجزء الأول من الحلقة من اللاعب إدخال حركة جديدة من خلال تحديد البرج الذي سينتقل منه القرص "from" إلى البرج الذي سينتقل القرص إليه "to". لاحظ تعليمة ()input("> ").upper().strip التي تتلقى مدخلات لوحة المفاتيح من اللاعب، إذ تقبل (" <")input إدخال النص من اللاعب من خلال الرمز <، الذي يشير إلى أنه يجب على اللاعب إدخال شيء ما، وقد يعتقد اللاعب أن البرنامج قد توقف إذا لم يوجد هذا الرمز. نستخدم تابع upper()‎ على السلسلة المُعادة من input()‎ لكي تُعيد صيغة الأحرف الكبيرة للسلسلة، ويسمح هذا للاعب بإدخال تسميات الأبراج بأحرف كبيرة أو صغيرة، مثل 'a' أو 'A' للبرج A، ثم يُستدعى تابع strip()‎ على السلسلة الكبيرة، وإعادة السلسلة دون أي مسافات فارغة على أي من الجانبين في حال أضاف المستخدم مسافة عن طريق الخطأ عند إدخال حركته. تجعل سهولة الاستخدام هذه برنامجنا أسهل قليلًا على اللاعبين لاستخدامه. استمرارًا للدالة getPlayerMove()‎، نتحقق من الدخل الذي يدخله المستخدم: if response == "QUIT": print("Thanks for playing!") sys.exit()‎ # Make sure the user entered valid tower letters: if response not in ("AB", "AC", "BA", "BC", "CA", "CB"): print("Enter one of AB, AC, BA, BC, CA, or CB.") continue # Ask player again for their move. إذا أدخل المستخدم QUIT (في أي حالة، وحتى مع وجود مسافات في بداية السلسلة النصية أو نهايتها، بسبب استدعاءات upper()‎ و strip()‎)، ينتهي البرنامج. كان من الممكن أن نجعل getPlayerMove()‎ تُعيد 'QUIT' للإشارة إلى أنه على االلاعب استدعاء sys.exit()‎ بدلًا من أن تستدعي الدالة getPlayerMove()‎ التابع sys.exit()‎، لكن هذا من شأنه أن يعقّد القيمة المُعادة للدالة getPlayerMove()‎: إذ سيعيد ذلك إما مجموعةً من سلسلتين (لتحرُّك اللاعب) أو سلسلة واحدة 'QUIT'. الدالة التي تُعيد قيمًا من نوع بيانات واحد أسهل في الفهم من الدالة التي يمكنها إعادة قيم من العديد من الأنواع الممكنة. ناقشنا ذلك سابقًا في القسم "ينبغي على القيم المعادة أن تتضمن دوما نمط البيانات نفسه" من المقال البرمجة الوظيفية Functional Programming وتطبيقها في بايثون. من الممكن فقط تكوين ست مجموعات من الأبراج بين الأبراج الثلاثة، وعلى الرغم من أننا وفرنا القيمة في الشيفرة لجميع القيم الست بصورةٍ ثابتة في الحالة التي تتحقق من الحركة، ستكون قراءة الشيفرة أسهل بكثير من قراءة شيء مثل: len(response) != 2 or response[0]‎ not in 'ABC' or response[1]‎ not in 'ABC'or response[0]‎ == response[1]‎ في ظل هذه الظروف، يعدّ التوفير الثابت للقيم في الشيفرة هو النهج الأكثر وضوحًا. يُعد توفير القيم في الشيفرة، مثل "AB" و"AC" وقيم أخرى على أنها قيم سحرية صالحة فقط طالما أن البرنامج يحتوي على ثلاثة أعمدة ممارسةً سيئةً عمومًا، ولكن على الرغم من أننا قد نرغب في تعديل عدد الأقراص عن طريق تغيير ثابت TOTAL_DISKS، فمن المستبعد جدًا أن نضيف المزيد من الأعمدة إلى اللعبة، فلا بأس بكتابة كل خطوة ممكنة على هذا النحو. نُنشئ متغيرين جديدين fromTower و toTower مثل أسماء وصفية للبيانات، فهي لا تخدم غرضًا وظيفيًا، لكنها تجعل قراءة الشيفرة أسهل من قراءة response[0]‎‎ و response[1]‎‎: # Use more descriptive variable names: fromTower, toTower = response[0]‎, response[1]‎ بعد ذلك، نتحقق مما إذا كانت الأبراج المحددة تشكل حركةً صالحة أم لا: if len(towers[fromTower]‎) == 0: # The "from" tower cannot be an empty tower: print("You selected a tower with no disks.") continue # Ask player again for their move. elif len(towers[toTower]‎) == 0: # Any disk can be moved onto an empty "to" tower: return fromTower, toTower elif towers[toTower]‎[-1]‎ < towers[fromTower]‎[-1]‎: print("Can't put larger disks on top of smaller ones.") continue # Ask player again for their move. إذا لم تكن الحركة صالحة، ستعيد عبارة continue التنفيذ إلى بداية الحلقة، التي تطلب من اللاعب إدخال حركته مرةً أخرى. لاحظ أننا نتحقق فيما إذا كان toTower فارغًا؛ فإذا كان الأمر كذلك، فإننا نعيد fromTower, toTower للتأكيد إلى أن عملية النقل كانت صالحة، لأنه يمكنك دائمًا وضع قرص على عمود فارغ. يضمن هذان الشرطان الأولان أنه بحلول الوقت الذي يجري فيه فحص الشرط الثالث، لن تكون towers[toTower]‎ و towers[fromTower]‎ فارغةً أو تتسبب بحدوث خطأ IndexError. لقد طلبنا هذه الشروط بطريقة تمنع IndexError أو أي فحص إضافي. من المهم أن يتعامل برنامجك مع أي إدخال غير صالح من المستخدم أو حالات الخطأ المحتملة؛ فقد لا يعرف المستخدمون ماذا يدخلون، أو قد يخطئون في الكتابة، وبالمثل، قد تختفي الملفات بصورةٍ غير متوقعة، أو قد تتعطل قواعد البيانات. يجب أن تكون برامجك مرنة في مواجهة الحالات الاستثنائية، وإلا ستتعطل بصورةٍ غير متوقعة أو تتسبب في حدوث أخطاء دقيقة في وقت لاحق. إذا لم يكن أي من الشروط السابقة صحيحًا، فإن الدالة getPlayerMove()‎ تُعيد fromTower, toTower: else: # This is a valid move, so return the selected towers: return fromTower, toTower تُعيد عبارات return دائمًا قيمة واحدة في بلغة بايثون. على الرغم من أن عبارة return هذه تبدو وكأنها تُعيد قيمتين، إلا أن بايثون تُعيد فعليًا مجموعةً واحدةً من قيمتين، وهو ما يعادل return (fromTower, toTower)‎. يتجاهل مبرمجو بايثون الأقواس غالبًا في هذا السياق، إذ لا تعرّف الأقواس صفوفًا tuples كما تفعل الفواصل. لاحظ أن البرنامج يستدعي دالة getPlayerMove()‎ مرةً واحدةً فقط من الدالة main()‎. لا تنقذنا الدالة من تكرار الشيفرة، وهو الغرض الأكثر شيوعًا لاستخدامها. لا يوجد سبب يمنعنا من وضع جميع الشيفرات في getPlayerMove()‎ في الدالة main()‎، ولكن يمكننا أيضًا استخدام الدوال مثل طريقة لتنظيم الشيفرة في وحدات منفصلة، وهذه هي الطريقة التي نستخدم بها getPlayerMove()‎، إذ يؤدي ذلك إلى منع دالة main()‎ من أن تصبح طويلة جدًا وغير عملية. تعرض دالة displayTowers()‎ الأقراص الموجودة على الأبراج A و B و C في الوسيط towers: def displayTowers(towers): """Display the three towers with their disks.""" # Display the three towers: for level in range(TOTAL_DISKS, -1, -1): for tower in (towers["A"], towers["B"], towers["C"]): if level >= len(tower): displayDisk(0) # Display the bare pole with no disk. else: displayDisk(tower[level]) # Display the disk. print()‎ تعتمد الدالة السابقة على دالة displayDisk()‎، التي سنغطيها تاليًا، لعرض كل قرص في البرج. تتحقق حلقة for level من كل قرص محتمل للبرج، وتتحقق حلقة for tower من الأبراج A و B و C. تستدعي دالة displayTowers()‎ دالة displayDisk()‎ لعرض كل قرص باتساع width معين، أو العمود الذي لا يحتوي على قرص في حال تمرير 0: # Display the tower labels A, B, and C: emptySpace = ' ' * (TOTAL_DISKS) print('{0} A{0}{0} B{0}{0} C\n'.format(emptySpace)) تعرض الشيفرة السابقة الأبراج A و B و C على الشاشة. يحتاج اللاعب إلى هذه المعلومات للتمييز بين الأبراج ولتعزيز أن الأبراج تحمل علامات A و B و C بدلًا من 1 و2 و3 أو يسار ومتوسط ويمين. اخترنا عدم استخدام 1 و2 و3 لاسم البرج لمنع اللاعبين من الخلط بين هذه الأرقام والأرقام المستخدمة لأحجام الأقراص. ضبطنا متغير emptySpace على عدد المسافات التي يجب وضعها بين كل تسمية، والتي بدورها تعتمد على TOTAL_DISKS، لأنه كلما زاد عدد الأقراص في اللعبة، كلما اتسعت المسافة بين العمودين. يمكننا استخدام تابع format()‎ بدلًا من سلسلة f النصية، فيما يلي: print(f'{emptySpace} A{emptySpace}{emptySpace} B{emptySpace}{emptySpace} C\n') يتيح لنا ذلك استخدام الوسيط emptySpace نفسه في أي مكان يظهر فيه {0} في السلسلة المعنية، مما ينتج عنه شيفرة أقصر وأكثر قابلية للقراءة من إصدار سلسلة f. تعرض دالة displayDisk()‎ قرصًا واحدًا مع اتساعه، وفي حالة عدم وجود قرص، فإنه يعرض العمود فقط: def displayDisk(width): """Display a disk of the given width. A width of 0 means no disk.""" emptySpace = ' ' * (TOTAL_DISKS - width) if width == 0: # Display a pole segment without a disk: print(f'{emptySpace}||{emptySpace}', end='') else: # Display the disk: disk = '@' * width numLabel = str(width).rjust(2, '_') print(f"{emptySpace}{disk}{numLabel}{disk}{emptySpace}", end='') نمثل هنا قرصًا يستخدم مساحةً فارغةً أولية، وعددًا من محارف "@" يساوي اتساع القرص، ومحرفين للاتساع (بما في ذلك شرطة سفلية إذا كان الاتساع رقمًا واحدًا)، وسلسلةً أخرى من محارف "@"، ثم مسافة فارغة لاحقة. لعرض العمود الفارغ فقط، كل ما نحتاجه هو المسافة الفارغة الأولية، وحرفي أنبوب pipe |، ومسافة فارغة لاحقة. نتيجةً لذلك، سنحتاج إلى ستة استدعاءات لعرض ()‎ displayDisk مع ستة وسطاء مختلفة للاتساع width للبرج التالي: || @_1@ @@_2@@ @@@_3@@@ @@@@_4@@@@ @@@@@_5@@@@@ لاحظ كيف تتقاسم دالتي displayTowers()‎ و displayDisk()‎ مسؤولية عرض الأبراج. على الرغم من أن displayTowers()‎ تقرر كيفية تفسير هياكل البيانات التي تمثل كل برج، إلا أنها تعتمد على displayDisk()‎ لعرض كل قرص في البرج فعليًا. يؤدي تقسيم البرنامج إلى دوال أصغر مثل هذه إلى تسهيل اختبار كل جزء. إذا كان البرنامج يعرض الأقراص بشكلٍ غير صحيح، فمن المحتمل أن تكون المشكلة في displayDisk()‎؛ أما إذا ظهرت الأقراص بترتيب خاطئ، فمن المحتمل أن تكون المشكلة في displayTowers()‎، وفي كلتا الحالتين، سيكون قسم الشيفرة الذي يتعين عليك تصحيحه أصغر بكثير. لاستدعاء الدالة main()‎، نستخدم دالة بايثون الشائعة: # If this program was run (instead of imported), run the game: if __name__ == '__main__': main()‎ تعيّن بايثون تلقائيًا المتغير __name__ إلى '__main__' إذا شغل اللاعب برنامج towerofhanoi.py مباشرةً، ولكن إذا استورد شخص ما البرنامج مثل وحدة باستخدام import towerofhanoi، سيُعيَّن __name__على 'towerofhanoi'. سيستدعي السطر ‎if __name__ == '__main__' :‎ الدالة main()‎، إذا شغّل شخص ما برنامجنا، وبدأ لعبة برج هانوي، ولكن إذا أردنا ببساطة استيراد البرنامج مثل وحدة حتى نتمكن -على سبيل المثال- من استدعاء الدوال الفردية فيه لاختبار الوحدة، فسيكون هذا الشرط False ولن تُستدعى main()‎. الخلاصة نمثّل الأبراج الثلاثة في أبراج هانوي، مثل قاموس بمفاتيح 'A' و 'B' و 'C' وقيمها هي قوائم من الأعداد الصحيحة. ينجح هذا الأمر في برنامجنا ولكن إذا كان برنامجنا أكبر أو أكثر تعقيدًا، فسيكون من الجيد تمثيل هذه البيانات باستخدام الأصناف classes. لم نستخدم الأصناف وتقنيات البرمجة كائنية التوجه لأننا لم نناقش هذه المواضيع بعد، لكن ضع في الحسبان أنه من الجيد تمامًا استخدام صنف لهيكل البيانات هذا. تظهر الأبراج على أنها محارف آسكي ASCII على الشاشة، باستخدام أحرف نصية لإظهار كل قرص من الأبراج. ترجمة -وبتصرف- لقسم من الفصل Practice Projects من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: قياس درجة تعقيد شيفرة بايثون باستخدام ترميز Big O الخوارزميات التعاودية recursive algorithms التعاود recursion في جافا: حل مشكلة أبراج هانوي تعلم كتابة أكواد بايثون من خلال الأمثلة العملية
  18. نستعرض في هذه المقالة كل من الأنواع types والدوال functions المتقدمة في لغة رست. الأنواع المتقدمة يحتوي نظام نوع رست على بعض الميزات التي ذكرناها سابقًا إلا أننا لم نناقشها بالتفصيل بعد، وسنبدأ بمناقشة الأنواع الجديدة newtypes بصورةٍ عامة والنظر إلى فائدتها كأنواع، ثم ننتقل إلى كتابة الاختصارات وهي ميزة مشابهة للأنواع الجديدة ولكن بدلالات مختلفة قليلًا. سنناقش أيضًا النمط ! والأنواع ذات الحجم الديناميكي dynamically sized types. استخدام نمط النوع الجديد لأمان النوع والتجريد ملاحظة: يفترض هذا القسم أنك قرأت قسم ''استخدام نمط النوع الجديد لتنفيذ سمات الخارجية على الأنواع الخارجية'' من المقال السابق مفاهيم متقدمة عن السمات Trait في لغة رست. يُعد نمط النوع الجديد مفيدًا أيضًا للمهمات التي تتجاوز تلك التي ناقشناها حتى الآن بما في ذلك الفرض الصارم بعدم الخلط بين القيم وكذلك الإشارة إلى وحدات القيمة. رأيتَ مثالًا على استخدام أنواع جديدة للإشارة إلى الوحدات في الشيفرة 15 من المقال السابق، تذكر أن هياكل Millimeters و Meters تغلف قيم u32 في نوع جديد. إذا كتبنا دالة بمحدد من النوع Millimeters فلن نتمكن من تصريف برنامج حاولَ عن طريق الخطأ استدعاء هذه الدالة بقيمة من النوع Meters أو u32 عادي. يمكننا أيضًا استخدام نمط النوع الجديد للتخلص من بعض تفاصيل التطبيق الخاصة بنوع ما، ويمكن أن يكشف النوع الجديد عن واجهة برمجية عامة API تختلف عن الواجهة البرمجية للنوع الداخلي الخاص. يمكن أن تخفي الأنواع الجديدة أيضًا التطبيق الداخلي، إذ يمكننا على سبيل المثال يمكننا منح نوع People لتغليف <HashMap<i32, String الذي يخزن معرف الشخص المرتبط باسمه. تتفاعل الشيفرة التي تستخدم People فقط مع الواجهة البرمجية العامة التي نقدمها مثل تابع لإضافة سلسلة اسم إلى مجموعة People، ولن تحتاج هذه الشيفرة إلى معرفة أننا نعيِّن معرفًا i32 للأسماء داخليًا. يعد نمط النوع الجديد طريقةً خفيفةً لتحقيق التغليف لإخفاء تفاصيل التطبيق التي ناقشناها سابقًا في قسم "التغليف وإخفاءه لتفاصيل التنفيذ" من المقال البرمجة كائنية التوجه OOP في لغة رست. إنشاء مرادفات للنوع بواسطة اسماء النوع البديلة توفّر رست القدرة على التصريح عن اسم بديل للنوع type alias لمنح نوع موجود اسمًا آخر، ونستخدم لذلك الكلمة المفتاحية type. يمكننا على سبيل المثال منح الاسم البديل Kilometers للنوع i32 على النحو التالي: type Kilometers = i32; يصبح الاسم المستعار Kilometers الآن مرادفًا للنوع i32 على عكس أنواع Millimeters و Meters التي أنشأناها في الشيفرة 15، إذ أن Kilometers ليست نوعًا جديدًا منفصلًا. ستُعامل القيم ذات النوع Kilometers نفس معاملة قيم النوع i32: type Kilometers = i32; let x: i32 = 5; let y: Kilometers = 5; println!("x + y = {}", x + y); يمكننا إضافة قيم من كلا النوعين نظرًا لأن Kilometers و i32 من النوع ذاته، كما يمكننا تمرير قيم Kilometers إلى الدوال التي تأخذ معاملات i32. لن نحصل على مزايا التحقق من النوع التي نحصل عليها من نمط النوع الجديد الذي ناقشناه سابقًا إذا استخدمنا هذا التابع، أي بعبارة أخرى إذا خلطنا قيم Kilometers و i32 في مكان ما فلن يعطينا المصرف خطأ. حالة الاستخدام الرئيسة لأسماء النوع البديلة هي تقليل التكرار، على سبيل المثال قد يكون لدينا نوع طويل مثل هذا: Box<dyn Fn() + Send + 'static> يمكن أن تكون كتابة هذا النوع المطول في بصمات الدوال ومثل تعليقات توضيحية للنوع في جميع أنحاء الشيفرة أمرًا مملًا وعرضةً للخطأ. تخيل وجود مشروع مليء بالسطر السابق كما توضح الشيفرة 24. let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi")); fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) { // --snip-- } fn returns_long_type() -> Box<dyn Fn() + Send + 'static> { // --snip-- } [الشيفرة 24: استعمال نوع طويل في أماكن كثيرة] يجعل الاسم البديل للنوع هذه الشيفرة أكثر قابلية للإدارة عن طريق تقليل التكرار، إذ قدّمنا في الشيفرة 25 اسمًا بديلًا هو Thunk للنوع المطول ويمكننا استبدال جميع استخدامات النوع بالاسم البديل الأقصر Thunk. type Thunk = Box<dyn Fn() + Send + 'static>; let f: Thunk = Box::new(|| println!("hi")); fn takes_long_type(f: Thunk) { // --snip-- } fn returns_long_type() -> Thunk { // --snip-- } [الشيفرة 25: استخدام اسم بديل Thunk لتقليل التكرار] هذه الشيفرة أسهل في القراءة والكتابة، ويمكن أن يساعد اختيار اسم ذي معنى لاسم بديل للنوع على إيصال نيتك أيضًا، إذ أن thunk هي كلمة لشيفرة تُقيَّم في وقت لاحق لذا فهو اسم مناسب للمغلّف closure الذي يُخزَّن. تُستخدم الأسماء البديلة للنوع أيضًا كثيرًا مع نوع <Result<T, E لتقليل التكرار، خذ على سبيل المثال وحدة std::io في المكتبة القياسية، إذ غالبًا ما تُعيد عمليات الدخل والخرج النوع <Result<T, E للتعامل مع المواقف التي تفشل فيها العمليات هذه، وتحتوي هذه المكتبة على هيكل std::io::Error الذي يمثل جميع أخطاء الدخل والخرج المحتملة، وتعيد العديد من الدوال في std::io النوع<Result<T, E بحيث تكون قيمة E هي std::io::Error كما هو الأمر بالنسبة للدوال الموجودة في سمة Write: use std::fmt; use std::io::Error; pub trait Write { fn write(&mut self, buf: &[u8]) -> Result<usize, Error>; fn flush(&mut self) -> Result<(), Error>; fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>; fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>; } تكررت <Result<..., Error كثيرًا، كما احتوت الوحدة std::io على هذا النوع من التصريح: type Result<T> = std::result::Result<T, std::io::Error>; يمكننا استخدام الاسم البديل المؤهل كليًا <std::io::Result<T لأن هذا التصريح موجود في الوحدة std::io، ويعني النوع السابق وجود النوع <Result<T, E مع ملء E بقيمة std::io::Error. تبدو بصمة السمة Write بنهاية المطاف على النحو التالي: pub trait Write { fn write(&mut self, buf: &[u8]) -> Result<usize>; fn flush(&mut self) -> Result<()>; fn write_all(&mut self, buf: &[u8]) -> Result<()>; fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>; } يساعد الاسم البديل للنوع بطريقتين، فهو يجعل كتابة الشيفرات أسهل ويعطينا واجهةً متسقة عبر جميع أنواع std::io، إذ نظرًا لأن الاسم البديل هو <Result<T, E ببساطة فهذا يعني أننا نستطيع استخدام أي تابع يعمل على <Result<T, E بالإضافة إلى صيغة خاصة مثل العامل ?. النوع Never الذي لا يعيد أي قيمة تمتلك لغة رست نوعًا خاصًا يدعى !، وهذا النوع معروف في لغة نظرية النوع بالنوع الفارغ empty type لأنه لا يحتوي على أي قيم، إلا أننا نفضل أن نطلق عليه اسم ''أبدًا Never'' لأنه يحلّ مكان النوع المُعاد عندما لا تُعيد الدالة أي قيمة، إليك مثالًا على ذلك: fn bar() -> ! { // --snip-- } تُقرأ الشيفرة السابقة على أنّ الدالة bar لا تُعيد أي قيمة، وتسمى الدوال التي لا تُعيد أي قيمة بالدوال المتباينة diverging functions. لا يمكننا إنشاء قيم من النوع ! لذلك لا يمكن للدالة bar أن تُعيد أي شيء. لكن ما فائدة نوع لا يمكنك أبدًا إنشاء قيم له؟ تذكر الشيفرة 5 سابقًا من المقال لغة رست غير الآمنة Unsafe Rust التي كانت جزءًا من لعبة التخمين بالأرقام، ولنعيد إنتاج جزء منها هنا في الشيفرة 26. let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; [الشيفرة 26: بنية match بذراع ينتهي بتعليمة continue] تخطينا بعض التفاصيل في هذه الشيفرة، إذ ناقشنا سابقًا في المقال بنية match للتحكم بسير برامج لغة رست، أن أذرع match يجب أن تُعيد جميعها النوع ذاته، وهذا السبب وراء عدم عمل الشيفرة التالية: let guess = match guess.trim().parse() { Ok(_) => 5, Err(_) => "hello", }; يجب أن يكون النوع guess في هذه الشيفرة عددًا صحيحًا وسلسلة وتتطلب رست أن يكون guess نوعًا واحدًا فقط. إذًا، ماذا تُعيد continue؟ كيف سُمحَ لنا بإعادة u32 من ذراع مع وجود ذراع آخر ينتهي بالتعليمة continue في الشيفرة 26؟ لربّما خمّنت ذلك فعلًا، إذ للتعليمة continue قيمة !، وذلك يعني أنه عندما تحسب رست النوع guess فإنها تنظر إلى ذراعي التطابق، الأول بقيمة u32 والأخير بقيمة !، ولأنه ليس من الممكن للقيمة ! أن تكون لها قيمة أبدًا، تقرر رست أن النوع guess هو u32. الطريقة الرسمية لوصف هذا السلوك هي أنه يمكن إجبار التعبيرات من النوع ! على أي نوع آخر. يُسمح لنا بإنهاء ذراع match هذا بالكلمة المفتاحية continue لأن continue لا تُعيد قيمة، ولكن بدلًا من ذلك يُنقل عنصر التحكم مرةً أخرى إلى أعلى الحلقة، لذلك في حالة Err لا نعيّن قيمة للمتغير guess إطلاقًا. النوع "أبدًا never" مفيدٌ في ماكرو !panic أيضًا؛ تذكر دالة unwrap التي نستدعيها على قيم <Option<T لإنتاج قيمة أو هلع بهذا التعريف: impl<T> Option<T> { pub fn unwrap(self) -> T { match self { Some(val) => val, None => panic!("called `Option::unwrap()` on a `None` value"), } } } يحدث أمر مماثل في هذه الشيفرة للشيفرة 26 ضمن match، إذ ترى رست أن val لديه النوع T و !panic من النوع ! لذا فإن نتيجة تعبير match الكلي هي T. تعمل هذه الشيفرة لأن !panic لا تنتج قيمة تنهي البرنامج. لن نعيد قيمة من unwrap في حالة None لذا فإن هذه الشيفرة صالحة. يحتوي تعبير أخير على النوع ! ألا وهو الحلقة: print!("forever "); loop { print!("and ever "); } لا تنتهي هنا الحلقة أبدًا لذا فإن ! هي قيمة التعبير، ومع ذلك لن يكون هذا صحيحًا إذا ضمنّنا break لأن الحلقة ستنتهي عندما تصل إلى break. الأنواع ذات الحجم الديناميكي والسمة Sized تحتاج رست إلى معرفة تفاصيل معينة حول الأنواع المستخدمة، مثل مقدار المساحة المراد تخصيصها لقيمة من نوع معين، وهذا يجعل من أحد جوانب نظام النوع الخاص به مربكًا بعض الشيء في البداية، تحديدًا مفهوم الأنواع ذات الحجم الديناميكي Dynamically Sized Types، ويشار إليها أحيانًا باسم DST أو الأنواع غير محددة الحجم unsized types، إذ تتيح لنا هذه الأنواع كتابة الشيفرات باستخدام قيم لا يمكننا معرفة حجمها إلا وقت التنفيذ. لنتعمق في تفاصيل النوع ذو الحجم الديناميكي المسمى str الذي استخدمناه سابقًا في جميع أنحاء السلسلة البرمجة بلغة رست، لاحظ أننا لم نقل str& وإنما str بذاتها، إذ تُعدّ من الأنواع ذات الحجم الديناميكي. لا يمكننا معرفة طول السلسلة حتى وقت التنفيذ، مما يعني أنه لا يمكننا إنشاء متغير من النوع str، ولا يمكننا أخذ وسيط من النوع str. ألقِ نظرةً على الشيفرة التالية التي لا تعمل: let s1: str = "Hello there!"; let s2: str = "How's it going?"; تحتاج رست أن تعرف مقدار الذاكرة المراد تخصيصها لأي قيمة من نوع معين ويجب أن تُستخدم جميع قيم النوع نفس المقدار من الذاكرة. إذا سمحت لنا رست بكتابة هذه الشيفرة فستحتاج قيمتي str هاتين إلى شغل المقدار ذاته من المساحة، إلا أن للقيمتين أطوال مختلفة، إذ يحتاج s1 إلى 12 بايت من التخزين ويحتاج s2 إلى 15، ولهذا السبب لا يمكن إنشاء متغير يحمل نوعًا محدد الحجم ديناميكيًا. إذًا ماذا نفعل؟ يجب أن تعلم الإجابة مسبقًا في هذه الحالة، إذ أن الحلّ هو بإنشاء الأنواع s1 و s2و str& بدلًا من str. تذكر سابقًا من قسم "شرائح السلاسل النصية" في المقال المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست أن هيكل بيانات الشريحة يخزن فقط موضع البداية وطول الشريحة، لذلك على الرغم من أن T& هي قيمة واحدة تخزن عنوان الذاكرة الخاص بالمكان الذي يوجد فيه T إلا أن str& هي قيمتان، ألا وهما عنوان str وطولها، ويمكننا على هذا النحو معرفة حجم قيمة str& في وقت التصريف، وهي ضعف طول usize، أي أننا نعرف دائمًا حجم str& بغض النظر عن طول السلسلة التي تشير إليها. هذه هي الطريقة التي تُستخدم بها الأنواع ذات الحجم الديناميكي عمومًا في رست، إذ لهذه الأنواع مقدار إضافي من البيانات الوصفية metadata التي تخزن حجم المعلومات الديناميكية. القاعدة الذهبية للأنواع ذات الحجم الديناميكي هي أنه يجب علينا دائمًا وضع قيم للأنواع ذات الحجم الديناميكي خلف مؤشر من نوع ما. يمكننا دمج str مع جميع أنواع المؤشرات، على سبيل المثال <Box<str أو <Rc<str، وقد فعلنا ذلك سابقًا ولكن بنوع ذو حجم ديناميكي مختلف، ألا وهو السمات traits، فكل سمة هي نوع ذو حجم ديناميكي يمكننا الرجوع إليه باستخدام اسم السمة. ذكرنا سابقًا في المقال استخدام كائنات السمة Object Trait في لغة رست أنه يجب وضع السمات خلف مؤشر لاستخدامها مثل كائنات سمات، مثل dyn Trait& أو <Box<dyn Trait (يمكن استخدام <Rc<dyn Trait أيضًا). توفر رست سمة Sized للعمل مع الأنواع ذات الأحجام الديناميكية لتحديد ما إذا كان حجم النوع معروفًا أم لا في وقت التصريف، إذ تُطبق هذه السمة تلقائيًا لكل شيء يُعرف حجمه في وقت التصريف، كما تضيف رست ضمنيًا تقييدًا على Sized لكل دالة عامة. يُعامل تعريف دالة عامة مثل هذه: fn generic<T>(t: T) { // --snip-- } كما لو أننا كتبنا هذا: fn generic<T: Sized>(t: T) { // --snip-- } ستعمل الدوال العامة افتراضيًا فقط على الأنواع التي لها حجم معروف في وقت التصريف، ومع ذلك يمكنك استخدام الصيغة الخاصة التالية لتخفيف هذا التقييد: fn generic<T: ?Sized>(t: &T) { // --snip-- } الصفة مرتبطة بـ Sized? تعني أن "T قد تكون أو لا تكون Sized" وهذا الترميز يلغي الافتراض الذي ينص على وجود حجم معروف للأنواع العامة وقت التصريف. صيغة Trait? بهذا المعنى متاحة فقط للسمة Sized وليس لأي سمات أخرى. لاحظ أيضًا أننا بدّلنا نوع المعامل t من T إلى T&، نظرًا لأن النوع قد لا يكون Sized فنحن بحاجة إلى استخدامه خلف نوع من المؤشرات، وفي هذه الحالة اخترنا مرجعًا. الدوال functions والمغلفات closures المتقدمة حان الوقت للتحدث عن بعض الخصائص المتقدمة المتعلقة بالمغلّفات والدوال بما في ذلك مؤشرات الدوال والمغلفات الراجعة Returing Closures. مؤشرات الدوال تحدثنا سابقًا عن كيفية تمرير المغلفات للدوال، ويمكننا أيضًا تمرير الدوال العادية للدوال. تفيد هذه التقنية عندما نريد تمرير دالة عرّفناها مسبقًا بدلًا من تعريف مغلف جديد. تُجبَر الدوال بالنوع fn (بحرف f صغير) -لا تخلط بينه وبين مغلف السمة Fn- يسمى نوع fn مؤشر دالة function pointer، ويسمح لك تمرير الدوال بمؤشرات الدوال باستخدام الدوال مثل وسطاء لدوال أُخرى. تشابه صياغة مؤشرات الدوال لتحديد معامل مثل مؤشر صياغتها في المغلفات كما تبين الشيفرة 27، إذ عرّفنا تابع add_one الذي يضيف واحد إلى معامله. تأخذ الدالة do_twice معاملين، هما مؤشر دالة لأي دالة تأخذ معامل i32 وتعيد النوع i32، وقيمة i32 واحدة. تستدعي دالة do_twice الدالة f مرتين وتمرر قيمة arg وتضيف نتيجتَي استدعاء الدالة معًا، بينما تستدعي الدالة main الدالة do_twice مع الوسيطين add_one و 5. اسم الملف: src/main.rs fn add_one(x: i32) -> i32 { x + 1 } fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 { f(arg) + f(arg) } fn main() { let answer = do_twice(add_one, 5); println!("The answer is: {}", answer); } [الشيفرة 27: استخدام نوع fn لقبول مؤشر دالة مثل وسيط] تطبع الشيفرة السابقة ما يلي: The answer is: 12 حددنا أن المعامل f في do_twice هو fn الذي يأخذ معامل واحد من النوع i32 ويُعيد i32، ويمكن بعدها استدعاء f من داخل الدالة do_twice. يمكننا في main تمرير اسم الدالة add_one على أنه الوسيط الأول إلى do_twice. على عكس المغلفات، فإن fn هو نوع وليس سمة، لذا نحدد fn مثل نوع معامل مباشرة بدلًا من تصريح معامل نوع معمم generic مع واحدة من سمات fn على أنه قيد سمة trait bound. تطبّق مؤشرات الدالة سمات المغلفة الثلاثة (Fn و FnMut و FnOnce). يعني ذلك أنه بإمكانك دائمًا تمرير مؤشر الدالة مثل وسيط لدالة تتوقع مغلفًا. هذه هي الطريقة الأفضل لكتابة الدوال باستخدام النوع المعمم وواحد من مغلف السمات بحيث يمكن للدوال الأخرى قبول دوال أو مغلفات. هناك مثال واحد تستطيع فيه قبول fn فقط وليس المغلفات وهو عندما نتعامل مع شيفرة خارجية لا تحتوي على مغلفات. يمكن لدوال لغة البرمجة سي أن تقبل الدوال مثل وسطاء، لكن ليس لديها مغلفات. لنأخذ مثالًا عن مكان استخدام مغلف معرّف ضمنيًا أو دالة مسماة، ولنتابع كيفية استخدام تابع map مقدم بسمة Iterator في المكتبة القياسية. يمكننا استخدام المغلف لاستخدام دالة map لتحويل شعاع أرقام إلى شعاع سلاسل نصية على النحو التالي: let list_of_numbers = vec![1, 2, 3]; let list_of_strings: Vec<String> = list_of_numbers.iter().map(|i| i.to_string()).collect(); أو يتسمية التابع مثل وسيط map بدلًا من المغلف على النحو التالي: let list_of_numbers = vec![1, 2, 3]; let list_of_strings: Vec<String> = list_of_numbers.iter().map(ToString::to_string).collect(); لاحظ أنه يجب استخدام الصيغة المؤهلة كليًاالتي تحدثنا عنها سابقًا في قسم "السمات المتقدمة" لأنه يوجد دوال متعددة جاهزة اسمها to_string. استخدمنا هنا الدالة to_string المعرّفة في سمة ToString التي تطبّقها المكتبة القياسية لأي نوع يطبّق Display. تذكر سابقًا من القسم "قيم التعداد" في المقال التعدادات enums في لغة رست أن اسم كل متغاير variant في تعداد enum عرّفناه يصبح أيضًا دالة تهيئة. يمكننا استخدام دوال التهيئة هذه مثل مؤشرات دالة تطبّق مغلفات السمة، ما يعني أنه يمكننا تحديد دوال التهيئة مثل وسطاء للتوابع التي تقبل المغلفات على النحو التالي: enum Status { Value(u32), Stop, } let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect(); أنشأنا هنا نسخةً من Status::Value باستخدام كل قيمة من النوع u32 ضمن المجال الذي استُدعي إليه map باستخدام دالة التهيئة Status::Value. يفضّل بعض الناس هذه الطريقة وآخرون يفضلون استخدام المغلفات، النتيجة بعد التصريف مماثلة للطريقتين لذا استخدم الطريقة الأوضح بالنسبة لك. إعادة المغلفات تُمثل المغلفات بسمات، ما يعني أنه لا يمكن إعادة المغلفات مباشرةً، إذ يمكنك استخدام النوع الحقيقي الذي ينفذ السمة مثل قيمة معادة للدالة في معظم الحالات عندما تريد إعادة سمة بدلًا من ذلك، ولكن لا يمكنك فعل ذلك في المغلفات لأنها لا تحتوي نوعًا حقيقيًا يمكن إعادته. على سبيل المثال، يُمنع استخدام مؤشرات الدالة fn مثل نوع مُعاد. تحاول الشيفرة التالية إعادة مغلف مباشرةً، ولكنها لن تُصرّف. fn returns_closure() -> dyn Fn(i32) -> i32 { |x| x + 1 } يكون خطأ المصرّف على النحو التالي: $ cargo build Compiling functions-example v0.1.0 (file:///projects/functions-example) error[E0746]: return type cannot have an unboxed trait object --> src/lib.rs:1:25 | 1 | fn returns_closure() -> dyn Fn(i32) -> i32 { | ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time | = note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits> help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:8]`, which implements `Fn(i32) -> i32` | 1 | fn returns_closure() -> impl Fn(i32) -> i32 { | ~~~~~~~~~~~~~~~~~~~ For more information about this error, try `rustc --explain E0746`. error: could not compile `functions-example` due to previous error يشير الخطأ إلى سمة Sized مجددًا. لا تعرف رست ما هي المساحة اللازمة لتخزين المغلف، وقد عرفنا حل هذه المشكلة سابقًا، إذ يمكننا استخدام كائن سمة. fn returns_closure() -> Box<dyn Fn(i32) -> i32> { Box::new(|x| x + 1) } تُصرّف الشيفرة بصورةٍ اعتيادية هنا. راجع المقال استخدام كائنات السمة Object Trait في لغة رست. سنتحدث لاحقًا عن الماكرو. ترجمة -وبتصرف- لقسم من الفصل Advanced Features من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: مفاهيم متقدمة عن السمات Trait في لغة رست تنفيذ نمط تصميمي Design Pattern كائني التوجه Object-Oriented في لغة رست الملكية Ownership في لغة رست
  19. غطينا مفهوم السمات سابقًا في فصل السمات Traits في لغة رست Rust، إلا أننا لم نناقش التفاصيل الأكثر تقدمًا. سنخوض الآن بالتفاصيل الجوهرية بعد أن تعملت المزيد عن لغة رست حتى الآن. تحديد أنواع الموضع المؤقت في تعريفات السمات مع الأنواع المرتبطة تصل الأنواع المرتبطة associated types نوع موضع مؤقت placeholder بسمة بحيث يمكن لتعريفات تابع السمة استخدام أنواع المواضع المؤقتة هذه في بصماتها signature، وسيحدد منفّذ السمة النوع الحقيقي الذي سيُستخدم بدلًا من نوع الموضع المؤقت للتنفيذ المعين. يمكننا بهذه الطريقة تحديد سمة تستخدم بعض الأنواع دون الحاجة إلى معرفة ماهية هذه الأنواع تحديدًا حتى تُطبَّق السمة. وصفنا معظم الميزات المتقدمة في هذا الفصل على أنها نادرًا ما تكون مطلوبة. توجد الأنواع المرتبطة في مكان ما في الوسط، إذ تُستخدَم نادرًا أكثر من الميزات الموضحة سابقًا في بقية الكتاب ولكنها أكثر شيوعًا من العديد من الميزات المتقدمة الأخرى التي نوقشت في هذا الفصل. أحد الأمثلة على سمة ذات نوع مرتبط هي سمة Iterator التي توفرها المكتبة القياسية. يُطلق على النوع المرتبط اسم Item ويرمز إلى نوع القيم التي يمرّ عليها النوع الذي ينفّذ سمة Iterator. تُعرّف سمة Iterator كما هو موضح في الشيفرة 12. pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } [الشيفرة 12: تعريف سمة Iterator التي لديها نوع مرتبط Item] النوع Item هو موضع مؤقت، ويوضح تعريف تابع next أنه سيعيد قيمًا من النوع <Option<Self::Item. سيحدد منفّذ سمة Iterator النوع الحقيقي للنوع Item وسيُعيد التابع next النوع Option الذي يحتوي على قيمة من هذا النوع الحقيقي. قد تبدو الأنواع المرتبطة مثل مفهوم مشابه للأنواع المعممة من حيث أن الأخير يسمح لنا بتعريف دالة دون تحديد الأنواع التي يمكنها التعامل معها، ولفحص الاختلاف بين المفهومين، سنلقي نظرةً على تنفيذ سمة Iterator على نوع يسمى Counter الذي يحدد نوع Item هو u32: اسم الملف: src/lib.rs impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { // --snip-- تبدو هذه الصيغة مشابهة لتلك الموجودة في الأنواع المعمّمة generic، فلماذا لا نكتفي بتعريف سمة Iterator باستخدام الأنواع المعممة كما هو موضح في الشيفرة 13؟ pub trait Iterator<T> { fn next(&mut self) -> Option<T>; } [الشيفرة 13: تعريف افتراضي لسمة Iterator باستخدام الأنواع المعممة] الفرق هو أنه علينا أن نوضح الأنواع في كل تنفيذ عند استخدام الأنواع المعمّمة كما في الشيفرة 13، لأنه يمكننا أيضًا تنفيذ Iterator<String> for Counter أو أي نوع آخر، فقد يكون لدينا تنفيذات متعددة للسمة Iterator من أجل Counter. بعبارة أخرى: عندما تحتوي السمة على معامل معمّم، يمكن تنفيذه لنوع ما عدة مرات مع تغيير الأنواع الحقيقية لمعاملات النوع المعمم في كل مرة، وعندما نستخدم التابع next على Counter سيتوجب علينا توفير التعليقات التوضيحية من النوع للإشارة إلى تنفيذ Iterator الذي نريد استخدامه. لا نحتاج مع الأنواع المرتبطة إلى إضافة تعليقات توضيحية للأنواع، لأننا لا نستطيع تنفيذ سمة على نوع عدة مرات. يمكننا فقط اختيار نوع Item مرةً واحدةً في الشيفرة 12 مع التعريف الذي يستخدم الأنواع المرتبطة لأنه لا يمكن أن يكون هناك سوى impl Iterator for Counter واحد. لا يتعين علينا تحديد أننا نريد مكررًا لقيم u32 في كل مكان نستدعي next على Counter. تصبح الأنواع المرتبطة أيضًا جزءًا من عقد contract السمة، إذ يجب أن يوفر منفّذو السمة نوعًا لملء الموضع المؤقت للنوع المرتبط. تمتلك الأنواع المرتبطة غالبًا اسمًا يصف كيفية استخدام النوع، كما يُعد توثيق النوع المرتبط في توثيق الواجهة البرمجية ممارسةً جيدة. معاملات النوع المعمم الافتراضي وزيادة تحميل العامل عندما نستخدم معاملات النوع المعمم يمكننا تحديد نوع حقيقي افتراضي للنوع المعمم، وهذا يلغي الحاجة إلى منفّذي السمة لتحديد نوع حقيقي إذا كان النوع الافتراضي يعمل. يمكنك تحديد نوع افتراضي عند التصريح عن نوع عام باستخدام الصيغة <PlaceholderType=ConcreteType>. من الأمثلة الرائعة على الموقف الذي تكون فيه هذه التقنية مفيدة هو زيادة تحميل العامل operator overloading، إذ يمكنك تخصيص سلوك عامل (مثل +) في مواقف معينة. لا تسمح لك رست بإنشاء عواملك الخاصة أو زيادة تحميل العوامل العشوائية، ولكن يمكنك زيادة تحميل العمليات والسمات المقابلة المدرجة في std::ops من خلال تنفيذ السمات المرتبطة بالعامل. على سبيل المثال في الشيفرة 14 زدنا تحميل العامل + لإضافة نسختين Point معًا عن طريق تنفيذ سمة Add على بنية Point. اسم الملف: src/main.rs use std::ops::Add; #[derive(Debug, Copy, Clone, PartialEq)] struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y, } } } fn main() { assert_eq!( Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, Point { x: 3, y: 3 } ); } [الشيفرة 14: تنفيذ سمة Add لزيادة تحميل العامل + لنسخ Point] يضيف التابع add قيم x لنسختَي Point وقيم y لنسختَي Point لإنشاء Point جديدة. للسمة Add نوع مرتبط يسمى Output الذي يحدد النوع الذي يُعاد من التابع add. النوع المعمم الافتراضي في هذه الشيفرة موجودٌ ضمن سمة Add. إليك تعريفه: trait Add<Rhs=Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output; } يجب أن تبدو هذه الشيفرة مألوفة عمومًا: سمة ذات تابع واحد ونوع مرتبط بها. الجزء الجديد هو Rhs=Self، إذ يُطلق على الصيغة هذه معاملات النوع الافتراضية default type parameters. يعرّف معامل النوع المعمم Rhs (اختصار للجانب الأيمن right hand size) نوع المعامل rhs في تابع add. إذا لم نحدد نوعًا حقيقيًا للقيمة Rhs عند تنفيذ سمة Add، سيُعيّن نوع Rhs افتراضيًا إلى Self الذي سيكون النوع الذي ننفّذ السمة Add عليه. عندما نفّذنا Add على Point استخدمنا الإعداد الافتراضي للنوع Rhs لأننا أردنا إضافة نسختين Point. لنلقي نظرةً على مثال لتنفيذ سمة Add، إذ نريد تخصيص نوع Rhs بدلًا من الاستخدام الافتراضي. لدينا الهيكلان Millimeters و Meters اللذان يحملان قيمًا في وحدات مختلفة، يُعرف هذا الغلاف الرقيق لنوع موجود في هيكل آخر باسم نمط النوع الجديد newtype pattern الذي نصفه بمزيد من التفصيل لاحقًا في قسم "استخدام نمط النوع الجديد لتنفيذ السمات الخارجية على الأنواع الخارجية". نريد أن نضيف القيم بالمليمترات إلى القيم بالأمتار وأن نجعل تنفيذ Add يجري التحويل صحيحًا. يمكننا تنفيذ Add للنوع Millimeters مع Meters مثل Rhs كما هو موضح في الشيفرة 15. اسم الملف: src/lib.rs use std::ops::Add; struct Millimeters(u32); struct Meters(u32); impl Add<Meters> for Millimeters { type Output = Millimeters; fn add(self, other: Meters) -> Millimeters { Millimeters(self.0 + (other.0 * 1000)) } } [الشيفرة 15: تنفيذ سمة Add على Millimeters لإضافة Millimeters للنوع Meters] لإضافة Millimeters و Meters نحدد <impl Add<Meters لتعيين قيمة محدد نوع Rhs بدلًا من استخدام الافتراضي Self. ستستخدم معاملات النوع الافتراضية بطريقتين رئيسيتين: لتوسيع نوع دون تعطيل الشيفرة الموجودة. للسماح بالتخصيص في حالات معينة لن يحتاجها معظم المستخدمين. تعد سمة المكتبة القياسية Add مثالًا على الغرض الثاني: ستضيف عادةً نوعين متشابهين، بينما توفّر خاصية Add القدرة على التخصيص بعد ذلك. يعني استخدام معامل النوع الافتراضي في تعريف سمة Add أنك لست مضطرًا لتحديد المعامل الإضافي في معظم الأوقات. بعبارة أخرى ليست هناك حاجة إلى القليل من التنفيذ المعياري، وهذا ما يسهل استخدام السمة. صيغة مؤهلة كليا للتوضيح باستدعاء التوابع التي تحمل الاسم ذاته لا شيء في رست يمنع سمةً ما من أن يكون لها تابع يحمل اسم تابع السمة الأخرى ذاتها، كما لا تمنعك رست من تنفيذ كلتا السمتين على نوع واحد، ومن الممكن أيضًا تنفيذ تابع مباشرةً على النوع الذي يحمل اسم التوابع نفسه من السمات. عند استدعاء التوابع التي تحمل الاسم نفسه، ستحتاج إلى إخبار رست بالتابع الذي تريد استخدامه. ألقِ نظرةً على الشيفرة 16، إذ عرّفنا سمتين Pilot و Wizard ولكل منهما تابع يسمى fly، ثم نفّذنا كلتا السمتين على نوع Human لديه فعلًا تابع يسمى fly منفّذ عليه، وكل تابع fly يفعل شيئًا مختلفًا. اسم الملف: src/main.rs trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } [الشيفرة 16: تعريف سمتين بحيث تملكان تابع fly وتنفيذهما على النوع Human وتنفيذ تابع fly على Human مباشرةً] عندما نستدعي fly على نسخة Human يتخلف المصرف عن استدعاء التابع الذي يُنفّذ مباشرةً على النوع كما هو موضح في الشيفرة 17. اسم الملف: src/main.rs fn main() { let person = Human; person.fly(); } [الشيفرة 17: استدعاء fly على نسخة Human] سيؤدي تنفيذ هذه الشيفرة إلى طباعة *waving arms furiously* مما يدل على أن رست استدعت تابع fly المنفّذ على Human مباشرةً. نحتاج إلى استخدام صيغة أكثر وضوحًا عند استدعاء توابع fly من سمة Pilot أو Wizard لتحديد تابع fly الذي نعنيه، وتوضّح الشيفرة 18 هذه الصيغة. اسم الملف: src/main.rs fn main() { let person = Human; Pilot::fly(&person); Wizard::fly(&person); person.fly(); } [الشيفرة 18: تحديد تابع السمة fly التي نريد استدعاءه] يوضح تحديد اسم السمة قبل اسم التابع لرست أي تنفيذ للتابع fly نريد استدعاءه. يمكننا أيضًا كتابة Human::fly(&person)‎ وهو ما يعادل person.fly()‎الذي استخدمناه في الشيفرة 18، ولكن هذا أطول قليلًا للكتابة إذا لم نكن بحاجة إلى توضيح. يؤدي تنفيذ هذه الشيفرة إلى طباعة التالي: $ cargo run Compiling traits-example v0.1.0 (file:///projects/traits-example) Finished dev [unoptimized + debuginfo] target(s) in 0.46s Running `target/debug/traits-example` This is your captain speaking. Up! *waving arms furiously* إذا كان لدينا نوعان ينفذ كلاهما سمةً واحدةً، يمكن أن تكتشف رست أي تنفيذ للسمة يجب استخدامه بناءً على نوع self نظرًا لأن تابع fly يأخذ معامل self، ومع ذلك فإن الدوال functions المرتبطة التي ليست بتوابع لا تحتوي على معامل self. عندما تكون هناك أنواع أو سمات متعددة تحدد دوالًا غير تابعية non-method بنفس اسم الدالة، لا تعرف رست دائمًا النوع الذي تقصده ما لم تستخدم صيغة مؤهلة كليًا fully qualified syntax. على سبيل المثال في الشيفرة 19 أنشأنا سمة لمأوى للحيوانات يسمّي جميع الجراء puppies الصغار Spot. ننشئ سمة Animal بدالة غير تابعية مرتبطة baby_name، تُنفّذ Animal لهيكل Dog الذي نوفّر عليه أيضًا دالةً غير تابعية مرتبطة baby_name مباشرةً. اسم الملف: src/main.rs trait Animal { fn baby_name() -> String; } struct Dog; impl Dog { fn baby_name() -> String { String::from("Spot") } } impl Animal for Dog { fn baby_name() -> String { String::from("puppy") } } fn main() { println!("A baby dog is called a {}", Dog::baby_name()); } [الشيفرة 19: سمة لها دالة مرتبطة ونوع له دالة مرتبطة بالاسم ذاته الذي ينفّذ السمة أيضًا] ننفّذ الشيفرة الخاص بتسمية كل الجراء في الدالة المرتبطة baby_name والمُعرّفة في Dog. ينفّذ النوع Dog أيضًا سمة Animal التي تصف الخصائص التي تمتلكها جميع الحيوانات. يُطلق على أطفال الكلاب اسم الجراء ويُعبّر عن ذلك في تنفيذ سمة Animal على Dog في الدالة المرتبطة baby_name بالسمة Animal نستدعي في الدالة main الدالة Dog::baby_name التي تستدعي الدالة المرتبطة المعرفة في Dog مباشرةً. تطبع هذه الشيفرة ما يلي: $ cargo run Compiling traits-example v0.1.0 (file:///projects/traits-example) Finished dev [unoptimized + debuginfo] target(s) in 0.54s Running `target/debug/traits-example` A baby dog is called a Spot لم نكن نتوقع هذه النتيجة، إذ نريد استدعاء دالة baby_name التي تعد جزءًا من سمة Animal التي نفّذناها على Dog حتى تطبع الشيفرة A baby dog is called a puppy. لا تساعد تقنية تحديد اسم السمة التي استخدمناها في الشيفرة 18 هنا، وإذا غيرنا main للشيفرة لما هو موجود في الشيفرة 20، سنحصل على خطأ عند التصريف. اسم الملف: src/main.rs fn main() { println!("A baby dog is called a {}", Animal::baby_name()); } [الشيفرة 20: محاولة استدعاء الدالة baby_name من السمة Animal دون معرفة رست بأي تنفيذ ينبغي استخدامه] لا تستطيع رست معرفة أي تنفيذ نريده للقيمة Animal::baby_name لأن Animal::baby_name لا تحتوي على معامل self ولأنه يمكن أن تكون هناك أنواع أخرى تنفّذ سمة Animal. سنحصل على خطأ المصرف هذا: $ cargo run Compiling traits-example v0.1.0 (file:///projects/traits-example) error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type --> src/main.rs:20:43 | 2 | fn baby_name() -> String; | ------------------------- `Animal::baby_name` defined here ... 20 | println!("A baby dog is called a {}", Animal::baby_name()); | ^^^^^^^^^^^^^^^^^ cannot call associated function of trait | help: use the fully-qualified path to the only available implementation | 20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name()); | +++++++ + For more information about this error, try `rustc --explain E0790`. error: could not compile `traits-example` due to previous error لإزالة الغموض وإخبار رست أننا نريد استخدام تنفيذ Animal للقيمة Dog بدلًا من استخدام Animal لبعض الأنواع الأخرى، نحتاج إلى استخدام صيغة مؤهلة كليًا. توضح الشيفرة 21 كيفية استخدام صيغة مؤهلة كليًا. اسم الملف: src/main.rs fn main() { println!("A baby dog is called a {}", <Dog as Animal>::baby_name()); } [الشيفرة 21: استعمال صيغة مؤهلة كليًا لتحديد أننا نريد استدعاء دالة baby_name من السمة Animal كما هو منفّذ في Dog] نقدم لرست تعليقًا توضيحيًا للنوع ضمن أقواس مثلثية angle brackets مما يشير إلى أننا نريد استدعاء تابع baby_name من سمة Animal كما هو منفّذ في Dog بالقول إننا نريد معاملة النوع Dog مثل النوع Animal لاستدعاء الدالة هذه. ستطبع الشيفرة البرمجية الآن ما نريد: $ cargo run Compiling traits-example v0.1.0 (file:///projects/traits-example) Finished dev [unoptimized + debuginfo] target(s) in 0.48s Running `target/debug/traits-example` A baby dog is called a puppy تعرف الصيغة المؤهلة كليًا عمومًا على النحو التالي: <Type as Trait>::function(receiver_if_method, next_arg, ...); بالنسبة للدوال المرتبطة التي ليست توابع، لن يكون هناك receiver، سيكون هناك فقط قائمة من الوسائط الأخرى. يمكنك استخدام صيغة مؤهلة كليًا في كل مكان تستدعي فيه دوالًا أو توابعًا، ومع ذلك يُسمح لك بحذف أي جزء من هذه الصيغة التي يمكن لرست اكتشافها من المعلومات الأخرى في البرنامج. ما عليك سوى استخدام هذه الصيغة المطولة أكثر في الحالات التي توجد فيها العديد من التنفيذات التي تستخدم الاسم ذاته وتحتاج رست إلى المساعدة في تحديد التنفيذ الذي تريد استدعاءه. استخدام سمات خارقة supertrait لطلب وظيفة إحدى السمات ضمن سمة أخرى في بعض الأحيان قد تكتب تعريف سمة يعتمد على سمة أخرى: لكي ينفّذ النوع السمة الأولى فأنت تريد أن تطلب هذا النوع لتنفيذ السمة الثانية أيضًا. يمكنك فعل ذلك حتى يتمكن تعريف السمة الخاص بك من الاستفادة من العناصر المرتبطة للسمة الثانية. تسمى السمة التي يعتمد عليها تعريف السمة الخاص بك بالسمة الخارقة لسمتك. على سبيل المثال لنفترض أننا نريد إنشاء سمة OutlinePrint باستخدام تابع outline_print الذي سيطبع قيمة معينة منسقة بحيث تكون مؤطرة بعلامات نجمية. بالنظر إلى هيكل Point الذي ينفّذ سمة المكتبة القياسية Display لتعطي النتيجة (x, y)، عندما نستدعي outline_print على نسخة Point التي تحتوي على 1 للقيمة x و 3 للقيمة y، يجب أن يطبع ما يلي: ********** * * * (1, 3) * * * ********** نريد استخدام وظيفة سمة Display في تنفيذ طريقة outline_print، لذلك نحتاج إلى تحديد أن سمة OutlinePrint ستعمل فقط مع الأنواع التي تنفّذ أيضًا Display وتوفر الوظائف التي تحتاجها OutlinePrint. يمكننا فعل ذلك في تعريف السمة عن طريق تحديد OutlinePrint: Display، وتشبه هذه التقنية إضافة سمة مرتبطة بالسمة. تُظهر الشيفرة 22 تنفيذ سمة OutlinePrint. اسم الملف: src/main.rs use std::fmt; trait OutlinePrint: fmt::Display { fn outline_print(&self) { let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {} *", output); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } } [الشيفرة 22: تنفيذ سمة OutlinePrint التي تتطلب الوظيفة من Display] بما أننا حددنا أن OutlinePrint تتطلب سمة Display، يمكننا استخدام دالة to_string التي تُنفّذ تلقائيًا لأي نوع ينفّذ Display. إذا حاولنا استخدام to_string دون إضافة نقطتين وتحديد سمة Display بعد اسم السمة، سنحصل على خطأ يقول بأنه لم يُعثر على تابع باسم to_string للنوع Self& في النطاق الحالي. لنرى ما يحدث عندما نحاول تنفيذ OutlinePrint على نوع لا ينفّذ Display مثل هيكل Point. اسم الملف: src/main.rs struct Point { x: i32, y: i32, } impl OutlinePrint for Point {} نحصل على خطأ يقول أن Display مطلوبة ولكن غير منفّذة: $ cargo run Compiling traits-example v0.1.0 (file:///projects/traits-example) error[E0277]: `Point` doesn't implement `std::fmt::Display` --> src/main.rs:20:6 | 20 | impl OutlinePrint for Point {} | ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter | = help: the trait `std::fmt::Display` is not implemented for `Point` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead note: required by a bound in `OutlinePrint` --> src/main.rs:3:21 | 3 | trait OutlinePrint: fmt::Display { | ^^^^^^^^^^^^ required by this bound in `OutlinePrint` For more information about this error, try `rustc --explain E0277`. error: could not compile `traits-example` due to previous error لإصلاح هذا الأمر ننفّذ Display على Point ونلبي القيد الذي تتطلبه OutlinePrint مثل: اسم الملف: src/main.rs use std::fmt; impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } سيُصرَّف تنفيذ سمة OutlinePrint على Point بعدها بنجاح، ويمكننا استدعاء outline_print على نسخة Point لعرضها ضمن مخطط من العلامات النجمية. استخدام نمط النوع الجديد لتنفيذ سمات الخارجية على الأنواع الخارجية ذكرنا سابقًا في الفصل السمات Traits في لغة رست في قسم "تطبيق السمة على نوع" القاعدة الوحيدة التي تنص على أنه لا يُسمح لنا إلا بتنفيذ سمة على نوع ما إذا كانت السمة أو النوع محليين بالنسبة للوحدة المصرفة الخاصة بنا، إلا أنه من الممكن التحايل على هذا القيد باستخدام نمط النوع الجديد Newtype pattern الذي يتضمن إنشاء نوع جديد في هيكل الصف (ناقشنا هياكل الصف سابقًا في قسم "استخدام هياكل الصفوف دون حقول مسماة لإنشاء أنواع مختلفة" من الفصل استخدام الهياكل structs لتنظيم البيانات في لغة رست)، إذ سيكون لهيكل الصف حقلًا واحدًا وسيكون هناك غلافًا رفيعًا حول النوع الذي نريد تنفيذ سمة له، ثم يكون نوع الغلاف محليًا بالنسبة للوحدة المصرفة الخاصة بنا ويمكننا تنفيذ السمة على الغلاف. النوع الجديد هو مصطلح ينشأ من لغة البرمجة هاسكل Haskell. لا يوجد تأثير سلبي على وقت التنفيذ لاستخدام هذا النمط ويُستبعد نوع الغلاف في وقت التصريف. على سبيل المثال، لنفترض أننا نريد تنفيذ Display على <Vec<T التي تمنعنا القاعدة الوحيدة من فعل ذلك مباشرةً لأن سمة Display ونوع <Vec<T مُعرّفان خارج الوحدة المصرفة الخاصة بنا. يمكننا عمل هيكل Wrapper يحتوي على نسخة من <Vec<T ومن ثم تنفيذ Display على Wrapper واستخدام قيمة <Vec<T كما هو موضح في الشيفرة 23. اسم الملف: src/main.rs use std::fmt; struct Wrapper(Vec<String>); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } fn main() { let w = Wrapper(vec![String::from("hello"), String::from("world")]); println!("w = {}", w); } [الشيفرة 23: إنشاء نوع Wrapper حول <Vec<String لتنفيذ Display] تُستخدم self.0 من قِبل تنفيذ Display للوصول إلى <Vec<T الداخلي، لأن Wrapper هيكل صف و <Vec<T هو العنصر في الدليل 0 في الصف. يمكننا بعد ذلك استخدام وظيفة نوع Display على Wrapper. الجانب السلبي لاستخدام هذه التقنية هو أن Wrapper هو نوع جديد لذلك لا يحتوي على توابع القيمة التي يحملها. سيتوجب علينا تنفيذ جميع توابع <Vec<T مباشرةً على Wrapper، بحيث تفوض هذه التوابع إلى self.0، ليسمح لنا بالتعامل مع Wrapper تمامًا مثل <Vec<T. إذا أردنا أن يحتوي النوع الجديد على كل تابع يمتلكه النوع الداخلي، سيكون تنفيذ سمة Deref (ناقشناها سابقًا في الفصل معاملة المؤشرات الذكية Smart Pointers مثل مراجع نمطية Regular References باستخدام سمة Deref في لغة رست) على Wrapper لإرجاع النوع الداخلي حلًا مناسبًا. إذا كنا لا نريد أن يحتوي نوع Wrapper على جميع التوابع من النوع الداخلي -على سبيل المثال لتقييد سلوك نوع Wrapper- سيتعين علينا تنفيذ التوابع التي نريدها يدويًا فقط. نموذج النمط الجديد هذا مفيد أيضًا حتى عندما لا تكون السمات متضمنة. لنغير تركيزنا الآن، ونلقي نظرةً على بعض الطرق المتقدمة للتفاعل مع نظام نوع رست. ترجمة -وبتصرف- لقسم من الفصل Advanced Features من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: لغة رست غير الآمنة مقدمة إلى مفهوم الأنواع المعممة Generic Types في لغة Rust أنواع البيانات Data Types في لغة رست المؤشرات الذكية Smart Pointers
  20. لدى كل الشيفرات البرمجية التي ناقشناها حتى الآن ضمانات لأمان الذاكرة في رست وتُفرض هذه الضمانات وقت التصريف، ومع ذلك فإن رست تحتوى على لغة ثانية مخبأة داخلها لا تفرض ضمانات أمان الذاكرة هذه، ويطلق عليها اسم رست غير الآمنة وتعمل تمامًا مثل رست العادية ولكنها تمنحنا قوى خارقة إضافية. توجد رست غير الآمنة لأن التحليل الساكن بطبيعته متحفظ، أي عندما يحاول المصرّف تحديد ما إذا كانت الشيفرة البرمجية تدعم الضمانات أم لا، فمن الأفضل له رفض بعض البرامج الصالحة بدلًا من قبول بعض البرامج غير الصالحة. يمكن أن تكون الشيفرة البرمجية تكون جيدة، إلا أن مصرف رست سيرفض تصريف الشيفرة البرمجية إن لم يكن لديه معلومات كافية ليكون واثقًا، ويمكنك في هذه الحالات استعمال شيفرة غير آمنة لإخبار المصرف "صدقني، أعرف ما أفعله"، ومع ذلك كن حذرًا من استعمال رست غير الآمنة على مسؤوليتك الخاصة: إذا استعملت شيفرة غير آمنة على نحوٍ غير صحيح فقد تحدث بعض المشاكل بسبب عدم أمان الذاكرة مثل تحصيل dereferencing مؤشر فارغ. السبب الآخر لوجود لغة رست غير آمنة هو أن عتاد الحاسب الأساسي غير آمن بطبيعته، فإذا لم تسمح لك رست بإنجاز عمليات غير آمنة فلن يمكنك إنجاز مهام معينة. تحتاج رست إلى السماح لك ببرمجة الأنظمة منخفضة المستوى مثل التفاعل المباشر مع نظام التشغيل أو حتى كتابة نظام التشغيل الخاص بك. يُعد العمل مع برمجة الأنظمة منخفضة المستوى أحد أهداف اللغة. لنكتشف ما يمكننا فعله مع رست غير الآمنة وكيفية إنجاز ذلك. القوى الخارقة غير الآمنة unsafe superpowers استخدم الكلمة المفتاحية unsafe للتبديل إلى رست غير الآمنة، ثم ابدأ كتلة جديدة تحتوي على الشيفرة غير الآمنة. يمكنك اتخاذ خمسة إجراءات في رست غير الآمنة لا يمكنك فعلها في رست الآمنة ونسميها القوى الخارقة غير الآمنة؛ تتضمن هذه القوى الخارقة القدرة على: تحصيل مؤشر خام raw pointer. استدعاء تابع أو دالة غير آمنين. الوصول أو التعديل على متغير ساكن static متغيّر mutable. تطبيق سمة trait غير آمنة. حقول الوصول الخاصة بـ union. من المهم أن نفهم أن unsafe لا توقف تشغيل مدقق الاستعارة أو تعطل أي من فحوصات أمان رست الأخرى؛ وإذا كنت تستخدم مرجعًا في شيفرة غير آمنة فسيظل التحقق منه جاريًا. تمنحك الكلمة المفتاحية unsafe فقط الوصول إلى هذه الميزات الخمس التي لم يجري التحقق منها بعد ذلك من المصرف من أجل سلامة الذاكرة، وستظل تتمتع بدرجة من الأمان داخل كتلة غير آمنة. لا تعني unsafe أن الشيفرة الموجودة داخل الكتلة هي بالضرورة خطيرة أو أنها ستواجه بالتأكيد مشكلات تتعلق بسلامة الذاكرة، القصد هو أنه بصفتك مبرمجًا ستضمن أن الشيفرة الموجودة داخل كتلة unsafe ستصل إلى الذاكرة بطريقة صالحة. ليس البشر معصومين والأخطاء تحصل، ويمكنك من خلال إجبار وجود هذه العمليات الخمس غير الآمنة داخل كتل موصّفة unsafe أن تضمن بقاء أي أخطاء متعلقة بأمان الذاكرة داخل كتلة unsafe. اجعل كتل unsafe صغيرة، ستقدر ذلك لاحقًا عندما تفتش عن أخطاء الذاكرة. لعزل الشيفرة غير الآمنة قدر الإمكان، يُفضّل تضمين الشيفرة غير الآمنة في عملية تجريد آمنة وتوفير واجهة برمجة تطبيقات آمنة التي سنناقشها لاحقًا في هذا الفصل عندما نتحدث عن الدوال والتوابع غير الآمنة. تُطبَّق أجزاء من المكتبة القياسية مثل تجريدات آمنة على الشيفرات البرمجية غير الآمنة التي قد جرى تدقيقها. يمنع تغليف الشيفرة غير الآمنة في عملية تجريد آمنة استخدامات unsafe من التسرب إلى جميع الأماكن التي قد ترغب أنت أو المستخدمين لديك في استخدام الوظيفة المطبقة بشيفرة unsafe لأن استخدام التجريد الآمن آمن. لنلقي نظرةً على كل من القوى الخارقة الخمس غير الآمنة بالترتيب، سنلقي نظرةً أيضًا على بعض الأفكار المجردة التي تقدم واجهةً آمنةً للشيفرات البرمجية غير الآمنة. تحصيل مرجع مؤشر خام ذكرنا سابقًا في الفصل المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست في قسم "المراجع المعلقة" أن المصرّف يضمن صلاحية المراجع دائمًا. تحتوي رست غير الآمنة على نوعين جديدين يدعيان المؤشرات الخام التي تشبه المراجع، فكما هو الحال مع المراجع يمكن أن تكون المؤشرات الخام ثابتة immutable أو متغيّرة mmutable وتُكتب بالطريقة const T* و mut T* على التوالي. لا تمثّل علامة النجمة عامل التحصيل وإنما هي جزءٌ من اسم النوع. يُقصد بمصطلح الثابت في سياق المؤشرات الخام أنه لا يمكن تعيين المؤشر مباشرةً بعد تحصيله. تختلف المؤشرات الأولية عن المراجع والمؤشرات الذكية بما يلي: يُسمح بتجاهل قواعد الاستعارة من خلال وجود مؤشرات ثابتة أو متغيّرة أو مؤشرات متعددة متغيّرة إلى الموقع ذاته. ليست مضمونة للإشارة إلى ذاكرة صالحة. من المسموح أن تكون فارغة. لا تطبق أي تحرير ذاكرة تلقائي. يمكنك التخلي عن الأمان المضمون من خلال تجاهل الضمانات التي تقدمها رست وذلك مقابل أداء أفضل أو القدرة على التفاعل مع لغة أو عتاد آخر لا تنطبق عليه ضمانات رست. توضح الشيفرة 1 كيفية إنشاء مؤشر خام ثابت ومتغيّر من المراجع. let mut num = 5; let r1 = &num as *const i32; let r2 = &mut num as *mut i32; [الشيفرة 1: إنشاء مؤشرات خام من المراجع] لاحظ أننا لا نضمّن الكلمة المفتاحية unsafe في هذه الشيفرة. يمكننا إنشاء مؤشرات خام في شيفرة آمنة، ولا يمكننا تحصيل المؤشرات الخام خارج كتلة غير آمنة كما سترى بعد قليل. أنشأنا مؤشرات خام باستعمال as لتحويل مرجع ثابت ومتغيّر إلى أنواع المؤشرات الخام الخاصة بهما، ونظرًا إلى أننا أنشأناها مباشرةً من مراجع مضمونة لتكون صالحة فنحن نعلم أن هذه المؤشرات الخام المعنيّة صالحة لكن لا يمكننا افتراض هذا حول أي مؤشر خام. لإثبات ذلك سننشئ مؤشر خام لا يمكننا التأكد من صحته. تظهر الشيفرة 2 كيفية إنشاء مؤشر خام يشير إلى موقع عشوائي في الذاكرة. محاولة استخدام الذاكرة العشوائية هي عملية غير معرّفة، إذ قد توجد بيانات في ذلك العنوان أو قد لا تكون موجودة، ومن الممكن للمصرّف أن يحسن الشيفرة بحيث لا يكون هناك وصول للذاكرة أو قد يخطئ البرنامج في وجود خطأ في التجزئة segmentation. لا يوجد عادةً سببٌ جيد لكتابة شيفرة مثل هذه، لكن هذا ممكن. let address = 0x012345usize; let r = address as *const i32; [الشيفرة 2: إنشاء مؤشر خام إلى عنوان ذاكرة عشوائي] تذكر أنه يمكننا إنشاء مؤشرات خام في شيفرة آمنة لكن لا يمكننا تحصيل المؤشرات الخام وقراءة البيانات التي يُشار إليها، ونستخدم في الشيفرة 3 عامل التحصيل * على مؤشر خام يتطلب كتلة unsafe. let mut num = 5; let r1 = &num as *const i32; let r2 = &mut num as *mut i32; unsafe { println!("r1 is: {}", *r1); println!("r2 is: {}", *r2); } [الشيفرة 3: تحصيل المؤشرات الخام ضمن كتلة unsafe] لا يضرّ إنشاء مؤشر؛ إذ يكمن الضرر فقط عندما نحاول الوصول إلى القيمة التي تشير إليها، فقد ينتهي بنا الأمر بالتعامل مع قيمة غير صالحة. لاحظ أيضًا أننا أنشأنا في الشيفرة 1 و3 مؤشرات خام من النوع const i32* و mut i32* التي أشارت كلتاهما إلى موقع الذاكرة ذاته، حيث يُخزَّن num. إذا حاولنا إنشاء مرجع ثابت ومتغيّر إلى num بدلًا من ذلك، فلن تُصرّف الشيفرة لأن قواعد ملكية رست لا تسمح بمرجع متغيّر في الوقت ذاته كما هو الحال مع أي مراجع ثابتة. يمكننا باستخدام المؤشرات الخام إنشاء مؤشر متغيّر ومؤشر ثابت للموقع ذاته وتغيير البيانات من خلال المؤشر المتغيّر مما قد يؤدي إلى إنشاء سباق بيانات data race. كن حذرًا. مع كل هذه المخاطر، لماذا قد تستخدم المؤشرات الخام؟ إحدى حالات الاستخدام الرئيسية هي عند التفاعل مع شيفرة سي C كما سترى لاحقًا في القسم "استدعاء دالة أو تابع غير آمنين"، وهناك حالة أخرى عند بناء تجريدات آمنة لا يفهمها مدقق الاستعارة. سنقدم دالات غير آمنة، ثم سنلقي نظرةً على مثال على التجريد الآمن الذي يستعمل شيفرةً غير آمنة. استدعاء تابع أو دالة غير آمنين النوع الثاني من العمليات التي يمكنك إجراؤها في كتلة غير آمنة هو استدعاء دالات غير آمنة، إذ تبدو الدالات والتوابع غير الآمنة تمامًا مثل الدالات والتوابع العادية ولكنها تحتوي على unsafe إضافية قبل بقية التعريف. تشير الكلمة المفتاحية unsafe في هذا السياق إلى أن الدالة لها متطلبات نحتاج إلى دعمها عند استدعاء هذه الدالة لأن رست لا يمكن أن تضمن أننا استوفينا هذه المتطلبات؛ فمن خلال استدعاء دالة غير آمنة داخل كتلة unsafe، فإننا نقول إننا قد قرأنا توثيق هذه الدالة ونتحمل مسؤولية دعم مواصفات الدالة هذه. فيما يلي دالة غير آمنة تدعى dangerous لا تنفّذ أي شيء داخلها: unsafe fn dangerous() {} unsafe { dangerous(); } يجب علينا استدعاء دالة dangerous داخل كتلة unsafe منفصلة، وإذا حاولنا استدعاء dangerous دون unsafe سنحصل على خطأ: $ cargo run Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example) error[E0133]: call to unsafe function is unsafe and requires unsafe function or block --> src/main.rs:4:5 | 4 | dangerous(); | ^^^^^^^^^^^ call to unsafe function | = note: consult the function's documentation for information on how to avoid undefined behavior For more information about this error, try `rustc --explain E0133`. error: could not compile `unsafe-example` due to previous error نؤكد لرست مع الكتلة unsafe أننا قرأنا توثيق الدالة ونفهم كيفية استخدامها صحيحًا وتحققنا من أننا نفي بمواصفات الدالة. يعدّ محتوى الدالات غير الآمنة بمثابة كتل unsafe، لذا لا نحتاج إلى إضافة كتلة unsafe أخرى لأداء عمليات أخرى غير آمنة ضمن دالة غير آمنة. إنشاء تجريد آمن على شيفرة غير آمنة لا يعني احتواء الدالة على شيفرة غير آمنة أننا بحاجة إلى وضع علامة بأن كامل الدالة غير آمنة، إذ يُعد تغليف الشيفرات البرمجية غير الآمنة في دالة آمنة تجريدًا شائعًا. وكمثال دعنا ننظر إلى الدالة split_at_mut الموجودة في المكتبة القياسية التي تتطلب بعض الشيفرات البرمجية غير الآمنة. سنكتشف كيف يمكننا تنفيذها. يُعرَّف هذا التابع الآمن على الشرائح المتغيّرة، فهو يأخذ شريحةً واحدة ويحوّلها لشريحتين عن طريق تقسيم الشريحة في الدليل المعطى مثل وسيط. توضح الشيفرة 4 كيفية استخدام split_at_mut. let mut v = vec![1, 2, 3, 4, 5, 6]; let r = &mut v[..]; let (a, b) = r.split_at_mut(3); assert_eq!(a, &mut [1, 2, 3]); assert_eq!(b, &mut [4, 5, 6]); [الشيفرة 4: استعمال الدالة الآمنة split_at_mut] لا يمكننا تنفيذ هذه الدالة باستعمال رست الآمنة فقط، وقد تبدو المحاولة السابقة مثل الشيفرة 5 التي لن تصرف. سننفّذ split_at_mut مثل دالة للتبسيط بدلًا من تابع لشرائح قيم i32 فقط بدلًا من النوع العام T. fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = values.len(); assert!(mid <= len); (&mut values[..mid], &mut values[mid..]) } [الشيفرة 5: محاولة تنفيذ split_at_mut فقط باستعمال رست الآمنة] تحصل هذه الدالة أولًا على الطول الكلي للشريحة، ثم تؤكد أن الدليل المعطى على أنه معامل موجود داخل الشريحة عن طريق التحقق مما إذا كان أقل أو يساوي الطول. يعني هذا التأكيد أنه إذا مررنا دليلًا أكبر من الطول لتقسيم الشريحة عنده، ستهلع الدالة قبل أن تحاول استعمال هذا الدليل. نعيد بعد ذلك شريحتين متغيّرتين في الصف، واحدة من بداية الشريحة الأصلية إلى الدليل mid والأخرى من mid إلى نهاية الشريحة. عندما نحاول تصريف الشيفرة البرمجية في الشيفرة 5 سنحصل على خطأ. $ cargo run Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example) error[E0499]: cannot borrow `*values` as mutable more than once at a time --> src/main.rs:6:31 | 1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { | - let's call the lifetime of this reference `'1` ... 6 | (&mut values[..mid], &mut values[mid..]) | --------------------------^^^^^^-------- | | | | | | | second mutable borrow occurs here | | first mutable borrow occurs here | returning this value requires that `*values` is borrowed for `'1` For more information about this error, try `rustc --explain E0499`. error: could not compile `unsafe-example` due to previous error لا يستطيع مدقق الاستعارة في رست أن يفهم أننا نستعير أجزاءً مختلفةً من الشريحة، إذ أنه يعرف فقط أننا نستعير من الشريحة نفسها مرتين. تعد عملية استعارة أجزاء مختلفة من الشريحة أمرًا مقبولًا بصورةٍ أساسية لأن الشريحتين غير متداخلتين لكن رست ليست ذكية بما يكفي لمعرفة ذلك. عندما نعلم أن الشيفرة على ما يرام لكن رست لا تعلم ذلك فهذا يعني أن الوقت قد حان لاستخدام شيفرة غير آمنة. توضح الشيفرة 6 كيفية استخدام كتلة unsafe ومؤشر خام وبعض الاستدعاءات للدالات غير الآمنة لجعل تنفيذ split_at_mut يعمل. use std::slice; fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = values.len(); let ptr = values.as_mut_ptr(); assert!(mid <= len); unsafe { ( slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), len - mid), ) } } [الشيفرة 6: استعمال شيفرة غير آمنة في تنفيذ دالة split_at_mut] تذكر سابقًا من قسم "نوع الشريحة" في الفصل المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست أن الشرائح هي مؤشرات لبعض البيانات وطول الشريحة. نستعمل تابع len للحصول على طول الشريحة وتابع as_mut_ptr للوصول إلى المؤشر الخام للشريحة، وفي هذه الحالة نظرًا لأن لدينا شريحة متغيّرة لقيم i32 فإن as_mut_ptr تُعيد مؤشرًا خامًا من النوع mut i32* وهو الذي خزّناه في المتغير ptr. نحافظ على التأكيد على أن الدليل mid يقع داخل الشريحة، ثم نبدأ بكتابة الشيفرة غير الآمنة: تأخذ الدالة slice::from_raw_parts_mut مؤشرًا خامًا وطولًا وتنشئ شريحة. نستخدم هذه الدالة لإنشاء شريحة تبدأ من ptr وتكون عناصرها بطول mid، ثم نستدعي التابع add على ptr مع الوسيط mid للحصول على مؤشر خام يبدأ من mid وننشئ شريحةً باستخدام هذا المؤشر والعدد المتبقي من العناصر بعد mid ليكون طول الشريحة. الدالة slice::from_raw_parts_mut غير آمنة لأنها تأخذ مؤشرًا خامًا ويجب أن تثق في أن هذا المؤشر صالح، كما يعد التابع add في المؤشرات الخام غير آمن أيضًا لأنه يجب أن تثق في أن موقع الإزاحة هو أيضًا مؤشر صالح، لذلك كان علينا وضع كتلة unsafe حول استدعاءات slice::from_raw_parts_mut و addحتى نتمكن من استدعائها. من خلال النظر إلى الشيفرة وإضافة التأكيد على أن mid يجب أن يكون أقل من أو يساوي len يمكننا أن نقول أن جميع المؤشرات الخام المستخدمة داخل الكتلة unsafe ستكون مؤشرات صالحة للبيانات داخل الشريحة، وهذا استخدام مقبول ومناسب للكتلة unsafe. لاحظ أننا لسنا بحاجة إلى وضع علامة على الدالة split_at_mut الناتجة بكونها unsafe، ويمكننا استدعاء هذه الدالة من رست الآمنة. أنشأنا تجريدًا آمنًا للشيفرة غير الآمنة من خلال تنفيذ الدالة التي تستعمل شيفرة unsafe بطريقة آمنة لأنها تُنشئ مؤشرات صالحة فقط من البيانات التي يمكن لهذه الدالة الوصول إليها. في المقابل، من المحتمل أن يتعطل استخدام slice::from_raw_parts_mut في الشيفرة 7 عند استعمال الشريحة. تأخذ هذه الشيفرة موقعًا عشوائيًا للذاكرة وتنشئ شريحة يبلغ طولها 10000 عنصر. use std::slice; let address = 0x01234usize; let r = address as *mut i32; let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) }; [الشيفرة 7: إنشاء شريحة من مكان ذاكرة عشوائي] لا نمتلك الذاكرة في هذا الموقع العشوائي وليس هناك ما يضمن أن الشريحة التي تنشئها هذه الشيفرة تحتوي على قيم i32 صالحة، كما تؤدي محاولة استخدام values كما لو كانت شريحة صالحة إلى سلوك غير معرّف. استعمال دوال extern لاستدعاء شيفرة خارجية قد تحتاج شيفرة رست الخاصة بك أحيانًا إلى التفاعل مع شيفرة مكتوبة بلغة برمجية أخرى، لهذا تحتوي رست على الكلمة المفتاحية extern التي تسهل إنشاء واستخدام واجهة الدالة الخارجية Foreign Function interface‎ -أو اختصارًا FFI، وهي طريقة للغة البرمجة لتعريف الدوال وتمكين لغة برمجة (خارجية) مختلفة لاستدعاء هذه الدوال. توضح الشيفرة 8 التكامل مع دالة abs من مكتبة سي القياسية، وغالبًا ما تكون الدوال المعلنة داخل الكتل extern غير آمنة لاستدعائها من شيفرة رست، والسبب هو أن اللغات الأخرى لا تفرض قواعد وضمانات رست ولا يمكن لرست التحقق منها لذلك تقع مسؤولية ضمان سلامتها على عاتق المبرمج. اسم الملف: src/main.rs extern "C" { fn abs(input: i32) -> i32; } fn main() { unsafe { println!("Absolute value of -3 according to C: {}", abs(-3)); } } [الشيفرة 8: التصريح عن الدالة extern واستدعاؤها في لغة أخرى] نُدرج ضمن كتلة ''extern ''C أسماء وبصمات signature الدوال الخارجية من لغة أخرى نريد استدعائها، إذ يحدد الجزء "C" واجهة التطبيق الثنائية application binary interface‎ -أو اختصارًا ABI- التي تستخدمها الدالة الخارجية. تعرّف واجهة التطبيق الثنائية ABI كيفية استدعاء الدالة على مستوى التجميع assembly، وتعد واجهة التطبيق الثنائية للغة ''C'' الأكثر شيوعًا وتتبع واجهة التطبيق الثنائية للغة البرمجة سي. استدعاء دوال رست من لغات أخرى يمكننا أيضًا استخدام extern لإنشاء واجهة تسمح للغات الأخرى باستدعاء دوال رست، وبدلًا من إنشاء كتلة extern كاملة نضيف الكلمة المفتاحية extern ونحدد واجهة التطبيق الثنائية ABI لاستخدامها قبل الكلمة المفتاحية fn للدالة ذات الصلة. نحتاج أيضًا إلى إضافة تعليق توضيحي [no_mangle]# لإخبار مصرّف رست بعدم تشويه mangle اسم هذه الدالة؛ إذ يحدث التشويه عندما يغير المصرف الاسم الذي أعطيناه للدالة لاسم مختلف يحتوي على مزيد من المعلومات لأجزاء أخرى من عملية التصريف لاستهلاكها ولكنها أقل قابلية للقراءة من قبل الإنسان. يشكّل كل مصرف لغة برمجية الأسماء على نحوٍ مختلف قليلًا، لذلك لكي تكون دالة رست قابلة للتسمية من اللغات الأخرى، يجب علينا تعطيل تشويه الاسم في مصرف رست. في المثال التالي نجعل دالة call_from_c قابلة للوصول من شيفرة مكتوبة بلغة سي بعد تصريفها في مكتبة مشتركة وربطها من لغة سي: #[no_mangle] pub extern "C" fn call_from_c() { println!("Just called a Rust function from C!"); } لا يتطلب استعمال extern الكتلة unsafe. الوصول أو تعديل متغير ساكن قابل للتغيير mutable لم نتحدث بعد عن المتغيرات العامة global التي تدعمها رست، إلا أنها قد تسبب مشكلةً مع قواعد ملكية رست. إذا كان هناك خيطان thread يصلان إلى نفس المتغير العام المتغيّر فقد يتسبب ذلك في حدوث سباق بيانات data race. تسمى المتغيرات العامة في رست بالمتغيرات الساكنة، وتظهر الشيفرة 9 مثالًا للتصريح عن متغير ساكن واستخدامه مع شريحة سلسلة نصية مثل قيمة. اسم الملف: src/main.rs static HELLO_WORLD: &str = "Hello, world!"; fn main() { println!("name is: {}", HELLO_WORLD); } [الشيفرة 9: تعريف واستعمال متغير ساكن ثابت] تشبه المتغيرات الساكنة الثوابت التي ناقشناها سابقًا في الفصل المتغيرات والتعديل عليها في لغة رست. أسماء المتغيرات الثابتة موجودة في SCREAMING_SNAKE_CASE اصطلاحًا، ويمكن للمتغيرات الساكنة فقط تخزين المراجع مع دورة حياة ساكنة static'، ما يعني أن مصرف رست يمكنه معرفة دورة الحياة الخاصة دون الحاجة لتحديده صراحةً، ويعد الوصول إلى متغير ساكن آمنًا. الفرق الدقيق بين الثوابت والمتغيرات الساكنة الثابتة immutable هو أن القيم في متغير ساكن لها عنوان ثابت في الذاكرة، كما سيؤدي استعمال القيمة دائمًا إلى الوصول إلى البيانات ذاتها. من ناحية أخرى، يُسمح للثوابت بتكرار بياناتها في أي وقت تُستخدم، الفرق الآخر هو أن المتغيرات الساكنة يمكن أن تكون متغيّرة. الوصول إلى المتغيرات الساكنة القابلة للتغيير وتعديلها غير آمن. توضح الشيفرة 10 كيفية التصريح عن متغير ساكن قابل للتغيير يسمى COUNTER والوصول إليه وتعديله. اسم الملف: src/main.rs static mut COUNTER: u32 = 0; fn add_to_count(inc: u32) { unsafe { COUNTER += inc; } } fn main() { add_to_count(3); unsafe { println!("COUNTER: {}", COUNTER); } } [الشيفرة 10: القراءة من أو الكتابة على متغير ساكن قابل للتغيير غير آمن] كما هو الحال مع المتغيرات العادية نحدد قابلية التغيير باستخدام الكلمة المفتاحية mut، ويجب أن تكون أي شيفرة تقرأ أو تكتب من COUNTER ضمن كتلة unsafe. تُصرَّف هذه الشيفرة وتطبع COUNTER: 3 كما نتوقع لأنها تستخدم خيطًا واحدًا، إذ من المحتمل أن يؤدي وصول خيوط متعددة إلى COUNTER إلى سباق البيانات. يصعب ضمان عدم وجود سباقات بيانات مع البيانات المتغيّرة التي يمكن الوصول إليها بصورةٍعامة، ولهذا السبب تنظر رست إلى المتغيرات الساكنة المتغيّرة بكونها غير آمنة. يُفضّل استخدام تقنيات التزامن والمؤشرات الذكية ذات الخيوط الآمنة التي ناقشناها سابقًا في الفصل استخدام الخيوط Threads لتنفيذ شيفرات رست بصورة متزامنة آنيًا حيثما أمكن حتى يتحقق المصرف من أن البيانات التي يجري الوصول إليها من الخيوط المختلفة آمنة. تنفيذ سمة غير آمنة يمكننا استعمال unsafe لتطبيق سمة غير آمنة؛ وتكون السمة غير آمنة عندما يحتوي أحد توابعها على الأقل على بعض اللامتغايرات invariant التي لا يستطيع المصرف التحقق منها. نصرّح بأن السمة unsafe عن طريق إضافة الكلمة المفتاحية unsafe قبل trait ووضع علامة على أن تنفيذ السمة unsafe أيضًا كما هو موضح في الشيفرة 11. unsafe trait Foo { // methods go here } unsafe impl Foo for i32 { // method implementations go here } fn main() {} [الشيفرة 11: تعريف وتنفيذ سمة غير آمنة] نعد بأننا سنلتزم باللا متغايرات التي لا يمكن للمصرف التحقق منها باستخدام unsafe impl. على سبيل المثال، تذكر سمات العلامة Sync و Send التي ناقشناها سابقًا في قسم "التزامن الموسع مع السمة Sync والسمة Send" في الفصل تزامن الحالة المشتركة Shared-State Concurrency في لغة رست وتوسيع التزامن مع Send و Sync، يطبّق المصرف هذه السمات تلقائيًا إذا كانت أنواعنا تتكون كاملًا من النوعين Sync و Send. إذا طبقنا نوعًا يحتوي على نوع ليس Sync و Send مثل المؤشرات الخام ونريد وضع علامة على هذا النوع على أنه Sync و Send فيجب علينا استخدام unsafe. لا تستطيع رست التحقق من أن النوع الخاص بنا يدعم الضمانات التي يمكن إرسالها بأمان عبر الخيوط أو الوصول إليها من خيوط متعددة، لذلك نحتاج إلى إجراء تلك الفحوصات يدويًا والإشارة إلى ذلك باستخدام unsafe. الوصول لحقول الاتحاد Union الإجراء الأخير الذي يعمل فقط مع unsafe هو الوصول إلى حقول الاتحاد؛ ويعد union مشابهًا للبنية struct ولكن يُستخدم فيه حقل مصرح واحد فقط في نسخة معينة في وقت واحد، وتُستخدم الاتحادات بصورةٍ أساسية للتفاعل مع الاتحادات في شيفرة لغة سي. يعد الوصول إلى حقول الاتحاد غير آمن لأن رست لا يمكنها ضمان نوع البيانات المخزنة حاليًا في نسخة الاتحاد. يمكنك معرفة المزيد عن الاتحادات في توثيق رست Rust Reference. متى تستعمل شيفرة غير آمنة؟ لا يُعد استعمال unsafe لفعل أحد الأفعال الخمسة (القوى الخارقة) التي ناقشناها للتو أمرًا خاطئًا أو غير مرغوب إلا أنه من الأصعب الحصول على شيفرة unsafe صحيحة لأن المصرف لا يستطيع المساعدة بدعم آمان الذاكرة. عندما يكون لديك سببًا لاستخدام شيفرة unsafe تستطيع ذلك، ويسهّل وجود تعليق توضيحي unsafe صريح تعقب مصدر المشكلات عند حدوثها. ترجمة -وبتصرف- لقسم من الفصل Advanced Features من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: صياغة أنماط التصميم الصحيحة Pattern Syntax في لغة رست البرمجة بلغة رست استخدام ميزة تمرير الرسائل Message Passing لنقل البيانات بين الخيوط Threads في لغة رست برمجة لعبة تخمين الأرقام بلغة رست
  21. لتحديد ما هو تعقيد big O لجزء من الشيفرة الخاصة علينا إنجاز أربع مهام، هي: تحديد ما هي n، عدّ الخطوات في الشيفرة، إسقاط المراتب الدُنيا، وإسقاط المعاملات. مثلًا، لنجد big O الخاص بالدالة readingList()‎: def readingList(books): print('Here are the books I will read:') numberOfBooks = 0 for book in books: print(book) numberOfBooks += 1 print(numberOfBooks, 'books total.') تذكر أن n تُمثل حجم بيانات الدخل التي تعمل عليها الشيفرة. تعتمد n في الدوال عادةً على المعامل، ومعامل الدالة readingList()‎ الوحيد هو books، لذا سيعطينا حجم books تصوّرًا جيدًا عن قيمة n، لأنه كلما زادت books يزداد وقت تنفيذ الدالة. الآن، لنعدّ الخطوات في هذه الشيفرة، ولكن ما يمكننا أن نعدّه خطوة STEP مبهمًا إلى حد ما، لكننا سنتبّع سطر الشيفرة بمثابة قاعدة. لدى الحلقات عدد خطوات بعدد التكرارات مضروبًا بعدد أسطر الشيفرة في الحلقة، ولمعرفة ماذا يعني ذلك، هذا تعداد خطوات الشيفرة داخل الدالة readingList()‎: def readingList(books): print('Here are the books I will read:') # خطوة واحدة numberOfBooks = 0 # خطوة واحدة for book in books: # ‫n مضروبًا بعدد الخطوات في هذه الحلقة print(book) # خطوة واحدة numberOfBooks += 1 # خطوة واحدة print(numberOfBooks, 'books total.') # خطوة واحدة نعامل كل سطر من الشيفرة على أنه خطوة ما عدا حلقة for، إذ يُنفذ السطر مرةً لكل عنصر في books، ولأن n هي حجم books، يمكننا القول إنه ينفِّذ n خطوة. ليس هذا فقط بل إنه ينفِّذ كل الخطوات داخل الحلقة n مرة، لأن هناك خطوتان داخل الحلقة، يكون المجموع 2×n خطوة. يمكننا وصف خطواتنا على النحو التالي: def readingList(books): print('Here are the books I will read:') # خطوة واحدة numberOfBooks = 0 # خطوة واحدة for book in books: # ‫n مضروبًا 2 خطوة print(book) # خطوة معدودة مسبقًا numberOfBooks += 1 # خطوة معدودة مسبقًا print(numberOfBooks, 'books total.') #خطوة واحدة عندما نحسب عدد الخطوات الكلي، سنحصل على ‎1+1+1‎+(n×2)‎. يمكننا كتابة هذا التعبير على نحوٍ أبسط 2n+3. ليس الهدف من big O حساب دقائق الأمور بل هو مؤشر عام، لذا نُسقط المراتب الأدنى من العد. المرتبة 2n+3 هي (2n) خطية و 3 هو ثابت. إذا ابقينا فقط أعلى مرتبة نحصل على 2n، ثم نُسقط المعاملات من المرتبة؛ ففي 2n المعامل هو 2، وبعد إسقاطه سيتبقى لنا n، وهذا يعطينا big O النهائي للدالة readingList()‎ الذي هو O(n)‎ أو تعقيد وقت خطي. هذا الترتيب منطقي إذا فكرت فيه، فهناك عدة خطوات في الدالة الخاصة بنا، ولكن عمومًا إذا زادت قائمة books عشر مرات، يزداد وقت التنفيذ عشرة أضعاف أيضًأ. تغير زيادة books من 10 كتب إلى 100 كتاب الخوارزمية من 1 + (2 × 10) +1 + 1 أو 23 خطوة إلى 1 + (2 × 100) +1+ 1 أو 203 خطوات. الرقم 203 هو تقريبًا عشرة أضعاف 23، لذا يزداد وقت التنفيذ بالتناسب مع ازدياد n. لماذا تكون المراتب الدنيا والمعاملات غير مهمة؟ نُسقط المراتب الأدنى من عد الخطوات لأنها تُصبح أقل أهمية مع ازدياد حجم n؛ فإذا زدنا قائمة books في الدالة السابقة readingList()‎ من 10 إلى 10,000,000,000 (10 مليار)، يزداد عدد الخطوات من 23 إلى 20,000,000,003 ولا تهم هذه الخطوات الثلاث الإضافية مع رقم كبير كهذا. لا يشكّل معاملًا coefficient كبيرًا لمرتبة صغيرة مع ازدياد كمية البيانات أي فرق مقارنة مع المراتب الأعلى، فعند حجم n معين، ستكون المراتب الأعلى دائما أبطأ من المراتب الأدنى، مثلًا لنقل إنه لدينا quadraticExample()‎ الذي هو O(n2)‎ وفيه 3n2 خطوة. لدينا أيضًا linearExample()‎ الذي هو O(n)‎ وفيه 1000n خطوة. لا يهم إذا كان المعامل 1000 أكبر من المعامل 3، فكلما زادت n ستكون بالنهاية العملية O(n2)‎ أبطأ من العملية الخطية O(n)‎. لا تهم الشيفرة تحديدًا ولكننا يمكن أن نفكر بها على النحو التالي: def quadraticExample(someData): # ‫n هو حجم someData for i in someData: # ‫n خطوة for j in someData: # ‫n خطوة print('Something') # خطوة واحدة print('Something') # خطوة واحدة print('Something') # خطوة واحدة def linearExample(someData): # ‫n هو حجم someData for i in someData: # ‫n خطوة for k in range(1000): # ‫1000 خطوة print('Something') # خطوة معدودة مسبقًا لدى الدالة linearExample()‎ معامل كبير (1000) مقارنة بالمعامل (3) الخاص بدالة quadraticExample. إذا كان حجم الدخل n هو 10، تظهر الدالة O(n2)‎ أسرع فقط في الخطوات ال 300 الخاصة به مقارنةً بخطوات الدالة O(n)‎ البالغ عددها 10000 خطوة. يهتم ترميز big O بصورةٍ أساسية بأداء الخوارزمية كلما تدرجت كمية العمل، فعندما يصل n إلى حجم 334 أو أكبر، ستكون الدالة quadraticExample()‎ دائمًا أبطأ من الدالة linearExample()‎، حتى لو كانت الدالة ()linearExample تتطلب 1,000,000n خطوة، ستصبح الدالة quadraticExample()‎ أبطأ عندما تصل n إلى 333,334. ستصبح العملية O(n2)‎ أبطأ من O(n)‎ أو المرتبة الأدنى. لمشاهدة ذلك لاحظ مخطط big O المبين بالشكل 3 التالي، الذي يبين كل مراتب ترميز big O. المحور X هو n حجم البيانات، والمحور y هو وقت التنفيذ اللازم لإنهاء العملية. [الشكل 3: مخطط مراتب big O] كما ترى، يزداد وقت التنفيذ بمعدل أسرع للمراتب الأعلى من المراتب الأدنى. على الرغم من أن المراتب الأدنى يمكن أن يكون لها معاملات أكبر مما يجعلها أكبر مؤقتًا من المراتب الأعلى، ستسبقهم المراتب الأعلى في النهاية. أمثلة عن تحليل Big O لنحدد مرتبة big O لبعض أمثلة الدوال، إذ سنستخدم في هذه الأمثلة المعامل المسمى books الذي هو قائمة سلسلة نصية لعناوين كتب. تحسب دالة countBookPoints()‎ النتيجة score اعتمادًا على عدد books في قائمة الكتب، معظم الكتب قيمتها نقطة وبعض الكتب لكُتّاب معينين قيمتها نقطتين. def countBookPoints(books): points = 0 # خطوة واحدة for book in books: # ‫n خطوة مضروبًا بعدد تكرارات الحلقة points += 1 # خطوة واحدة for book in books: # ‫n خطوة مضروبًا بعدد تكرارات الحلقة if 'by Al Sweigart' in book: # خطوة واحدة points += 1 # خطوة واحدة return points # خطوة واحدة يصبح عدد الخطوات ‎1+ (n × 1) + (n × 2) + 1 الذي يصبح 3n + 2 بعد جمع الحدود المتشابهة، وبعد إسقاط المراتب الأدنى والمعاملات يصبح O(n)‎ (تعقيد خطي)، حتى لو مررنا على books مرةً أو مرتين أو مليار مرة. تستخدم كل الأمثلة حتى الآن حلقةً واحدةً بتعقيد خطي، ولكن هذه الحلقات تتكرر n مرة. سنرى في الأمثلة اللاحقة أن حلقة وحيدة في الشيفرة لا تُمثل تعقيدًا خطيًا بينما تمثّل حلقةً تمرّ على كل البيانات الخاصة بك ذلك. تطبع دالة iLoveBooks()‎ جملة "I LOVE BOOKS!!!‎" أو "BOOKS ARE GREAT!!!‎" عشر مرات: def iLoveBooks(books): for i in range(10): # ‫10 خطوات مضروبًا بعدد تكرارات الحلقة print('I LOVE BOOKS!!!') # خطوة واحدة print('BOOKS ARE GREAT!!!') # خطوة واحدة لدى هذه الدالة حلقة for ولكنها لا تمرّ على قائمة books وتجري عشرين خطوة مهما كان حجم books. يمكننا إعادة كتابة ذلك كما يلي: (1)20، وبعد إسقاط المعامل 20، يبقى لدينا O(1)‎ أو تعقيد زمن ثابت. هذا منطقي لأن الدالة تستغرق نفس زمن التنفيذ مهما كان n حجم قائمة books. لاحقًا، لدينا الدالة cheerForFavoriteBook()‎ التي تبحث في قائمة books لإيجاد كتاب مفضل: def cheerForFavoriteBook(books, favorite): for book in books: # ‫n خطوة مضروبًا بعدد تكرار الحلقة print(book) # خطوة واحدة if book == favorite: # خطوة واحدة for i in range(100): # ‫100 خطوة مضروبًا بعدد خطوات الحلقة print('THIS IS A GREAT BOOK!!!') # خطوة واحدة تمرّ حلقة for book على قائمة books التي تتطلب n خطوة مضروبةً بعدد الخطوات داخل الحلقة، وتضم هذه الحلقة حلقة for i مضمّنة تتكرر مئة مرة، وهذا يعني أن حلقة for book تتكرر 102‎×n مرة أو 102n خطوة. سنجد بعد إسقاط المعامل أن cheerForFavoriteBook()‎ هي عملية O(n)‎ خطية. قد يكون المعامل 102 كبيرًا لكي يُهمل لكن خذ بالحسبان إذا لم تظهر favorite أبدًا في قائمة books ستُنفذ الدالة 1n خطوة فقط. يختلف تأثير المعاملات كثيرًا ولذا هي ليست ذات معنى كبير. تبحث الدالة findDuplicateBooks()‎ في قائمة books (عملية خطية) مرةً لكل كتاب (عملية خطية أُخرى): def findDuplicateBooks(books): for i in range(books): # ‫n خطوة for j in range(i + 1, books): # ‫n خطوة if books[i] == books[j]: # خطوة واحدة print('Duplicate:', books[i]) # خطوة واحدة تمرّ حلقة for i على كل قائمة books وتُنفذ الخطوات داخل الحلقة n مرة. وتمرّ الحلقة for j على جزء من قائمة الكتب، على الرغم من أننا نُسقط المعاملات، لا تزال هذه العملية بتعقيد وقت خطي. هذا يعني أن حلقة for i تنجز n×n عملية أي n2، وهذا يعني أن الدالة findDuplicateBooks()‎ هي عملية بوقت تنفيذ متعدد الحدود polynomial time operation أي O(n2)‎. لا تشير الحلقات المتداخلة Nested loops لوحدها أنها عمليات متعددة الحدود، ولكن الحلقات المتداخلة التي تكرر فيها الحلقات n مرة تُنتج n2 خطوة، مما يدل على عملية O(n2)‎. لنتابع على مثال أصعب؛ إذ تعمل عملية البحث الثنائي المذكورة سابقًا عن طريق البحث في منتصف القائمة المرتبة (سنسميها haystack) عن عنصر (سنسميه needle). إذا لم نجد needle هنا، سنكمل البحث في النصف التالي أو اللاحق من haystack اعتمادًا على أي نصف نتوقع فيه إيجاد needle فيه. نكرر هذه العملية عن طريق البحث عن الأنصاف الأصغر والأصغر حتى نجد needle أو ننهي العمل إذا لم تكن في haystack. لاحظ أن البحث الثنائي يعمل فقط مع العناصر في haystack مرتبة: def binarySearch(needle, haystack): if not len(haystack): # خطوة واحدة return None # خطوة واحدة startIndex = 0 # خطوة واحدة endIndex = len(haystack) - 1 # خطوة واحدة haystack.sort() # عدد غير معلوم من الخطوات while start <= end: # عدد غير معلوم من الخطوات midIndex = (startIndex + endIndex) // 2 # خطوة واحدة if haystack[midIndex] == needle: # خطوة واحدة # Found the needle. return midIndex # خطوة واحدة elif needle < haystack[midIndex]: # خطوة واحدة # Search the previous half. endIndex = midIndex - 1 # خطوة واحدة elif needle > haystack[mid]: # خطوة واحدة # Search the latter half. startIndex = midIndex + 1 # خطوة واحدة هناك سطرين في binarySearch()‎ ليس من السهل عدهما، إذ يعتمد ترميز big O الخاص باستدعاء الدالة haystack.sort()‎ على الشيفرة داخل وحدة sort()‎ الخاصة ببايثون. ليست هذه الشيفرة سهلة الإيجاد ولكن يمكنك معرفة ترميز big O على الإنترنت والذي هو O(n log n)‎. كل خوارزميات الترتيب العامة هي في أفضل الأحوال O(n log n)‎. سنغطي ترميز big O لعدد من التوابع والدوال الخاصة بلغة بايثون في فقرة "مراتب Big O لاستدعاءات الدوال العامة" لاحقًا في هذا الفصل. حلقة while ليست بسيطة التحليل مثل حلقات for التي رأيناها، إذ علينا فهم خوارزمية البحث الثنائي لتحديد كم تكرار موجود في الحلقة. تغطي startIndex و endIndex قبل الحلقة كل مجال haystack وضُبطت midIndex في منتصف المجال. في كل تكرار لحلقة while يحصل واحد من شيئين: إذا كان haystack[midIndex] == needle نعرف أننا وجدنا needle وتُعيد الدالة الفهرس needle في haystack. إذا كان needle < haystack[midIndex]‎ أو needle > haystack[midIndex]‎ سيُنصّف المجال المُغطى من startIndex و endIndex، إما عن طريق تعديل startIndex أو endIndex. عدد المرات التي يمكن تقسيم أي قائمة حجمها n إلى النصف هو log2(n)‎. لذا، لدى حلقة while مرتبة big O هي O(log n)‎K ولكن لأن مرتبة O(n log n) ‎ في سطر haystack.sort()‎ هي أعلى من O(log n)‎، نسقط مرتبة O(log n)‎ الأدنى وتصبح مرتبة big O لكل الدالة binarySearch()‎ هي O(n log n)‎. إذا ضمنا أن binarySearch()‎ ستُستدعى فقط على قائمة مرتبة من haystack، يمكننا إزالة السطر haystack.sort()‎ وجعل binarySearch()‎ دالة O(log n)‎. يحسّن هذا تقنيًا من كفاءة الدالة ولكن لا يجعل البرنامج أكثر كفاءة لأنه ينقل عمل الترتيب المطلوب إلى قسم آخر من البرنامج. تترك معظم تطبيقات البحث الثنائي خطوة الترتيب وبالتالي نقول أن خوارزميات البحث الثنائي لها تعقيد لوغاريتمي O(log n)‎. مراتب Big O لاستدعاءات الدالة الشائعة يجب أن يأخذ تحليل big O مرتبة big O بالحسبان لأي دالة يجري استدعاؤها. إذا كتبت دالةً يمكنك تحليل الشيفرة الخاصة بك، ولكن لمعرفة مرتبة big O لدوال وتوابع بايثون المضمّنة يجب عليك إرجاعها إلى قوائم. تحتوي هذه القائمة مراتب big O لعمليات بايثون الشائعة لأنواع المتتاليات مثل السلاسل النصية والصفوف tuples والقوائم: s[i] reading and s[i] = value assignment هي عمليات O(1)‎. s.append(value)‎ هي عملية O(1)‎. s.insert(i, value)‎ هي عملية O(n)‎. يحتاج إدخال قيم في متتالية (خاصة من المقدمة) إلى إزاحة كل القيم إلى الأعلى في الفهارس فوق i بمكان واحد في التسلسل. s.remove(value)‎ هي عملية O(n)‎. يحتاج إزالة قيم في متتالية (خاصة من المقدمة) إلى إزاحة كل القيم إلى الأسفل في الفهارس فوق I بمكان واحد في التسلسل. s.reverse()‎ هي عملية O(n)‎ لأنه يجب إعادة ترتيب كل عنصر في المتتالية. s.sort()‎ هي عملية O(n log n)‎ لأن خوارزمية الترتيب الخاصة ببايثون هي O(n log n)‎. value in s هي عملية O(n)‎ لأنه يجب التحقق من كل عنصر. :for value in s عملية O(n)‎ len(s)‎ هي عملية O(n) ‎لأن بايثون يتابع كم عنصر موجود في المتتالية لذا لا يحتاج لإعادة عدهم عندما يُمرّر إلى len()‎ تحتوي هذه القائمة على مراتب big O لعمليات بايثون الشائعة أنواع الربط مثل القواميس والمجموعات sets والمجموعات الجامدة frozensets: m[key] reading and m[key] = value assignment عمليات O(1)‎. m.add(value)‎ عملية O(1)‎. value in m عمليات O(1)‎ للقواميس التي هي أسرع باستخدام in مع المتتاليات. for key in m: عملية O(n)‎. len(m)‎ عملية O(1)‎ لأن بايثون يتابع كم عنصر موجود في الربط لذا لا يحتاج لإعادة عدهم عندما يمرر إلى len()‎. على الرغم من أن القوائم تحتاج عمومًا إلى البحث عن كل العناصر من بدايتها حتى نهايتها، لكن القواميس تستخدم المفتاح لحساب العنوان والوقت اللازم للبحث عن قيمة المفتاح يبقى ثابتًا. يسمى هذا الاستدعاء خوارزمية التعمية Hashing Algorithm والعنوان يسمى التعمية hash. التعمية هي خارج نطاق هذا الكتاب ولكن يمكنك الاطلاع على مفهوم التعمية والدوال المرتبطة بها على موقع حسوب من خلال الرابط، إذ أنها السبب في جعل العديد من عمليات الربط بوقت ثابت O(1)‎. تستخدم المجموعات التعمية أيضًا لأن المجموعات هي قواميس بحقيقتها ولكن بوجود مفاتيح بدلًا من أزواج مفتاح-قيمة. لكن تحويل القائمة إلى مجموعة هو عملية بتعقيد O(n)‎ لذا لا يفيد تحويل القائمة إلى مجموعة ومن ثم الوصول إلى العناصر في تلك المجموعة. تحليل Big O بنظرة سريعة عندما تألف تنفيذ تحليل big O فأنت لست بحاجة لفعل كل الخطوات، إذ ستشاهد بعد فترة بعض الدلالات التي يمكن منها تحديد مرتبة big O بسرعة. لاحظ أن n هي حجم البيانات التي تعمل عليها الشيفرة، هذه بعض القواعد العامة المُستخدمة: إذا كانت الشيفرة لا تصل إلى أي بيانات هي O(1)‎. إذا كانت الشيفرة تمر على البيانات تكون O(n)‎. إذا كانت الشيفرة فيها حلقتين متداخلتين تمرّان على البيانات O(n2)‎. لا تُعدّ استدعاءات الدالة خطوة واحدة بل هي عدد الخطوات داخل الدالة. راجع فقرة "مرتبة Big O لاستدعاءات الدالة العامة" في الأعلى. إذا كانت للشيفرة عملية "فرّق تسد" التي تنصف البيانات تكون O(log n)‎. إذا كانت للشيفرة عملية "فرّق تسد" التي تُنفذ مرة لكل عنصر في البيانات تكون O(n log n)‎. إذا كانت الشيفرة تمر على كل مجموعة ممكنة من القيم في البيانات n تكون O(n2)‎ أو مرتبة أسية أُخرى. إذا كانت الشيفرة تمر على كل تبديل (أي ترتيب) للقيم في البيانات تكون O(n!)‎. إذا كانت الشيفرة تتضمن ترتيب البيانات تكون على الأقل O(n log n)‎. هذه القيم هي نقطة انطلاق جيدة ولكن لا يوجد بديل لتحليل big O. تذكر أن مرتبة big O ليست الحكم النهائي على الشيفرة إذا ما كانت سريعة أو بطيئة أو فعّالة. لنرى الدالة waitAnHour()‎: import time def waitAnHour(): time.sleep(3600) تقنيًا هذه الدالة waitAnHour()‎ هي وقت ثابت O(1)‎، نفكّر دائمًا أن شيفرة الوقت الثابت سريعة، ولكن وقت تنفيذها هو ساعة. هل هذه الشيفرة فعّالة؟ لا، ولكن من الصعب تحسين برمجة دالة waitAnHour()‎ تستطيع أن تُنفذ بأسرع من ساعة. ترميز Big O ليس بديلًا عن تحليل الشيفرة الخاصة بك، وإنما الهدف منه هو إعطاؤك نظرةً عن أداء الشيفرة مع زيادة كمية البيانات المُدخلة. لا يفيدنا Big O عندما تكون n صغيرة وعادة ما تكون n صغيرة ربما ستكون متسرعًا لتحليل كل قطعة شيفرة تكتبها مع هذه المعلومات عن ترميز big O. قبل أن تستخدم المطرقة التي بيدك (شيفرة بايثون) لكل مسمار تراه، خذ بالحسبان أن ترميز big O يفيد أكثر عندما يكون هناك كميات كبيرة من البيانات لمعالجتها، وفي الواقع أغلب كميات البيانات هي صغيرة، وفي مثل تلك الحالات، لا يستحق إنشاء خوارزميات مُنمقة ومتطورة مع مراتب big O منخفضة ذلك العناء. لدى مصمم لغة البرمجة جو Go روب بايك Rob Pike خمس قواعد عن البرمجة، واحدة منها هي: "الخوارزميات المنمّقة بطيئة عندما تكون 'n' صغيرة وبالعادة 'n' صغيرة". لن يواجه معظم مطورو البرمجيات مراكز بيانات كبيرة أو عمليات حسابية معقدة، بل برامج أبسط من ذلك، وفي هذه الحالات سيعطي تنفيذ الشيفرة مع محلًل معلومات profiler أدق عن أداء الشيفرة بدلًا من تحليل big O. الخلاصة خذ بالحسبان أن big O هي أداة تحليل مفيدة، ولكنها ليست بديل عن تنفيذ الشيفرة مع محلًل لمعرفة أين يوجد عنق الزجاجة، إذ تساعدك المعرفة بترميز big O وكيفية تباطؤ الشيفرة مع زيادة البيانات في تجنُّب كتابة الشيفرة في مراتب أبطأ من حاجتها. ترجمة -وبتصرف- لقسم من الفصل Measuring Performance And Big O Algorithm Analysis من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: ترميز Big O وحساب مراتب تعقيد الخوارزميات خوارزميات البحث المرجع الشامل إلى تعلم الخوارزميات للمبتدئين دليل شامل عن تحليل تعقيد الخوارزمية المرجع الشامل إلى تعلم لغة بايثون
  22. سنجمع في هذا المقال الصياغة الصالحة في الأنماط وسنتحدث عن مكان استخدام كل واحد منها. مطابقة القيم المجردة Literals يمكننا مطابقة الأنماط مباشرةً مع القيم المجرّدة كما رأينا سابقًا في الفصل التعدادات enums في لغة رست. تمنحنا الشيفرة البرمجية التالية بعض الأمثلة: let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("anything"), } تطبع هذه الشيفرة one لأن القيمة في x هي 1. تُعد هذه الصياغة مفيدة عندما تريد من الشيفرة أن تنفّذ عملًا ما عندما تحصل على قيمة معينة واحدة. مطابقة المتغيرات المسماة Named Variables المتغيرات المُسمّاة هي أنماط غير قابلة للجدل تطابق أي قيمة، وقد استخدمناها مرات عديدة سابقًا، ولكن هناك تعقيدات عند استخدامها في تعابير match. ينشئ تعبير match نطاقًا scope جديدًا، وبالتالي ستُخفي المتغيرات المُصرّح عنها على أنها جزء من النمط داخل match المتغيرات التي تحمل الاسم ذاته خارج هيكل match كما هو الحال في جميع المتغيرات. صرّحنا ضمن الشيفرة 11 عن متغير مُسمى x قيمته Some(5)‎ ومتغير y قيمته 10، ثم أنشأنا تعبير match على القيمة x. ألقِ نظرةً على الأنماط في أذرع المطابقة و !println وحاول اكتشاف ماذا ستطبع الشيفرة قبل تنفيذ هذه الشيفرة أو متابعة القراءة. اسم الملف: src/main.rs let x = Some(5); let y = 10; match x { Some(50) => println!("Got 50"), Some(y) => println!("Matched, y = {y}"), _ => println!("Default case, x = {:?}", x), } println!("at the end: x = {:?}, y = {y}", x); [الشيفرة 11: تعبير match مع ذراع يتسبب بظهور متغير خفي y] لنرى ما سيحصل عند تنفيذ تعبير match؛ إذ لا تتطابق القيمة المُعرّفة x مع ذراع المطابقة الأول في النمط لذا يستمر تنفيذ الشيفرة. يتسبب النمط الموجود في ذراع المطابقة الثاني بإنشاء متغير جديد باسم y وهو يطابق أي قيمة داخل قيمة Some، وذلك لأننا في نطاق جديد داخل تعبير match، هذا المتغير الجديد yهو ليس المتغير y ذاته الذي صرّحنا عنه في البداية بقيمة 10. يطابق الإسناد الجديد للمتغير y أي قيمة داخل Some وهي القيمة الموجودة في x، وبالتالي ترتبط y الجديدة بقيمة Some الداخلية في x، وتبلغ تلك القيمة 5، لذلك يُنَفَذ تعبير الذراع ويطبع Matched, y = 5. لن تُطابق الأنماط في ذراعي النمط الأولين إذا كانت القيمة في x هي None بدلًا من Some(5)‎، لذا ستُطابق القيم مع الشرطة السفلية underscore. لم نُضِف المتغير x في نمط ذراع الشرطة السفلية، لذلك يبقى x في التعبير هو المتغير x الخارجي الذي لم يُخفى، وتطبع match في هذه الحالة الافتراضية Default case, x = None. عندما ينتهي تعبير match ينتهي نطاقه أيضًا، وينتهي أيضًا نطاق المتغير y الداخلي. تطبع آخر تعليمة println!‎ ما يلي: at the end: x = Some(5), y = 10 يجب علينا استخدام درع مطابقة شرطي match guard conditional لإنشاء تعبير match يقارن قيم x و y الخارجية عوضًا عن تقديم متغير خفي، وسنتحدث عن ذلك لاحقّا في قسم "الشرطيات الإضافية مع دروع المطابقة". الأنماط المتعددة Multiple Patterns يمكننا مطابقة عدة أنماط في تعبير match باستخدام الصياغة | التي يمكن أن تكون النمط أو المعامل. نطابق في المثال التالي قيمة x مع أذرع المطابقة، بحيث يحتوي أول ذراع على الخَيَار "أو or"، بمعنى أنه إذا طابقت القيمة x أحد القيمتين في الذراع تُنفَّذ شيفرة الذراع كما يلي: let x = 1; match x { 1 | 2 => println!("one or two"), 3 => println!("three"), _ => println!("anything"), } تطبع الشيفرة السابقة ما يلي: one or two مطابقة مجالات القيم باستخدام الصيغة =.. تسمح صيغة =.. بمطابقة مجال شامل من القيم، إذ تُنفَّذ الشيفرة البرمجية الخاصة بالذراع عندما يطابق النمط أي قيمة في مجال ما كما في الشيفرة التالية: let x = 5; match x { 1..=5 => println!("one through five"), _ => println!("something else"), } تتطابق الذراع الأولى إذا كانت قيمة x هي 1 أو 2 أو 3 أو 4 أو 5، هذه الصياغة ملائمة لمطابقة قيم متعددة بدلًا من استخدام المعامل | للتعبير عن الفكرة ذاتها؛ إذا أردنا استخدام | فيجب تحديد 5 | 4 | 3 | 2 | 1، وستكون طريقة تحديد المجال أقصر خاصةً إذا أردنا مطابقة أي رقم بين 1 و 1,000. يتحقق المصرّف وقت التصريف من أن المجال غير فارغ لأن أنواع المجالات التي تستطيع رست تمييز ما إذا كانت فارغة أم لا هي char والقيم العددية، إذ يُسمح باستخدام المجالات فقط مع القيم العددية أو قيم char. إليك مثالًا يستخدم مجال من قيم char: let x = 'c'; match x { 'a'..='j' => println!("early ASCII letter"), 'k'..='z' => println!("late ASCII letter"), _ => println!("something else"), } تميز رست أن 'c' تقع داخل مجال النمط الأول وتطبع early ASCII letter. التفكيك Restructuring لتجزئة القيم يمكننا استخدام الأنماط لتفكيك الهياكل أو المعدّدات enums أو الصفوف tuples لاستخدام الأجزاء المختلفة من القيم، لنستعرض كل حالة من الحالات السابقة. تفكيك الهياكل تظهر الشيفرة 12 هيكل Point بحقلين x و y يُمكن تجزئتهما باستخدام نمط مع التعليمة let. اسم الملف:src/main.rs struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; let Point { x: a, y: b } = p; assert_eq!(0, a); assert_eq!(7, b); } [الشيفرة 12: تفكيك حقل هيكل إلى متغيرات منفصلة] تُنشئ الشيفرة السابقة المتغيرين a و b اللذين يطابقان قيمتي الحقلين x و y في الهيكل p، ويوضح هذا المثال أنه ليس من الضروري لأسماء المتغيرات في النمط أن تُطابق أسماء الحقول في الهيكل، ولكن من الشائع مطابقة أسماء المتغيرات مع أسماء الحقول لتذكر ارتباط المتغير بحقل معين. بما أن كتابة ما يلي مثلًا: let Point { x: x, y: y } = p;‎ تحتوي على الكثير من التكرار، لدى رست طريقةً مختصرة للأنماط التي تطابق حقول الهيكل؛ إذ عليك فقط أن تُضيف اسم حقل الهيكل مما يجعل المتغيرات المُنشأة من هذا النمط تحمل الاسم ذاته. تعمل الشيفرة 13 بطريقة عمل الشيفرة 12 ذاتها إلا أن المتغيرات المُنشأة في النمط let هي x و y بدلًا من a و b. اسم الملف: src/main.rs struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; let Point { x, y } = p; assert_eq!(0, x); assert_eq!(7, y); } [الشيفرة 13: تفكيك حقول هيكل باستخدام طريقة حقل الهيكل المختصرة] تُنشئ الشيفرة المتغيرين x و y اللذين يطابقان الحقلين x و y الخاصين بالهيكل p، والنتيجة هي احتواء المتغيرين x و y على القيم الموجودة في الهيكل p. يمكننا إجراء عملية التفكيك باستخدام القيم المجردة مثل جزء من نمط الهيكل بدلًا من إنشاء متغيرات لكل الحقول، ويسمح لنا ذلك باختبار بعض الحقول لقيم معينة أثناء إنشاء متغيرات لتفكيك حقول أخرى. لدينا في الشيفرة 14 تعبير match يقسم قيم Point إلى ثلاث حالات: نقاط تقع مباشرةً على محور x (الذي يُعدّ محققًا عندما y = 0)، ونقاط تقع على المحور y (أي x = 0)، ونقاط لا تقع على أي من المحورين. اسم الملف: src/main.rs fn main() { let p = Point { x: 0, y: 7 }; match p { Point { x, y: 0 } => println!("On the x axis at {x}"), Point { x: 0, y } => println!("On the y axis at {y}"), Point { x, y } => { println!("On neither axis: ({x}, {y})"); } } } [ الشيفرة 14: تفكيك ومطابقة القيم المجردة في نمط واحد] ستتطابق الذراع الأولى مع أي نقطة تقع على المحور x عن طريق تحديد أن الحقل y يقابل القيمة المجردة 0، ويُنشئ النمط متغير x يمكن استخدامه في شيفرة هذا الذراع؛ وبصورةٍ مشابهة، ستتطابق الذراع الثانية مع أي نقطة تقع على المحور y عن طريق تحديد أن الحقل x يقابل القيمة 0 ويُنشئ النمط متغير y للقيمة في الحقل y، ولا تحدد الذراع الثالثة أي قيمة مجرّدة لذا تُطابق أي قيمة Point أُخرى ويُنشأ متغيران لكل من الحقلين x و y. تطابِق القيمة p الذراع الثانية في هذا المثال بفضل احتواء x على 0 وتطبع هذه الشيفرة ما يلي: On the y axis at 7. تذكر أن تعبير match يتوقف عن التحقق من الأذرع عندما يجد أول نمط مطابق لذا حتى لو كانت Point { x: 0, y: 0}‎ موجودةً على المحورين x و y ستطبع الشيفرة فقط On the x axis at 0. تفكيك المعددات فكّكنا سابقًا المعدّدات (الشيفرة 5 في الفصل بنية match للتحكم بسير برامج لغة رست Rust) إلا أننا لم نتحدث صراحةً أن النمط لتفكيك المعدّد يوافق طريقة تخزين البيانات داخله. تستخدم الشيفرة 15 معدّدًا يدعى Message من الشيفرة 2 من الفصل التعدادات enums في لغة رست وتُكتب match مع أنماط تفكك كل من القيم الداخلية الخاصة بذلك المعدّد. اسم الملف: src/main.rs enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() { let msg = Message::ChangeColor(0, 160, 255); match msg { Message::Quit => { println!("The Quit variant has no data to destructure."); } Message::Move { x, y } => { println!("Move in the x direction {x} and in the y direction {y}"); } Message::Write(text) => { println!("Text message: {text}"); } Message::ChangeColor(r, g, b) => { println!("Change the color to red {r}, green {g}, and blue {b}",) } } } [الشيفرة 15: تفكيك متغايرات المعدّد التي تحتوي على أنواع مختلفة من القيم] تطبع الشيفرة السابقة ما يلي: Change the color to red 0, green 160, and blue 255 حاول تغيير قيمة msg لملاحظة تنفيذ الشيفرة البرمجية للأذرع الأُخرى. لا يمكننا تفكيك القيم أكثر من ذلك في متغيرات المعدّد التي لا تحتوي على أي بيانات مثل Message::Quit، إذ يمكننا فقط مطابقة القيمة المجرّدة Message::Quit ولا يوجد أي متغيرات في النمط. يمكن استخدام أنماط مشابهة للنمط الذي نحدده لمطابقة الهياكل من أجل متغايرات المعدّدات التي تشبه الهيكل مثل Message::Move، إذ نضع أقواس معقوصة بعد اسم المتغير وبعدها نضع الحقول مع المتغيرات لتجزئتها واستخدامها في شيفرة الذراع، واستخدمنا هنا الطريقة المُختصرة كما فعلنا في الشيفرة 13. يشابه النمط الذي نستخدمه لمتغيرات المعدّدات التي تشبه الصفوف مثل Message::Write الذي يحتوي على صف مع عنصر واحد و Message::ChangeColor الذي يحتوي صف مع ثلاثة عناصر للنمط الذي نحدده لمطابقة الصفوف، ويجب أن يطابق عدد المتغيرات في النمط عدد العناصر في المتغير المُراد مطابقته. تفكيك الهياكل والمعددات المتداخلة كانت أمثلتنا حتى الآن مقتصرةً على مطابقة الهياكل structs والمعدّدات enums بعمق طبقة واحدة، إلا أن المطابقة تعمل على العناصر المتداخلة أيضًا، فعلى سبيل المثال يمكن إعادة بناء الشيفرة 15 لتدعم ألوان RGB و HSV في رسالة ChangeColor كما تبين الشيفرة 16. enum Color { Rgb(i32, i32, i32), Hsv(i32, i32, i32), } enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(Color), } fn main() { let msg = Message::ChangeColor(Color::Hsv(0, 160, 255)); match msg { Message::ChangeColor(Color::Rgb(r, g, b)) => { println!("Change color to red {r}, green {g}, and blue {b}"); } Message::ChangeColor(Color::Hsv(h, s, v)) => { println!("Change color to hue {h}, saturation {s}, value {v}") } _ => (), } } [الشيفرة 16: مطابقة معدّدات متداخلة] يتطابق النمط في الذراع الأولى في تعبير match مع متغاير معدد Message::ChangeColor، الذي يحتوي على متغاير معدد Color::Rgb، ويرتبط النمط مع قيم i32 الداخلية الثلاث، بينما يتطابق نمط الذراع الثانية مع متغاير معدد Message::ChangeColor ويطابق المعدّد الداخلي Color::Hsv، ويمكن تحديد هذه الشروط المعقدة في تعبير match واحد حتى لو كان هناك معدّدان. تفكيك الهياكل والصفوف يمكننا مزج ومطابقة وتضمين الأنماط المفككة بطرق معقدة أكثر، ويبين المثال التالي تفكيك معقد، إذ نضمّن هياكل وصفوف داخل صف ونفكك جميع القيم الأولية primitive: let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 }); تسمح لنا الشيفرة السابقة بتجزئة الأنواع المعقدة إلى الأجزاء المكونة لها لاستخدام القيم التي نريدها على نحوٍ منفصل. تُعد التجزئة مع الأنماط طريقةً مناسبة لاستخدام أجزاء من القيم مثل قيمة من كل حقل في هيكل منفصلين عن بعضهم. تجاهل القيم في نمط من المفيد أحيانًا تجاهل القيم في نمط ما كما هو الحال في ذراع match الأخير، للحصول على مطابقة مع الكل catchall التي لا تفعل أي شيء بدورها سوى تعريف كل القيم الممكنة المتبقية. هناك عدة طرق لتجاهل قيم كاملة أو جزء من قيم في نمط، منها: استخدام نمط _ (الذي تطرقنا له سابقًا)، أو استخدام نمط _ مع نمط أخر، أو استخدام اسم يبدأ بشرطة سفلية، أو استخدام .. لتجاهل باقي أجزاء القيمة، وسنتعرف أكثر عن مكان وسبب استخدام كل نوع من هذه الأنماط. تجاهل قيمة كاملة باستخدام _ استخدمنا الشرطة السفلية مثل نمط محرف بدل wildcard يُطابق أي قيمة ولكن لا يرتبط بها، هذا مفيد لذراع أخير في تعبير match ولكن يمكن استخدامه أيضًا في أي نمط مثل معاملات الدالة كما تبين الشيفرة 17. اسم الملف: src/main.rs fn foo(_: i32, y: i32) { println!("This code only uses the y parameter: {}", y); } fn main() { foo(3, 4); } [الشيفرة 17: استخدام _ في بصمة الدالة] تتجاهل الشيفرة السابقة القيمة 3 المُمَررة مثل معامل أول تمامًا وتطبع: This code only uses the y parameter: 4 ستحتاج في معظم الحالات لتغيير الوسيط عندما لا توجد حاجة لمعامل دالة معين وذلك كي لا يُضمَّن المعامل غير المُستخدم، ويُعد تجاهل معامل الدالة مفيدًا في حالة تطبيق سمة trait بحاجة لنوع وسيط معين ولكن لا يحتاج متن الدالة في التطبيق إلى أحد الأنماط. يمكنك تفادي الحصول على تنبيه من المصرّف عن معاملات الدالة غير المُستخدمة كما تفعل لو استعضت عنها باسم آخر. تجاهل أجزاء من القيمة باستخدام _ متداخلة يمكن استخدام _ داخل نمط آخر لتجاهل جزء من القيمة، إذ من الممكن مثلًا اختبار جزء فقط من القيمة وألا تكون هناك حاجة لباقي الأجزاء في الشيفرة المرافقة التي نريد تنفيذها. تمثّل الشيفرة 18 الشيفرة المسؤولة عن تنظيم إعدادات القيم، إذ لا تسمح متطلبات العمل للمستخدم الكتابة فوق تعديل لإعداد موجود سابقًا ولكن يمكن إزالة ضبط الإعدادات واعطائه قيمة إذا كان غير مضبوط بعد. let mut setting_value = Some(5); let new_setting_value = Some(10); match (setting_value, new_setting_value) { (Some(_), Some(_)) => { println!("Can't overwrite an existing customized value"); } _ => { setting_value = new_setting_value; } } println!("setting is {:?}", setting_value); [الشيفرة 18: استخدام شرطة سفلية داخل الأنماط التي تطابق متغايرات Some عندما لا توجد حاجة لاستخدام القيمة داخل Some] تطبع الشيفرة ما يلي: Can't overwrite an existing customized value وبعدها: setting is Some(5)‎ لا نحتاج لمطابقة أو استخدام القيمة في كِلا متغايري Some في ذراع المطابقة الأولى، لكننا بحاجة لاختبار الحالة عندما يكون متغايرا Some هما setting_value و new_setting_value، وفي تلك الحالة نطبع سبب عدم تغيير setting_value ولا تتغيّر؛ ومن أجل باقي الحالات (إذا كانت setting_value أو new_setting_value هي None) مُعبرة بالنمط _ في الذراع الثانية، سنسمح للقيمة new_setting_value بأن تصبح setting_value. يمكننا استخدام الشرطة السفلية في أماكن متعددة داخل نمط واحد لتجاهل قيمة معينة، وتبين الشيفرة 19 مثالًا عن تجاهل القيمتين الثانية والرابعة في صف مكون من خمس قيم. let numbers = (2, 4, 8, 16, 32); match numbers { (first, _, third, _, fifth) => { println!("Some numbers: {first}, {third}, {fifth}") } } [الشيفرة 19: تجاهل قيم متعددة في صف] تطبع الشيفرة Some numbers: 2, 8, 32 متجاهلةً القيمتين 4 و16. تجاهل المتغيرات غير المستخدمة بكتابة _ بداية اسمها تنبّهك رست إذا أنشأت متغيرًا ولم تستخدمه في أي مكان، إذ يمكن أن يكون عدم استخدام متغير خطأ برمجي، ولكن من المفيد إنشاء متغير لا تريد استخدامه حاليًا مثل عندما نريد كتابة نموذج أولي prototype أو عند بداية مشروع جديد، ففي هذه الحالات يمكن إخبار رست بعدم التنبيه عن المتغيرات غير المستخدمة بكتابة شرطة سفلية "_" قبل اسم المتغير. أنشأنا في الشيفرة 20 متغيرين غير مستخدمين ولكن عندما نصرّف الشيفرة هذه يجب أن نحصل على تنبيه بخصوص واحد منهما فقط. اسم الملف: src/main.rs fn main() { let _x = 5; let y = 10; } [الشيفرة 20: كتابة اسم المتغير مسبوقًا بشرطة سفلية لتجنب الحصول على تنبيه متغير غير مُستخدم] نحصل على تنبيه عن عدم استخدام المتغير y ولكن لن نحصل على تنبيه لعدم استخدام المتغير ‎_x. لاحظ أن هناك اختلاف بسيط بين استخدام _ فقط أو استخدام اسم مسبوقًا بشرطة سفلية، إذ تُسنِد الصيغة ‎_x القيمة بالمتغير ولكن لا تُسند الصيغة _ أي قيمة إطلاقًا، ولتوضيح أهمية الفرق إليك الشيفرة 21 التي تعطينا الخطأ التالي. let s = Some(String::from("Hello!")); if let Some(_s) = s { println!("found a string"); } println!("{:?}", s); [الشيفرة 21: يُسند متغير غير مستخدم يبدأ بشرطة سفلية إلى قيمة، مما قد يمنحه ملكية القيمة] سنحصل على خطأ لأن قيمة s ستنتقل إلى s_ التي تمنع استخدام s مجددًا، بينما لا يُسند استخدام الشرطة السفلية لوحدها القيمة أبدًا. تُصرّف الشيفرة 22 التالية دون أي أخطاء لأن s لا تنتقل إلى _. let s = Some(String::from("Hello!")); if let Some(_) = s { println!("found a string"); } println!("{:?}", s); [ الشيفرة 22: استخدام الشرطة السفلية لا يُسند القيمة] تعمل الشيفرة بصورةٍ صحيحة لأن s لا تُسند لأي قيمة ولا يتغير مكانها. تجاهل الأجزاء المتبقية من القيمة باستخدام .. يمكننا استخدام الصيغة .. لاستخدام أجزاء معينة من القيمة وتجاهل الباقي وذلك مع القيم التي تحتوي على أجزاء متعددة، ودون الحاجة لاستخدام الشرطة السفلية لكل قيمة مُتَجَاهلة؛ إذ يتجاهل النمط .. أي جزء لم يُطابق من القيمة صراحةً في باقي النمط. لدينا في الشيفرة 23 هيكل Point يحتوي إحداثيات في الفضاء ثلاثي الأبعاد، ونريد في تعبير match أن نعمل فقط على إحداثيات x وتتجاهل القيم الموجودة في الحقلين y و z. struct Point { x: i32, y: i32, z: i32, } let origin = Point { x: 0, y: 0, z: 0 }; match origin { Point { x, .. } => println!("x is {}", x), } [الشيفرة 23: تجاهل كل الحقول في Point عدا الحقل x باستخدام ..] نضع القيمة x في قائمة وبعدها نضيف النمط ..، وتُعد هذه الطريقة أسرع من كتابة كل من y: _‎ و z: _‎ وتحديدًا عند العمل مع هياكل تحتوي على العديد من الحقول وتريد الحصول على حقل واحد أو اثنين. يمكن زيادة الصيغة .. إلى عدد كبير من القيم حسب الحاجة، وتظهر الشيفرة 24 كيفية استخدام .. مع صف. اسم الملف: src/main.rs fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, .., last) => { println!("Some numbers: {first}, {last}"); } } } [الشيفرة 24: مطابقة القيمتين الأولى والأخيرة في الصف وتجاهل باقي القيم] تتطابق القيمتين الأولى والأخيرة مع first و last في هذه الشيفرة، وتطابق .. القيمتين وتتجاهل كل شيء في المنتصف. يجب استخدام .. بوضوح، إذ تعطي رست رسالة خطأ إذا كانت القيمة المُراد مطابقتها والقيم المُتجاهلة غير واضحة. تبين الشيفرة 25 مثالًا لاستخدام .. غير واضح ونتيجة لذلك فإن الشيفرة لا تُصرَّف. اسم الملف: src/main.rs fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (.., second, ..) => { println!("Some numbers: {}", second) }, } } [الشيفرة 25: محاولة استخدام .. بطريقة غير واضحة] عندما نصرّف الشيفرة في هذا المثال نحصل على الخطأ التالي: $ cargo run Compiling patterns v0.1.0 (file:///projects/patterns) error: `..` can only be used once per tuple pattern --> src/main.rs:5:22 | 5 | (.., second, ..) => { | -- ^^ can only be used once per tuple pattern | | | previously used here error: could not compile `patterns` due to previous error من المستحيل أن تحدد رست عدد القيم التي تتجاهلها في الصف قبل مطابقة قيمة مع second وكم قيمة ستتجاهل بعدها، قد تعني هذه الشيفرة أننا نريد تجاهل 2 وإسناد second إلى 4 وبعدها تجاهل 8 و 16 و 32 أو قد تعني أيضًا تجاهل 2 و 4 وإسناد second إلى 8 وبعدها تجاهل 16 و 32 وهكذا. لا يعني اسم المتغير second أي شيء مميز لرست لذا نحصل على خطأ تصريفي لأن استخدام .. في مكانين يجعل النمط غير واضح. تعابير شرطية إضافية مع دروع المطابقة درع المطابقة هو شرط if إضافي يُحدّد بعد النمط في ذراع match ويجب أن يطابق الذراع حتى يجري اختياره، وتفيد دروع المقابلة للتعبير عن أفكار معقّدة لا يمكننا التعبير عنها بالنمط لوحده. يمكن أن يستخدم الشرط متغيرات مُنشأة في النمط، وتبين الشيفرة 26 تعليمة match تحتوي الذراع الأولى فيها على النمط Some(x)‎ وأيضًا درع مطابقة if x % 2 == 0 (الذي يكون صحيحًا إذا كان العدد زوجي). let num = Some(4); match num { Some(x) if x % 2 == 0 => println!("The number {} is even", x), Some(x) => println!("The number {} is odd", x), None => (), } [الشيفرة 26: إضافة درع مطابقة إلى نمط] ستطبع الشيفرة السابقة The number 4 is even، وتتطابق عندما تُقارن num مع النمط الموجود في الذراع الأولى، لأن Some(4)‎ تطابق Some(x)‎. بعد ذلك، يتحقق درع المطابقة إذا كان الباقي من عملية قسمة x على 2 يساوي 0 ولأن هذه الحالة محقّقة يقع الاختيار على الذراع الأولى. سيكون درع المطابقة في الذراع الأولى خاطئًا إذا كان num هو Some(5)‎، وذلك لأن باقي قسمة 5 على 2 هو 1 وهو لا يساوي 0، وتنتقل بعدها رست إلى الذراع الثانية التي ليس فيها درع مطابقة وبالتالي تطابق أي متغير Some. لا توجد طريقة للتعبير عن شرط if x % 2 == 0 داخل النمط، لذا يسمح لنا درع المطابقة بالتعبير عن هذا المنطق. سلبية هذا التعبير الإضافي هي أن المصرّف لا يتحقق من الشمولية عند تواجد تعابير درع مطابقة. ذكرنا في الشيفرة 11 أنه يمكننا استخدام دروع المطابقة لحل مشكلة إخفاء النمط pattern-shadowing. تذكر أننا أنشأنا متغيرًا جديدًا داخل النمط في التعبير match عوضًا عن استخدام المتغير خارج match، ويعني إنشاء هذا المتغير الجديد أنه لا يمكن اختبار القيم مع المتغير الخارجي. تبين الشيفرة 27 كيفية استخدام درع المطابقة لحل هذه المشكلة. اسم الملف: src/main.rs fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!("Got 50"), Some(n) if n == y => println!("Matched, n = {n}"), _ => println!("Default case, x = {:?}", x), } println!("at the end: x = {:?}, y = {y}", x); } [الشيفرة 27: استخدام درع المطابقة لاختبار المساواة مع متغير خارجي] ستطبع الشيفرة الآن: Default case, x = Some(5)‎ لا يقدم النمط في الذراع الثاني متغير y جديد يخفي المتغير y الخارجي، وهذا يعني أنه يمكن استخدام y الخارجية في درع المطابقة، إذ نحدّد Some(n)‎ عوضًا عن تحديد النمط Some(y)‎ الذي كان سيخفي بدوره المتغير y الخارجي، وسينشئ ذلك متغيرًا جديدًا يدعى n لا يُخفي أي شيء لعدم وجود أي متغير بالاسم n خارج match. ليس درع المطابقة if n == y نمطًا وبالتالي لا يقدم أي متغيرات جديدة، إذ أن y هذه هي y الخارجية ذاتها وليست y مخفية جديدة، ويمكن البحث عن قيمة لديها نفس قيمة y الخارجية بمقارنة n مع y. يمكن استخدام المعامل "أو" | في درع المطابقة لتحديد الأنماط المتعددة، وسيُطبق شرط درع المطابقة على كل الأنماط. تبين الشيفرة 28 الأسبقية عند جمع نمط يستخدم | مع درع مطابقة، والقسم الأهم من هذا المثال هو درع المطابقة if y الذي يطبق على 4 و 5 و 6 على الرغم من أن if y تبدو أنها مطبقةٌ فقط على 6. let x = 4; let y = false; match x { 4 | 5 | 6 if y => println!("yes"), _ => println!("no"), } [ الشيفرة 28: جمع عدة أنماط مع درع مطابقة] ينصّ شرط المطابقة على أن الذراع تتطابق فقط إذا كانت قيمة x تساوي 4 أو 5 أو 6 وإذا كانت قيمة y هي true، وعندما تُنفذ الشيفرة يُطَابَق نمط الذراع الأول لأن x هي 4 ولكن درع المطابقة if y خاطئ، لذا لا يقع الاختيار على الذراع الأولى وتنتقل الشيفرة إلى الذراع الثانية التي تُطابَق ويطبع البرنامج no، والسبب وراء ذلك هو تطبيق شرط if لكل النمط 6 | 5 | 4، وليس فقط للقيمة الأخيرة 6، بمعنى أخر تتصرف أسبقية درع المطابقة مع النمط على النحو التالي: (4 | 5 | 6) if y => ... عوضًا عن: 4 | 5 | (6 if y) => ... سلوك الأسبقية واضح بعد تنفيذ الشيفرة، إذ ستُطابق الذراع وسيطبع البرنامج yes إذا كان درع المطابقة مطبقًا فقط على القيمة الأخيرة في قائمة القيم المحددة بالمعامل |. ارتباطات @ يسمح لنا معامل at @ بإنشاء متغيرات تحتوي قيمة واختبارها من أجل مطابقة نمط بنفس الوقت. نريد في الشيفرة 29 اختبار حقل id في Message::Hello إذا كان ضمن المجال 3‎..=7، ونريد أيضًا ربط القيمة إلى المتغير id_variable لكي نستخدمها في الشيفرة المرتبطة مع الذراع. يمكن تسمية هذا المتغير باسم الحقل id، لكننا استخدمنا اسمًا مختلفًا. enum Message { Hello { id: i32 }, } let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id_variable @ 3..=7, } => println!("Found an id in range: {}", id_variable), Message::Hello { id: 10..=12 } => { println!("Found an id in another range") } Message::Hello { id } => println!("Found some other id: {}", id), } [الشيفرة 29: استخدام @ لربط القيمة في النمط مع اختبارها أيضًا] سيطبع المثال السابق ما يلي: Found an id in range: 5 نلتقط أي قيمة تطابق المجال ‎3..=7 بتحديد id_variable @‎ قبله، إضافةً إلى اختبار نمط تلك القيمة المطابقة. لا تحتوي الشيفرة في الذراع متغيرًا يحتوي على قيمة حقيقية في حقل id في الذراع الثانية، إذ لدينا فقط مجالًا محددًا في النمط. يمكن أن تكون قيمة الحقل id مساوية إلى 10 أو 11 أو 12 لكن لا تعرف الشيفرة التي في ذلك النمط أي قيمة هي منهم، ولا يستطيع نمط الشيفرة استخدام القيمة من حقل id لأننا لم نحفظ قيمة id في المتغير. ليس لدينا قيمةً متوفرةً لاستخدامها في شيفرة الذراع الأخيرة في المتغير المسمى id، إذ حددنا متغيرًا دون مجال، ويعود سبب ذلك إلى استخدام صيغة حقل الهيكل المختزلة، وعدم تطبيق أي اختبار على القيمة في حقل id` في هذا الذراع كما فعلنا في الذراعين الأوليين، فأي قيمة ستطابق هذا النمط. يسمح لنا استخدام @ باختبار القيمة وحفظها في متغير داخل نمط واحد. ترجمة -وبتصرف- لقسم من الفصل Patterns and Matching من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: الأنماط Patterns واستخداماتها وقابليتها للدحض Refutability في لغة رست توثيق أنماط التصميم أنماط التصميم البرمجي Design patterns أنماط التصميم وتقنيات إعادة التصميم في Cpp
  23. بعد أن تعلمنا كيفية قياس سرعة البرامج في المقال السابق قياس أداء وسرعة تنفيذ شيفرة بايثون، سنتعلم كيفية قياس الزيادات النظرية theoretical increases في وقت التنفيذ runtime مع نمو حجم البيانات الخاصة بالبرنامج، ويُطلق على ذلك في علوم الحاسوب ترميز O الكبير big O notation. ربما يشعر مطورو البرامج الذين ليس لديهم خلفية في علوم الحاسوب التقليدية بوجود نقص في معارفهم، على الرغم من كون المعرفة في علوم الحاسوب مفيدة، لكنه ليس مرتبط مباشرةً مع تطوير البرمجيات. تُعدّ Big O نوعًا من خوارزميات التحليل التي تصف تعقيد الشيفرة مع زيادة عدد العناصر التي تعمل عليها. تصنف الشيفرة في مرتبة تصف عمومًا الوقت الذي تستغرقه الشيفرة لتُنفّذ، وكلما زادت تزيد كمية العمل الواجب إنجازه. يصف مطور لغة بايثون Python نيد باتشلدر Ned Batchelder خوارزمية big O بكونها تحليلًا لكيفية "تباطؤ الشيفرة كلما زادت البيانات" وهو عنوان حديثه في معرض PyCon 2018. لننظر إلى المثال التالي: لنفترض أنه لديك كميةٌ معينةٌ من العمل الذي يستغرق ساعةً ليكتمل، فإذا تضاعفت كمية العمل، كم سيستغرق من الوقت؟ ربما تعتقد أنه سيستغرق ضعف المدة ولكن الجواب الصحيح هو: يعتمد الأمر على نوع العمل المُنجز. إذا استغرقت ساعةً لقراءة كتاب قصير، فستستغرق أكثر أو أقل من ساعتين لقراءة كتابين قصيرين، وإذا كان بإمكانك ترتيب 500 كتاب أبجديًا، سيستغرق ترتيب 1000 كتاب أكثر من ساعتين لأنك ستبحث عن المكان المناسب لكل كتاب في مجموعة أكبر من الكتب. من ناحية أخرى، إذا أردت التأكد أن رف الكتب فارغ أو لا، لا يهم إذا كان هناك 0 أو 10 أو 1000 كتاب على الرف، فنظرةٌ واحدةٌ كافية لتعرف الجواب، إذ سيبقى وقت التنفيذ على حاله بغض النظر عن عدد الكتب الموجودة. يمكن أن يكون بعض الناس أسرع أو أبطأ في قراءة أو ترتيب الكتب أبجديًا ولكن تبقى هذه التوجهات العامة نفسها. تصف خوارزمية big O هذه التوجهات، إذ يمكن أن تُنفذ الخوارزمية على حاسوب سريع أو بطيء ولكننا نستطيع استخدام big O لوصف كيفية عمل الخوارزمية عمومًا، بغض النظر عن العتاد الذي ينفذ هذه الخوارزمية. لا تستخدم big O وحدات معينة مثل الثواني أو دورات المعالج لوصف وقت تنفيذ الخوارزمية لأنها تختلف بين أجهزة الحاسوب ولغات البرمجة. مراتب Big O يُعرّف ترميز Big O عادةً المراتب التالية، التي تتراوح من المنخفضة -التي تصف الشيفرة التي لا تتباطأ كثيرًا كلما زادت البيانات- إلى المراتب العليا -التي تصف الشيفرة الأكثر تباطؤًا: O(1)‎ وقت ثابت (أدنى مرتبة) O(log n)‎ وقت لوغاريتمي O(n)‎ وقت خطي O(n log n)‎ وقت N-Log-N O(n2)‎ وقت متعدد الحدود O(2n)‎ وقت أسي O(n!)‎ وقت عاملي (أعلى مرتبة) لاحظ أن big O تستخدم حرف O كبير متبوعًا بقوسين يحتويان وصف المرتبة، إذ يمثّل حرف O الكبير المرتبة وتمثل n حجم دخل البيانات التي تعمل عليها الشيفرة. نلفظها "big oh of n" أو "big oh n". لا تحتاج لفهم المعنى الدقيق لكلمات مثل لوغاريتمي أو حدودي لاستخدام صيغة big O، إذ سنصِف كل نوع بالتفصيل في الفقرة التالية ولكن هذا تبسيط لهم: خوارزميات O(1)‎ و O(log n)‎ سريعة خوارزميات O(n)‎ و O(n log n)‎ ليست سيئة خوارزميات O(n2)‎ و O(2n)‎ بطيئة يمكن طبعًا مناقشة العكس في بعض الحالات ولكن هذه التوصيفات هي قواعد جيدة عمومًا، فهناك مراتب أكثر من big O المذكورة هنا ولكن هذه هي الأعم. دعنا نتحدث عن أنواع المهام التي يصفها كل نوع من هذه المهام. اصطلاح رف الكتب Bookshelf لمراتب Big O سنستمر في المثال التالي لمراتب big O باستخدام مصطلح رف الكتب، إذ تمثّل n عدد الكتب في الرف ويصف ترميز Big O كيف أن المهام المختلفة تستغرق وقتًا أطول كلما زاد عدد الكتب. تعقيد O(1)‎: وقت ثابت معرفة "هل رف الكتب فارغ؟" هي عملية ذات وقت ثابت، إذ لا يهم عدد الكتب في الرف، فنظرةٌ واحدةٌ ستخبرنا ما إذا كان رف الكتب فارغ أم لا. يمكن أن يختلف عدد الكتب ولكن وقت التنفيذ يبقى ثابتًا، لأنه طالما رأينا كتابًا واحدًا على الرف يمكننا إيقاف البحث. القيمة n غير مهمة لسرعة تنفيذ المهمة لذا لا يوجد n في O(1)‎. يمكنك أيضًا رؤية الوقت الثابت مكتوبًا (O(c أحيانًا ‎. تعقيد O(log n)‎: لوغاريتمي اللوغاريتم هو عكس الأس، الأس 24 أو 2×2×2×2 يساوي 16 ولكن اللوغاريتم log2(16)‎ (تلفظ لوغاريتم أساس 2 للعدد 16) يساوي 4. نفترض في البرمجة قيمة الأساس 2 ولذلك نكتب O(log n)‎ بدلًا من O(log2 n)‎. البحث عن كتاب في رف كتب مرتب أبجديًا هي عملية لوغاريتمية الوقت؛ فمن أجل إيجاد كتاب واحد، يمكن التحقق من الكتاب في منتصف الرف، وإذا كان هو الكتاب المطلوب تكون قد انتهيت، وإذا لم يكن كذلك، يمكن تحديد إذا كان الكتاب قبل أو بعد الكتاب الذي في المنتصف. يقلل ذلك مجال البحث إلى النصف، ويمكنك تكرار هذه العملية مجددًا من خلال فحص الكتاب الذي في منتصف النصف الذي تتوقع فيه إيجاد الكتاب. نسمي ذلك خوارزمية بحث ثنائية binary search وهناك مثال على ذلك في "أمثلة عن ترميز Big O" لاحقًا. عدد المرات التي تستطيع قسم مجموعة n كتاب للنصف هي log2 n، في رف فيه 16 كتاب ستحتاج إلى 4 خطوات على الأكثر لإيجاد الكتاب الصحيح، لأن كل خطوة تقلل عدد الكتب التي يجب البحث فيها إلى النصف. يحتاج رف الكتب الذي فيه ضعف عدد الكتب فقط إلى خطوة إضافية واحدة للبحث عنه. إذا كان هناك 4.2 مليار كتاب في رف كتب مرتبة أبجديًا ستحتاج فقط إلى 32 خطوة لإيجاد كتاب معين. تتضمن خوارزميات Log n عادةً خطوة فرّق تسد divide and conquer وتنطوي على اختيار نصف دخل n للعمل عليه وبعدها نصف آخر من ذلك النصف وهكذا دواليك. تتدرج عمليات log n جيدًا، إذ يمكن أن يزداد العمل n إلى الضعف ولكن سيزداد وقت التنفيذ خطوةً واحدةً فقط. تعقيد O(n)‎: وقت خطي تستغرق عملية قراءة كل الكتب على الرف وقتًا خطيًا؛ فإذا كانت الكتب بنفس الطول تقريبًا وضاعفت عدد الكتب على الرف، فسيستغرق ضعف الوقت تقريبًا لقراءة كل الكتب، ويزداد وقت التنفيذ بالتناسب مع عدد الكتب n. تعقيد O(n log n)‎: وقت N-Log-N ترتيب الكتب أبجديًا هي عملية تستغرق وقت n-log-n. هذه المرتبة هي ناتج ضرب وقتي التنفيذ O(n)‎ و O(log n)‎ ببعضهما. يمكن القول أن مهمة O(n log n)‎ هي مهمة O(log n)‎ مع تنفيذها n مرة. فيما يلي تفسير بسيط حول ذلك. ابدأ بمجموعة كتب يجب ترتيبها أبجديًا ورف كتب فارغ، واتبع الخطوات في خوارزمية البحث الثنائي كما موضح في الفقرة السابقة "تعقيد O(log n)‎: زمن لوغاريتمي" لمعرفة مكان كل كتاب على الرف. هذه عملية O(log n)‎، ومن أجل ترتيب n كتاب أبجديًا وكان كل كتاب بحاجة إلى log n خطوة لترتيبه أبجديًا، ستحتاج n×log n أو n log n خطوة لترتيب كل مجموعة الكتب أبجديًا. إذا تضاعف عدد الكتب سيستغرق أكثر من ضعف الوقت لترتيبهم أبجديًا، لذا تتدرج خوارزميات n log n جيدًا. خوارزميات الترتيب ذات الكفاءة، هي: O(n log n)‎، مثل ترتيب الدمج merge sort والترتيب السريع quicksort وترتيب الكومة heapsort وترتيب تيم Timsort (من اختراع تيم بيترز Tim Peters وهي الخوارزمية التي يستخدمها تابع sort()‎ الخاص ببايثون). تعقيد O(n2)‎: وقت متعدد الحدود التحقق من الكتب المكررة في رف كتب غير مرتب هي عملية تستغرق وقت حدودي polynomial time operation؛ فإذا كان هناك 100 كتاب يمكنك أن تبدأ بالكتاب الأول وتقارنه مع التسعة وتسعون الكتاب الباقين لمعرفة التشابه، ثم نأخذ ثاني كتاب ونتحقق بنفس الطريقة مثل باقي بقية الكتب التسعة وتسعون. خطوات التحقق من التكرار لكتاب واحد هي 99 (سنقرّب هذا الرقم إلى 100 الذي هو n في هذا المثال). يجب علينا فعل ذلك 100 مرة، مرةً لكل كتاب، لذا عدد خطوات التحقق لكل كتاب على الرف هو تقريبًا n×n أو n2. (يبقى هذا التقريب n2 صالحًا حتى لو كنا أذكياء ولم نكرر المقارنات). يزداد وقت التنفيذ بازدياد عدد الكتب تربيعيًا. سيأخذ التحقق من التكرار لمئة كتاب 100×100 أو 10,000 خطوة، ولكن التحقق من ضعف هذه القيمة أي 200 كتاب سيكون 200×200 أو 40,000 خطوة، أي أربع مرات عمل أكثر. وجد بعض الخبراء في كتابة الشيفرات الواقعية للعالم الحقيقي أن معظم استخدامات تحليل big O هي لتفادي كتابة خوارزمية O(n2)‎ عن طريق الخطأ، عند وجود خوارزمية O(n log n)‎ أو O(n)‎. مرتبة O(n2)‎ هي عندما تبدأ الخوارزميات بالتباطؤ كثيرًا، لذا معرفة أن الشيفرة الخاصة بك في مرتبة O(n2)‎ أو أعلى يجب أن يجعلك تتوقف. ربما توجد خوارزمية مختلفة يمكنها حل المشكلة بصورةٍ أسرع، ويمكن في هذه الحالة أن يكون الإطلاع على قسم هيكلة البيانات والخوارزميات Data Structure and Algorithms -أو اختصارًا DSA- إما على أكاديمية حسوب مفيدًا. نسمي أيضًا O(n2)‎ وقتًا تربيعيًا، ويمكن أن يكون لخوارزميات O(n3) وقتًا تكعيبيًا وهو أبطأ من O(n2)‎ أو وقتًا رباعيًا O(n4)‎ الذي هو أبطأ من O(n3)‎ أو غيره من الأوقات الزمنية متعددة الحدود. تعقيد O(2n): وقت أسي أخذ صور لرف الكتب مع كل مجموعة ممكنة من الكتب هو عملية تستغرق وقتًا أُسّيًا. انظر لهذا الأمر بهذه الطريقة، كل كتاب على الرف يمكن أن يكون في الصورة أو لا يكون. يبين الشكل 1 كل مجموعة ممكنة، إذ تكون n هي 1 أو 2 أو 3. إذا كانت n هي 1 هناك طريقين للتجميع، إذا كانت n هي 2 هناك أربعة صور ممكنة، الكتابان على الرف، أو الكتابان ليسا على الرف، أو الكتاب الأول موجود والثاني ليس موجودًا، أو الكتاب الأول ليس موجودًا والثاني موجود. إذا أضفنا كتابًا ثالثًا، نكون قد ضاعفنا مرةً ثانية العمل المُراد فعله، لذا يجب عليك النظر إلى كل مجموعة فرعية لكتابين التي تضم الكتاب الثالث (أربعة صور) وكل مجموعة فرعية لكتابين دون الكتاب الثالث (أربعة صور أُخرى أي 23 أو 8 صور). يضاعف كل كتاب إضافي كمية العمل، فمن أجل n كتاب سيكون عدد الصور التي يجب أخذها (أي العمل الواجب فعله) هو 2n. [الشكل 1: مجموعة الكتب الممكنة على رف كتب من أجل كتاب واحد أو اثنين أو ثلاث كتب] يزداد وقت التنفيذ للمهام الأُسية بسرعة كبيرة. تحتاج ستة كتب إلى 26 أو 32 صورة، ولكن 32 كتاب يحتاج 232 أو أكثر من 4.2 مليار صورة. مرتبة O(22) أو O(23)‎ أو O(24) وما بعدها هي مراتب مختلفة ولكنها كلها تحتوي تعقيدات وقت أُسّي. تعقيد O(n!)‎: وقت عاملي أخذ صورةٍ لكل ترتيب معين هي عملية تستغرق وقتًا عاملي. نطلق على كل ترتيب ممكن اسم التبديل permutation من أجل n كتاب. النتيجة هي ترتيب n!‎ أو n عاملي، فمثلًا 3‎!‎‎ هي 3×2×1 أو 6. يبين الشكل 2 كل التبديلات الممكنة لثلاثة كتب. [الشكل 2: كل تبديلات !3 (أي 6) لثلاثة كتب على رف كتب] لحساب ذلك بنفسك، فكر بكل التبديلات الممكنة بالنسبة إلى n كتاب. لديك n خيار ممكن للكتاب الأول وبعدها n-1 خيار ممكن للكتاب الثاني (أي كل كتاب ما عدا المكان الذي اخترته للكتاب الأول) وبعدها n-2 خيار ممكن للكتاب الثالث وهكذا دواليك. مع 6 كتب تكون نتيجة !6 هي 6×5×4×3×2×1 أو 720 صورة. إضافة كتاب واحد آخر يجعل عدد الصور المطلوبة !7 أو 5.040. حتى من أجل قيم n صغيرة، تصبح خوارزميات الوقت العاملي مستحيلة الإنجاز في وقت منطقي، فإذا كان لديك 20 كتاب ويمكنك ترتيبهم وأخذ صورة كل ثانية، فستحتاج إلى وقت أكثر من عمر الكون للانتهاء من كل تبديل. واحدة من مشاكل O(n!)‎ المعروفة هي معضلة مندوب المبيعات المسافر، إذ يجب على مندوب المبيعات أن يزور n مدينة ويريد حساب المسافة المقطوعة لكل مراتب n!‎ الممكنة والتي يمكنه زيارتها، إذ يستطيع من تلك الحالات إيجاد أقصر طريق، ومن أجل منطقة بعدد كبير من المدن تصبح هذه المهمة مستحيلة الإنجاز في وقت منطقي. لحسن الحظ هناك خوارزميات مُحسنة لإيجاد طريق قصير (ولكن ليس من المضمون أن يكون الأقصر) بطريقة أسرع من O(n!)‎. يحسب ترميز Big O الحالات الأسوأ يحسب Big O أسوأ حالة ممكنة لأي مهمة، إذ يحتاج إيجاد كتاب معين في رف كتب غير مُنظم مثلًا أن تبدأ من أحد الأطراف وتتحقق من الكتب حتى تجد الكتاب المطلوب. يمكن أن تكون محظوظًا ويكون الكتاب المطلوب هو أول كتاب تتحقق منه، ولكن ربما تكون سيء الحظ ويكون الكتاب الذي تريده هو آخر كتاب تتحقق منه، أو قد لا يكون موجودًا على الرف إطلاقًا. لذلك، في أفضل الحالات لا يهم إذا كان هناك مليارات الكتب التي يجب البحث فيها لأنك ستجد الكتاب الذي تريده مباشرةً، لكن هذا التفاؤل ليس مفيدًا في خوارزميات التحليل. تصف Big O ماذا يحصل في الحالات الأسوأ حظًا، أي إذا كان لديك n كتاب يجب عليك البحث في كل الكتب، ففي هذا المثال يزداد وقت التنفيذ بنفس معدل ازدياد عدد الكتب. يستخدم بعض المبرمجين ترميز big Omega لوصف الحالة الأفضل للخوارزمية، فمثلًا تعمل خوارزمية ‎Ω(n)‎ بكفاءة خطية في أفضل حالاتها وفي الحالة الأسوأ ربما تستغرق وقتًا أطول. تواجه بعض الخوارزميات حالات محظوظة جدًا، يحيث لا تعمل أي شيء، مثل إيجاد مسار الطريق لمكان أنت أصلًا فيه. يصف ترميز Big Theta الخوارزميات التي لها الترتيب نفسه في أسوأ وأفضل الحالات، فمثلًا تصف ‎Θ(n)‎ خوارزميةً لديها كفاءة خطية في أحسن وأسوأ الحالات، أي أنها خوارزمية O(n)‎ وكذلك ‎Ω(n)‎. لا يُستخدم هذين الترميزين كثيرًا مثل استخدام big O ولكن تجدر معرفتهما. يُعد سماع الناس يتحدثون عن "big O الحالة الوسطية" عندما يعنون big Theta أو "big O الحالة الفُضلى" عندما يعنون big Omega أمرًا شائعًا رغم أنه متناقض؛ إذ تصف big O الحالة الأسوأ لوقت تنفيذ الخوارزمية تحديدًا، ولكن حتى لو كانت كلماتهم خاطئة لا تزال تفهم المعنى بغض النظر. العمليات الرياضية الكافية للتعامل مع Big O إذا كان علم الجبر لديك ضعيفًا فمعرفة العمليات الرياضية التالية أكثر من كافي عند التعامل مع big O: الضرب: تكرار الإضافة أي 2×4=8 هو مثل 2+2+2+2=8، وفي حال المتغيرات يكون n+n+n هو 3‎×n. ترميز الضرب: يهمل ترميز الجبر عادةً إشارة ×، لذا 2‎×n تُكتب 2n ومع الأرقام 3×2 تُكتب (3)2 أو ببساطة 6. خاصية الضرب بالعدد 1: ضرب أي عدد بالرقم 1 يُنتج الرقم نفسه أي 5=x1‏5 و42 =x1‏42 أو عمومًا n×1=n توزيع الضرب على الجمع: (3×2) + (3×2) = (4+3)2x كل طرف من المعادلة يساوي 14 أي عمومًا a(b+c) = ab+ac الأس: تكرار الضرب 16= 24 (تُلفظ "2 مرفوعة للقوة الرابعة تساوي 16") مثل 2×2×2×2= 16 هنا تكون 2 هي الأساس و 4 هي الأس. باستخدام المتغيرات n×n×n×n هي n4. يُستخدم في بايثون المعامل ** على سبيل المثال 2**4 تساوي 16. الأس الأول يساوي الأساس: 2= 21 و 9999=99991 وبصورةٍ عامة n1=n الأس 0 يساوي 1: 1= 20 و 1=99990 وبصورةٍ عامة n0=1 المعاملات: عوامل الضرب في 3n2+4n+5 المعاملات هي 3 و4 و5. يمكنك معرفة أن 5 هي معامل لأن 5 يمكن أن يُعاد كتابتها بالشكل (1)5 وأيضًا يمكن إعادة كتابتها 5n0. اللوغاريتمات: عكس الأس. لأن 16=24 نعرف أن log2(16)=4. نفول لوغاريتم الأساس 2 للعدد 16 هو 4. نستخدم في بايثون دالة math.log()‎ إذ math.log(16, 2)‎ تساوي إلى 4.0. يتطلب حساب big O تبسيط العمليات عن طريق جمع الحدود المتشابهة؛ والحد هو مجموعة من الأرقام والمتغيرات مضروبة مع بعضها، ففي 3n2+4n+5 تكون الحدود هي 3n2 و4n و5، إذ أن الحدود المتشابهة لديها نفس المتغير مرفوعًا لنفس القوة. في التعبير 3n2+4n+6n+5 الحدان 4n و6n هما متشابهان بإمكاننا التبسيط وإعادة الكتابة كالتالي 3n2+10n+5. خذ بالحسبان أنه يمكن كتابة 3n2+5n+4 على النحو التالي 3n2+5n+4(1)‎، إذ تطابق الحدود في هذا التعبير مرتبات big O التالية O(n2)‎ و O(n)‎ و O(1)‎. سيفيد هذا لاحقًا عندما نُسقط المعاملات في حسابات big O. ستفيد هذه التذكرة عندما تحاول معرفة big O لقطعة من الشيفرة، ولكن لن تحتاجها بعد أن تنتهي من "تحليل Big O بنظرة سريعة" في المقال التالي. مفهوم big O بسيط ويمكن أن يفيد حتى لو لم تتبع القواعد الرياضية بصرامة. الخلاصة يُعد ترميز Big O من أكثر المفاهيم انتشارًا في علم الحواسيب للمبرمجين، وهو بحاجة لبعض المفاهيم في الرياضيات لفهمه، ولكن يمكن للمفهوم الأساسي، ألا وهو معرفة أن الشيفرة ستبطأ كلما زادت البيانات، أن يصف الخوارزميات دون الحاجة إلى أرقام كبيرة لحسابها. هناك سبع مراتب لترميز big O، وهي: O(1)‎ أو الوقت الثابت، الذي يصف الشيفرة التي لا تتغير مع زيادة البيانات؛ و O(log n)‎ أو الوقت اللوغاريتمي الذي يصف الشيفرة التي تزداد بخطوة كلما تضاعف عدد البيانات بمقدار n؛ و O(n)‎ أو الوقت الخطي، الذي يصف الشيفرة التي تتباطأ يتناسب مع زيادة حجم البيانات بمقدار n؛ و O(n log n)‎ أو وقت n-log-n، الذي يصف الشيفرة التي هي أبطأ من O(n)‎ والعديد من خوارزميات الترتيب لديها هذه المرتبة. المراتب الأعلى هي أبطأ لأن وقت تنفيذها يزداد بصورةٍ أسرع من زيادة حجم دخل البيانات. يصف الوقت الحدودي O(n2)‎ الشيفرة التي يزداد وقت تنفيذها بتربيع الدخل n. ليست المراتب O(2n)‎ أو الوقت الأسّي، و O(n!)‎ أو الوقت العاملي شائعة جدًا، ولكنها تأتي مع المجموعات والتبديلات على الترتيب. ترجمة -وبتصرف- لقسم من الفصل Measuring Performance And Big O Algorithm Analysis من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: قياس أداء وسرعة تنفيذ شيفرة بايثون -تحليل الخوارزميات تعقيد الخوارزميات Algorithms Complexity دليل شامل عن تحليل تعقيد الخوارزمية
  24. تظهر الأنماط في العديد من الأماكن في رست، وقد استخدمتها سابقًا دون ملاحظتها غالبًا. سنتحدّث في هذه المقالة عن جميع الحالات التي يكون فيها استخدام الأنماط صالحًا، إضافةً إلى الحالات التي تكون فيها قابلة للدحض Refutable. أذرع تعبير match كما تحدثنا سابقًا في الفصل التعدادات enums في لغة رست، يمكننا استخدام الأنماط في أذرع arms تعبير match، ويُعرَّف التعبير match بالكلمة المفتاحية match، تليها قيمة للتطابق معها وواحد أو أكثر من أذرع المطابقة التي تتألف من نمط وتعبير يُنفذ إذا طابقت القيمة نمط الذراع على النحو التالي: match VALUE { PATTERN => EXPRESSION, PATTERN => EXPRESSION, PATTERN => EXPRESSION, } على سبيل المثال إليك تعبير match من الشيفرة 5 (من الفصل المذكور آنفًا) الذي يطابق القيمة Option<i32>‎ في المتغير x: match x { None => None, Some(i) => Some(i + 1), } الأنماط في تعبير match السابق، هما: None و Some(i)‎ على يسار كل سهم. أحد مُتطلبات تعبير match هو ضرورة كونه شاملًا لجميع الحالات، أي ينبغي أخذ جميع القيم المُحتملة بالحسبان في تعبير match. أحد الطرق لضمان تغطية شاملة الاحتمالات هي وجود نمط مطابقة مع الكل Catchall pattern للذراع الأخير، فلا يمكن مثلًا فشل تطابق اسم متغير لأي قيمة إطلاقًا وبذلك يغطي كل حالة متبقية. يطابق النمط المحدد _ أي قيمة، لكن لا يرتبط بمتغير، لذلك يُستخدم غالبًا في ذراع المطابقة الأخير. يمكن أن يكون النمط _ مفيدًا عندما نريد تجاهل أي قيمة غير محددة مثلًا، وسنتحدث لاحقًا بتفصيل أكثر عن النمط _ في قسم "تجاهل القيم في النمط". تعابير if let الشرطية تحدثنا في الفصل التعدادات enums في لغة رست عن كيفية استخدام تعابير if let بصورةٍ أساسية مثل طريقة مختصرة لكتابة مكافئ التعبير match، بحيث يطابق حالةً واحدةً فقط، ويمكن أن يكون التعبير if let مترافقًا مع else اختياريًا، بحيث يحتوي على شيفرة برمجية تُنفّذ في حال لم يتطابق النمط في if let. تبيّن الشيفرة 1 أنه من الممكن مزج ومطابقة تعابير if let و else if و else if let، لإعطاء مرونة أكثر من استخدام التعبير match الذي يقارن قيمةً واحدة مع الأنماط. إضافةً إلى ذلك، لا تتطلب رست أن تكون الأذرع في سلسلة if let أو else if أو else if let متعلقة ببعضها بعضًا. تحدد الشيفرة 1 لون الخلفية اعتمادّا على تحقّق عدد من الشروط، إذ أنشأنا في مثالنا هذا متغيرات مع قيم مضمّنة في الشيفرة بحيث يمكن لبرنامج حقيقي استقبالها مثل مدخلات من المستخدم. اسم الملف: src/main.rs fn main() { let favorite_color: Option<&str> = None; let is_tuesday = false; let age: Result<u8, _> = "34".parse(); if let Some(color) = favorite_color { println!("Using your favorite color, {color}, as the background"); } else if is_tuesday { println!("Tuesday is green day!"); } else if let Ok(age) = age { if age > 30 { println!("Using purple as the background color"); } else { println!("Using orange as the background color"); } } else { println!("Using blue as the background color"); } } [الشيفرة 1: استخدام if let و else if و else if let و else بنفس الوقت] يُستخدم اللون المفضّل للمستخدم لونًا للخلفية إذا حدده المستخدم، وفي حال لم يُحدد لونه المفضل وكان اليوم خميس فسيكون لون الخلفية أخضر؛ وإذا حدّد المستخدم عمره في سلسلة نصية string، يمكننا تحليلها إلى رقم بنجاح، ويكون اللون إما برتقاليًا أو بنفسجيًا اعتمادًا على قيمة هذا الرقم؛ وأخيرًا إذا لم تنطبق أي من هذه الشروط سيكون لون الخلفية أزرق. يسمح لنا هذا الهيكل الشرطي بدعم المتطلبات المعقدة، إذ نطبع في هذا المثال باستخدام القيم المضمّنة في الشيفرة ما يلي: Using purple as the background color يمكنك ملاحظة أن التعبير if let تسبب بحصولنا على متغير مخفي shadowed variable بالطريقة ذاتها التي تسببت بها أذرع التعبير match؛ إذ يُنشئ السطر if let Ok(age) = age متغيرًا مخفيًا جديد يدعى age يحتوي على القيمة داخل المتغاير Ok، وهذا يعني أنه يجب إضافة الشرط if age > 30 داخل الكتلة؛ إذ لا يمكننا جمع الشَرطين في if let ok(age) = age && age > 30، والقيمة الخفية age التي نريد مقارنتها مع 30 غير صالحة حتى يبدأ النطاق scope الجديد بالقوس المعقوص curly bracket. الجانب السلبي في استخدامنا لتعبير if let هو أن المصرّف لا يتحقق من شمولية الحالات مثلما يفعل تعبير match. إذا تخلّينا عن كتلة else الأخيرة وبالتالي فوّتنا معالجة بعض الحالات، لن ينبهنا المصرّف على احتمالية وجود خطأ منطقي. حلقات while let الشرطية تسمح الحلقة الشرطية while let بتنفيذ حلقة while طالما لا يزال النمط مُطابقًا على نحوٍ مشابه لبُنية if let. كتبنا في الشيفرة 2 حلقة while let تستخدم شعاعًا مثل مكدّس stack وتطبع القيم في الشعاع بالترتيب العكسي لإدخالها. fn main() { let mut stack = Vec::new(); stack.push(1); stack.push(2); stack.push(3); while let Some(top) = stack.pop() { println!("{}", top); } } [الشيفرة 2: استخدام حلقة while let لطباعة القيم طالما يُعيد استدعاء stack.pop()‎ المتغاير Some] يطبع المثال السابق القيمة 3 ثم 2 ثم 1، إذ يأخذ التابع pop آخر عنصر في الشعاع vector ويُعيد Some(value)‎، ويعيد القيمة None إذا كان الشعاع فارغًا. يستمر تنفيذ الحلقة while والشيفرة البرمجية داخل كتلتها طالما يُعيد استدعاء pop القيمة Some، وعندما يعيد استدعاء pop القيمة None تتوقف الحلقة، ويمكننا استخدام while let لإزالة pop off كل عنصر خارج المكدّس. حلقات for التكرارية القيمة التي تتبع الكلمة المفتاحية for في حلقة for هي النمط، فعلى سبيل المثال النمط في for x in y هو x. توضح الشيفرة 3 كيفية استخدام النمط في حلقة for لتفكيك destructure أو تجزئة الصف tuple إلى جزء من حلقة for. fn main() { let v = vec!['a', 'b', 'c']; for (index, value) in v.iter().enumerate() { println!("{} is at index {}", value, index); } } [الشيفرة 3: استخدام نمط في حلقة for لتفكيك صف] تطبع الشيفرة 3 ما يلي: $ cargo run Compiling patterns v0.1.0 (file:///projects/patterns) Finished dev [unoptimized + debuginfo] target(s) in 0.52s Running `target/debug/patterns` a is at index 0 b is at index 1 c is at index 2 نُعدل مكرّرًا iterator باستخدام التابع enumerate بحيث يعطينا قيمةً ودليلًا index لهذه القيمة ضمن صف tuple، بحيث تكون القيمة الأولى هي الصف ('‎0, 'a). عندما تطابق هذه القيمة النمط (index, value) ستكون index هي 0 وستكون value هي a، مما سيتسبب بطباعة السطر الأول من الخرج. تعليمات let تحدثنا سابقًا عن استخدام الأنماط مع match و if let فقط، إلا أننا استخدمنا الأنماط في أماكن أُخرى أيضًا مثل تعليمة let. ألقِ نظرةً على عملية إسناد المتغير التالية باستخدام let: #![allow(unused)] fn main() { let x = 5; } اِستخدمت الأنماط في كلّ مرة استخدمت فيها تعليمة let بالشكل السابق دون معرفتك لذلك. تكون تعليمة let بالشكل التالي: let PATTERN = EXPRESSION; يشكّل اسم المتغير في التعليمات المشابهة لتعليمة let x = 5;‎ -مع اسم المتغير في فتحة PATTERN- نوعًا بسيطًا من الأنماط. تقارن رست التعابير مع الأنماط وتُسند أي اسم تجده، أي تتألف التعليمة let x = 5‎;‎ من نمط هو x يعني "اربط ما يتطابق هنا مع المتغير x"، ولأن الاسم x يمثّل كامل النمط، فإن هذا النمط يعني "اربط كل شيء مع المتغير x مهما تكُن القيمة". لملاحظة مطابقة النمط في let بوضوح أكبر، جرّب الشيفرة 4 التي تستخدم نمطًا مع let لتفكيك الصف. let (x, y, z) = (1, 2, 3); [الشيفرة 4: استخدام نمط لتفكيك الصف وإنشاء ثلاثة متغيرات بخطوة واحدة] طابقنا الصف مع النمط هنا، إذ تُقارن رست القيمة (3, 2, 1) مع النمط (x, y, z) وترى أن القيمة تطابق النمط فعلًا، لذلك تربط رست 1 إلى xو 2 إلى y و 3 إلى z. يمكن عدّ نمط الصف هذا بمثابة تضمين ثلاثة أنماط متغيرات مفردة داخله. إذا كان عدد العناصر في النمط لا يطابق عدد العناصر في الصف، لن يُطابَق النوع بأكمله وسنحصل على خطأ تصريفي. تبيّن الشيفرة 5 على سبيل المثال محاولة تفكيك صف بثلاثة عناصر إلى متغيرين وهي محاولة فاشلة. let (x, y) = (1, 2, 3); [الشيفرة 5: بناء خاطئ لنمط لا تطابق متغيراته عدد العناصر الموجودة في الصف] ستتسبب محاولة تصريف الشيفرة السابقة بالخطأ التالي: $ cargo run Compiling patterns v0.1.0 (file:///projects/patterns) error[E0308]: mismatched types --> src/main.rs:2:9 | 2 | let (x, y) = (1, 2, 3); | ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})` | | | expected a tuple with 3 elements, found one with 2 elements | = note: expected tuple `({integer}, {integer}, {integer})` found tuple `(_, _)` For more information about this error, try `rustc --explain E0308`. error: could not compile `patterns` due to previous error يمكننا تجاهل قيمة أو أكثر في الصف لتصحيح الخطأ وذلك باستخدام _ أو .. كما سنرى لاحقًا في قسم "تجاهل القيم في النمط". إذا كانت المشكلة هي وجود عدة متغيرات في النمط فإن الحل يكمن بجعل الأنواع تتطابق عن طريق إزالة المتغيرات حتى يتساوى عدد المتغيرات وعدد العناصر في الصف. معاملات الدالة Function Parameters يمكن لمعاملات الدالة أن تمثّل أنماطًا. ينبغي أن تكون الشيفرة 6 مألوفةً بالنسبة لك، إذ نصرّح فيها عن دالة اسمها foo تقبل معاملًا واحدًا اسمه x من النوع i32. fn foo(x: i32) { // تُكتب الشيفرة البرمجية هنا } [الشيفرة 6: بصمة دالة function signature تستخدم الأنماط في معاملاتها] يشكّل الجزء x نمطًا. يمكننا مطابقة الصف في وسيط الدالة مع النمط كما فعلنا مع let. تجزِّء الشيفرة 7 القيم الموجودة في الصف عندما نمررها إلى الدالة. اسم الملف: src/main.rs fn print_coordinates(&(x, y): &(i32, i32)) { println!("Current location: ({}, {})", x, y); } fn main() { let point = (3, 5); print_coordinates(&point); } [الشيفرة 7: دالة تفكك الصف مع معاملاتها] تطبع الشيفرة السابقة: Current location: (3, 5)‎، إذ تطابق القيم ‎&(3, 5)‎ النمط ‎&(x, y)‎، لذا فإن قيمة x هي 3 وقيمة y هي 5. يمكننا أيضًا استخدام الأنماط في قوائم معاملات التغليف closure parameter lists بطريقة قوائم معاملات الدالة function parameter lists ذاتها، لأن المغلفات مشابهة للدالات كما رأينا سابقًا في الفصل المغلفات closures في لغة رست. رأينا بحلول هذه النقطة عدة طرق لاستخدام الأنماط، إلا أن الأنماط لا تعمل بالطريقة ذاتها في كل مكان تُستخدم فيه، إذ يجب أن تكون الأنماط غير قابلة للدحض في بعض الأماكن وفي بعضها الآخر كذلك، وسنتحدث عن هذين المفهومين تاليًا. قابلية الدحض refutability واحتمالية فشل مطابقة النمط تأتي الأنماط بشكلين: قابلة للدحض أو النقض refutable وغير قابلة للدحض irrefutable، إذ تُدعى الأنماط التي تطابق أي قيمة تمرر خلالها بالأنماط القابلة للدحض، ومثال على ذلك هو x في التعليمة let x = 5;‎ وذلك لأن المتغير x سيطابق أي شيء وبالتالي لا تفشل المطابقة؛ بينما تُدعى الأنماط التي تفشل في بعض القيم بالأنماط القابلة للدحض، ومثال على ذلك هو Some(x)‎ في التعبير if let Some(x) = a_value لأن النمط Some(x)‎ لن يُطابق إذا كانت القيمة في المتغير a_value هي None عوضًا عن Some. تقبل معاملات الدالة وتعليمات let وحلقات for فقط الأنماط غير القابلة للدحض لأن البرنامج لا يستطيع عمل أي شيء مفيد عندما لا تتطابق القيم. يقبل التعبيران if let و while let الأنماط القابلة للدحض وغير القابلة للدحض، إلا أنّ المصرّف يحذّر من استخدام الأنماط غير القابلة للدحض، لأنها -بحسب تعريفها- ليست معدّة لتتعامل مع فشل محتمل، إذ تتمثّل الوظيفة الشرطية بقدرتها على التصرف بصورةٍ مختلفة اعتمادًا على النجاح أو الفشل. عمومًا، لا يهم كثيرّا التمييز بين الأنماط القابلة للدحض وغير القابلة للدحض، ولكن يجب أن يكون مفهوم قابلية الدحض مألوفًا، وذلك لحل الأخطاء التي قد تحصل، إذ يجب في تلك الحالات تغيير إما النمط أو البنية construct المستخدمة مع النمط حسب السلوك المُراد من الشيفرة. لنتابع مثالّا لما قد يحصل عندما نجرب استخدام نمط قابل للدحض عندما تتطلب رست نمطّا غير قابل للدحض -والعكس صحيح- إذ تبيّن الشيفرة 8 تعليمة let إلا أن النمط الذي حددناه هو Some(x)‎ وهو نمط قابل للدحض، ولن تُصرَّف الشيفرة كما هو متوقع. let Some(x) = some_option_value; [الشيفرة 8: محاولة استخدام نمط قابل للدحض مع let] ستفشل مطابقة النمط في Some(x)‎ إذا كانت القيمة في some_option_value هي None أي أن النمط هو قابل للدحض، ولكن تقبل تعليمة let فقط الأنماط غير القابلة للدحض لأنه لا توجد قيمة صالحة تستطيع الشيفرة استخدامها مع قيمة None. تنبّهنا رست عند استخدام قيمة قابلة للدحض عندما يتطلب الأمر وجود قيمة غير قابلة للدحض وقت التصريف: $ cargo run Compiling patterns v0.1.0 (file:///projects/patterns) error[E0005]: refutable pattern in local binding: `None` not covered --> src/main.rs:3:9 | 3 | let Some(x) = some_option_value; | ^^^^^^^ pattern `None` not covered | = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant = note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html note: `Option<i32>` defined here --> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:518:1 | = note: /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:522:5: not covered = note: the matched value is of type `Option<i32>` help: you might want to use `if let` to ignore the variant that isn't matched | 3 | let x = if let Some(x) = some_option_value { x } else { todo!() }; | ++++++++++ ++++++++++++++++++++++ help: alternatively, you might want to use let else to handle the variant that isn't matched | 3 | let Some(x) = some_option_value else { todo!() }; | ++++++++++++++++ For more information about this error, try `rustc --explain E0005`. error: could not compile `patterns` due to previous error تُعطينا رست الخطأ التصريفي السابق، وذلك بسبب عدم تغطيتنا لكل القيم الممكنة مع النمط Some(x)‎، ولن نستطيع فعل ذلك حتى لو أردنا. يمكننا إصلاح المشكلة في حال وجود نمط قابل للدحض يحل مكان نمط غير قابل للدحض عن طريق تغيير الشيفرة التي تستخدم النمط؛ فبدلًا من استخدام let نستخدم if let، وهكذا إذا لم يُطابق النمط تتخطى الشيفرة تلك الشيفرة الموجودة في القوسين المعقوصين وتستمر بذلك صلاحية الشيفرة. تبيّن الشيفرة 9 كيفية إصلاح الخطأ في الشيفرة 8. if let Some(x) = some_option_value { println!("{}", x); } [الشيفرة 9: استخدام if let وكتلة تحتوي على أنماط قابلة للدحض بدلًا من let] سُمح للشيفرة السابقة بالتصريف، فهذه الشيفرة صالحة، على الرغم من أنه لا يمكن استخدام نمط غير قابل للدحض دون رسالة خطأ. إذا أعطينا if let نمطًا يطابق دومًا مثل x كما في الشيفرة 10، سيمنحنا المصرّف تنبيهًا. fn main() { if let x = 5 { println!("{}", x); }; } [الشيفرة 10: محاولة استخدام نمط غير قابل للدحض مع if let] تشتكي رست من عدم منطقيّة استخدام if let مع نمط غير قابل للدحض: $ cargo run Compiling patterns v0.1.0 (file:///projects/patterns) warning: irrefutable `if let` pattern --> src/main.rs:2:8 | 2 | if let x = 5 { | ^^^^^^^^^ | = note: this pattern will always match, so the `if let` is useless = help: consider replacing the `if let` with a `let` = note: `#[warn(irrefutable_let_patterns)]` on by default warning: `patterns` (bin "patterns") generated 1 warning Finished dev [unoptimized + debuginfo] target(s) in 0.39s Running `target/debug/patterns` 5 يجب أن تستخدم مطابقة الأذرع match arms الأنماط القابلة للدحض لهذا السبب ما عدا الذراع الأخير، الذي يجب أن يطابق أي قيمة متبقية من النمط غير القابل للدحض. تسمح رست باستخدام نمط غير قابل للدحض في match باستخدام ذراع واحد فقط، ولكن الصياغة هذه ليست مفيدة ويمكن استبدالها بتعليمة let أبسط. بعدما عرفنا أماكن استخدام الأنماط والفرق بين الأنماط القابلة للدحض وغير القابلة للدحض، دعنا نكمل طريقة الصياغة syntax التي يمكن استخدامها لإنشاء الأنماط. ترجمة -وبتصرف- لقسم من الفصل Patterns and Matching من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: تنفيذ نمط تصميمي Design Pattern كائني التوجه Object-Oriented في لغة رست أنماط التصميم البرمجي Design patterns أنماط التصميم وتقنيات إعادة التصميم في Cpp توثيق أنماط التصميم
  25. لا يهم الأداء كثيرًا من أجل البرامج الصغيرة، فربما تستغرق ساعةً في كتابة برنامج نصي لأتمتة مهمة تحتاج ثواني لتُنفذ. حتى لو استغرقت وقتًا أطول فسينتهي البرنامج عندما تعود لمكتبك مع فنجان القهوة، إلا أنه من الضروري أحيانًا الاهتمام بتعلم كيفية جعل البرامج النصية أسرع، ولكن لا نستطيع معرفة إذا كان التغييرات قد حسّنت البرنامج إذا لم نكن نعرف كيفية قياس سرعة البرنامج. يأتي هنا دور وحدات مثل timeit و cProfile الخاصة بلغة بايثون، إذ تقيس هذه الوحدات سرعة تنفيذ الشيفرة فقط وتُنشئ أيضًا توصيفًا لأجزاء الشيفرة السريعة والأجزاء التي تحتاج إلى تحسين. سنتعلم في هذا الفصل -إضافةً إلى قياس سرعة البرنامج- إلى كيفية قياس الزيادات النظرية theoretical increases في وقت التنفيذ runtime مع نمو حجم البيانات الخاصة ببرنامجك. يُطلق على ذلك في علوم الحاسوب ترميز O الكبير big O notation. وحدة timeit تُعد مقولة "التحسين السابق لأوانه هو أصل كل شر Premature optimization is the root of all evil" مقولةً شائعةً في تطوير البرمجيات، والتي تُنسب إلى عالم الحاسوب دونالد نوث Donald Knuth، الذي ينسبها بدوره إلى طوني هوري Tony Hoare. وهو بدوره ينسبها إلى دونالد نوث Donald Knuth. تظهر أهمية التحسين السابق لأوانه Premature optimization أو التحسين قبل معرفة ما يجب تحسينه، عندما يستخدم المبرمجون خدعًا ذكيةً لتوفير الذاكرة وكتابة الشيفرة بصورةٍ أسرع. مثال عن هذه الخدع هي استخدام خوارزمية XOR للتبديل بين عددين صحيحين دون استخدام عدد ثالث مثل متغير مؤقت. >>> a, b = 42, 101 # ضبط المتغيرَين >>> print(a, b) 42 101 >>> # ‫ستبدّل سلسلة من عمليات XOR قيمتَي المتغيرَين >>> a = a ^ b >>> b = a ^ b >>> a = a ^ b >>> print(a, b) # بُدّلت القيم الآن 101 42 تبدو هذه الشيفرة مبهمة إذا لم تكن خوارزمية XOR مألوفة لديك (التي تستخدم المعامل الثنائي ^). المشكلة في استخدام خدع برمجية ذكية أنها تُنتج شيفرةً معقدةً وغير مقروءة، وذكرنا سابقًا أنّ أحد نقاط بايثون المهمة هي قابلية القراءة. في حالات أسوأ، يمكن ألا تكون الخدع الذكية ذكيةً إطلاقًا، إذ لا يمكن افتراض أن هذه الخدع أسرع أو أن الشيفرة التي تستبدلها هي بالأساس بطيئة. الطريقة الوحيدة لمعرفة ذلك هي قياس ومقارنة وقت التنفيذ، الذي هو الوقت الذي يستغرقه البرنامج لتنفيذ البرنامج أو قطعة من الشيفرة البرمجية. يجب أخذ العلم أن زيادة وقت التنفيذ يعني أن البرنامج يتباطأ؛ أي يستغرق وقتًا أطول لتنفيذ نفس كمية العمل (نستخدم أيضًا مصطلح "وقت التنفيذ" ليعني الوقت الذي يكون به البرنامج عاملًا. عندما نقول أن الخطأ قد حصل وقت التنفيذ، يعني أن الخطأ حصل عندما كان البرنامج يعمل وليس عندما كان يُصرّف إلى شيفرة ثنائية bytecode. يمكن لوحدة timeit الخاصة بالمكتبة القياسية لبايثون قياس سرعة وقت التنفيذ لأجزاء صغيرة من الشيفرة عن طريق تنفيذ الشيفرة آلاف أو ملايين المرات والسماح لك بتحديد وقت التنفيذ الوسطي. تعطِّل أيضًا وحدة timeit كانس المهملات garbage collector التلقائي للحصول على أوقات تنفيذ ثابتة. يمكنك تمرير سلسلة نصية متعددة الأسطر أو فصل أسطر الشيفرة باستخدام الفاصلة المنقوطة إذا أردت اختبار عدة أسطر: >>> import timeit >>> timeit.timeit('a, b = 42, 101; a = a ^ b; b = a ^ b; a = a ^ b') 0.1307766629999998 >>> timeit.timeit("""a, b = 42, 101 ... a = a ^ b ... b = a ^ b ... a = a ^ b""") 0.13515726800000039 تستغرق خوارزمية XOR على جهاز الحاسوب الخاص بي حوالي عشر الثانية لتنفيذ الشيفرة، هل هذا سريع؟ لنقارنها مع شيفرة تبديل الأعداد الصحيحة التي تستخدم متغير ثالث. >>> import timeit >>> timeit.timeit('a, b = 42, 101; temp = a; a = b; b = temp') 0.027540389999998638 هذه مفاجأة، ليست خوارزمية المتغير الثالث أسهل للقراءة فقط، لكنها أسرع بمرتين. خدعة XOR الذكية ربما توفر بعض البايتات من الذاكرة ولكن على حساب السرعة وسهولة القراءة. التضحية بسهولة قراءة الشيفرة لتوفير بعض البايتات من استخدام الذاكرة أو بضعة أجزاء من الثانية من وقت التنفيذ ليس بهذا القدر من الأهمية. المفاجأة الأفضل هي عند التبديل بين متغيرين باستخدام خدعة الإسناد المتعدد multiple assignment أو التفريغ المكرّر iterable unpacking التي تُنفذ أيضًا في وقت قصير: >>> timeit.timeit('a, b = 42, 101; a, b = b, a') 0.024489236000007963 ليس هذه الشيفرة هي الأسهل للقراءة فقط، لكنها الأسرع. عرفنا ذلك ليس لأننا افترضنا ولكن لأننا قسنا ذلك بموضوعية. يمكن أن تأخذ دالة timeit.timeit()‎ وسيط سلسلة نصية ثانٍ من شيفرة SETUP. تُنفَّذ شيفرة الإعداد هذه مرةً واحدةً قبل تنفيذ أول سلسلة نصية من الشيفرة. يمكن تغيير عدد المحاولات بتمرير عدد صحيح لوسيط الكلمة المفتاحية number. يختبر المثال التالي سرعة وحدة random الخاصة ببايثون لإنشاء عشرة ملايين رقم عشوائي من 1 إلى 100، وذد استغرق ذلك حوالي 10 ثوان على جهاز حاسوب ما. >>> timeit.timeit('random.randint(1, 100)', 'import random', number=10000000) 10.020913950999784 قياسيًا، لا تستطيع الشيفرة في السلسلة النصية المُمررة إلى timeit.timeit()‎ الوصول إلى المتغيرات والدوال في باقي البرنامج: >>> import timeit >>> spam = 'hello' #‫نعرّف المتغير spam >>> timeit.timeit('print(spam)', number=1) # نقيس الوقت المستغرق لطباعة المتغير‫ spam Traceback (most recent call last): File "<stdin>", line 1, in <module> File "C:\Users\Al\AppData\Local\Programs\Python\Python37\lib\timeit.py", line 232, in timeit return Timer(stmt, setup, timer, globals).timeit(number) File "C:\Users\Al\AppData\Local\Programs\Python\Python37\lib\timeit.py", line 176, in timeit timing = self.inner(it, self.timer) File "<timeit-src>", line 6, in inner NameError: name 'spam' is not defined لإصلاح ذلك، مرّر الدالة والقيمة المُعادة للدالة globals()‎ إلى وسيط الكلمة المفتاحية globals: >>> timeit.timeit('print(spam)', number=1, globals=globals()) hello 0.000994909999462834 قاعدة جيدة في كتابة الشيفرة هي أن تجعل الشيفرة تعمل ومن ثم جعلها سريعة، إذ يمكنك التركيز على جعل الشيفرة أكثر كفاءة بعد الحصول على شيفرة تعمل. فحص الأداء بواسطة cProfile على الرغم من أن وحدة timeit مفيدة لقياس أجزاء صغيرة من الشيفرة، إلا أن وحدة cProfile مفيدةٌ أكثر في تحليل دوال أو برامج كاملة. يحلًل فحص الأداء Profiling سرعة واستخدام الذاكرة وبعض النواحي الأخرى للبرنامج الخاص بك. تُعد وحدة cProfile هي فاحص الأداء profiler الخاص ببايثون أو البرنامج الذي يستطيع قياس وقت تنفيذ البرنامج، إضافةً لإنشاء توصيف لأوقات تنفيذ استدعاءات دوال البرنامج كلٌّ على حِدى. تقدم هذه المعلومات قياسات أكثر دقة للشيفرة الخاصة بك. يمرر محلًل cProfile سلسلةً نصيةً من الشيفرة التي تريد قياسها إلى cProfile.run()‎. لنتابع كيف يقيس cProfiler ويعطي تقريرًا عن تنفيذ دالة قصيرة تجمع كل الأرقام من 1 إلى 1,000,000: import time, cProfile def addUpNumbers(): total = 0 for i in range(1, 1000001): total += i cProfile.run('addUpNumbers()') يكون الخرج عند تنفيذ البرنامج على النحو التالي: 4 function calls in 0.064 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.064 0.064 <string>:1(<module>) 1 0.064 0.064 0.064 0.064 test1.py:2(addUpNumbers) 1 0.000 0.000 0.064 0.064 {built-in method builtins.exec} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} يمثل كل سطر دالةً مختلفةً والوقت المستغرق في تلك الدالة. تكون الأعمدة في خرج cProfile.run()‎ على النحو التالي: ncalls: عدد استدعاءات الدالة. tottime: الوقت الكلي المستغرق في الدالة ما عدا الوقت في الدوال الفرعية. percall: الوقت الكلي مقسومًا على عدد الاستدعاءات. cumtime: الوقت التراكمي المستغرق في الدالة ولك الدوال الفرعية. percall: الوقت التراكمي مقسومًا على عدد الاستدعاءات. filename:lineno(function)‎: الملف الذي فيه الدالة وفي أي رقم سطر. مثال: نزّل الملفين "rsaCipher.py" و "al_sweigart_pubkey.txt" من الموقع. أدخل ما يلي على الصدفة التفاعلية لتحليل دالة encryptAndWriteToFile()‎ أثناء تشفير رسالة مكونة من 300,000 محرفًا ومُنشأة باستخدام التعبير ‎'abc' * 100000: >>> import cProfile, rsaCipher >>> cProfile.run("rsaCipher.encryptAndWriteToFile('encrypted_file.txt', 'al_sweigart_pubkey.txt', 'abc'*100000)") 11749 function calls in 28.900 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.001 0.001 28.900 28.900 <string>:1(<module>) 2 0.000 0.000 0.000 0.000 _bootlocale.py:11(getpreferredencoding) --snip-- 1 0.017 0.017 28.900 28.900 rsaCipher.py:104(encryptAndWriteToFile) 1 0.248 0.248 0.249 0.249 rsaCipher.py:36(getBlocksFromText) 1 0.006 0.006 28.873 28.873 rsaCipher.py:70(encryptMessage) 1 0.000 0.000 0.000 0.000 rsaCipher.py:94(readKeyFile) --snip-- 2347 0.000 0.000 0.000 0.000 {built-in method builtins.len} 2344 0.000 0.000 0.000 0.000 {built-in method builtins.min} 2344 28.617 0.012 28.617 0.012 {built-in method builtins.pow} 2 0.001 0.000 0.001 0.000 {built-in method io.open} 4688 0.001 0.000 0.001 0.000 {method 'append' of 'list' objects} --snip-- يمكنك ملاحظة أن الشيفرة التي مررناها إلى cProfile.run()‎ استغرقت 28.9 ثانية لتنتهي. انتبه إلى الدوال بأطول الأوقات الكلية، وفي حالتنا هي الدالة pow()‎ التي تستغرق 28.617 ثانية، وهذا تقريبًا هو كل وقت تنفيذ الشيفرة. لا يمكن تعديل هذه الشيفرة (هي جزء من بايثون) ولكن ربما نستطيع الاعتماد بصورةٍ أقل عليها، وهذا غير ممكن في هذه الحالة، لأن برنامج rsaCipher.py مُحسن جيدًا، إلا أن تحليل هذه الشيفرة أعطانا نظرةً إلى أن عنق الزجاجة الأساسي هو pow()‎ لذا لا يوجد فائدة من محاولة تحسين الدالة readKeyFile()‎ التي لا تستغرق وقت تنفيذ أبدًا حتى أن cProfile أعطانا وقت لتنفيذها يبلغ 0. هذه الفكرة موجودة في قانون أمدال Amdahl's Law وهي معادلة تحسب كيف يُسرّع البرنامج إذا حسّننا أحد أجزائه، فوفقًا لمعادلة أمدال يكون تسريع المهمة الكلي مساويًا إلى: 1‎ / ((1 – p) + (p / s))‎ إذ يمثّل s التسريع الحاصل لأحد الأجزاء، و p هو نسبة ذلك الجزء من كل البرنامج، أي إذا ضاعفنا سرعة أحد الأجزاء الذي يشكل 90% من وقت تنفيذ البرنامج سنحصل على تسريع بنسبة 82% لكل البرنامج: ‎1 / ((1 – 0.9) + (0.9 / 2)) = 1.818 وهذا أفضل من تسريع جزء بمقدار ثلاث أضعاف، ولكنه يشكّل 25% من وقت التنفيذ الكلي، الذي يعطي نسبة 14% تسريع كلي: 1‎ / ((1 – 0.25) + (0.25 / 2)) = 1.143 لست بحاجة حفظ هذه المعادلة فقط تذكر أن مضاعفة سرعة أجزاء الشيفرة البطيئة أو الطويلة مفيدٌ أكثر من مضاعفة سرعة قسم قصير أو سريع. هذه يجب أن تكون معرفة عامة، حسم 10% من بيت باهظ الثمن أفضل من حسم 10% من زوج أحذية رخيص. الخلاصة تأتي مكتبة بايثون القياسية مع وحدتين للتحليل timeit و cProfiler. تفيد الدالة time.timeit()‎ في تنفيذ قطع صغيرة من الشيفرة للمقارنة بين سرعة كل قطعة منها. تقدم دالة cProfile.run()‎ تقريرًا مفصلًا للتوابع الأكبر وتدل على وجود عنق زجاجة. من المهم قياس أداء الشيفرة الخاصة بك بدلًا من تقدير ذلك، إذ يمكن لبعض حيل تسريع البرامج إبطائه بالحقيقة، أو ربما تستغرق وقتًا أطول في تحسين ما هو جزء بسيط من البرنامج الخاص بك. يوضح ذلك قانون أمدال رياضيًا، إذ تصف هذه المعادلة أثر تسريع مكون واحد على كل البرنامج. ترجمة -وبتصرف- لقسم من الفصل Measuring Performance And Big O Algorithm Analysis من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: استخدامات متقدمة لنظام التحكم بالإصدار Git لإدارة مشاريع بايثون الوحدات Modules والحزم Packages في بايثون أنواع البيانات والعمليات الأساسية في لغة بايثون
×
×
  • أضف...