الكتابة الصحيحة: الجانب الاستخفاف بالكود النظيف

مرحبا الزملاء.

منذ وقت ليس ببعيد ، جذب انتباهنا الكتاب الذي تم الانتهاء منه تقريبًا من قِبل Manning Publishing House "برمجة مع أنواع" ، والذي يفصل أهمية الكتابة الصحيحة ودورها في كتابة رمز نظيف وطويل الأمد.



في الوقت نفسه ، في مدونة المؤلف ، وجدنا مقالًا مكتوبًا ، على ما يبدو ، في المراحل الأولى من العمل على الكتاب ويسمح بإحداث انطباع بمادته. نقترح مناقشة مدى أهمية أفكار المؤلف والكتاب بأكمله

المريخ المناخ المدار

تحطمت المركبة الفضائية المريخية المدارية خلال الهبوط وانهارت في الغلاف الجوي للمريخ ، حيث أعطى مكون برنامج لوكهيد قيمة الزخم ، ويقاس بالثواني بقوة الجنيه ، في حين أن المكون الآخر الذي طورته ناسا أخذ قيمة الزخم في نيوتن ثانية.

يمكنك تخيل المكون الذي طورته ناسا في الشكل التالي تقريبًا:

//    ,  >= 2 N s void trajectory_correction(double momentum) { if (momentum < 2 /* N s */) { disintegrate(); } /* ... */ } 

يمكنك أيضًا أن تتخيل أن مكون Lockheed يسمى الكود أعلاه كما يلي:

 void main() { trajectory_correction(1.5 /* lbf s */); } 

تبلغ قوة الجنيه الثانية (lbfs) حوالي 4.448222 نيوتن في الثانية (Ns). وبالتالي ، من وجهة نظر Lockheed ، يجب أن يكون تمرير 1.5 lbfs إلى تصحيح trajectory_correction طبيعيًا تمامًا: 1.5 lbfs حوالي 6.672333 Ns ، أعلى بكثير من عتبة 2 Ns.

المشكلة هي تفسير البيانات. نتيجة لذلك ، يقارن مكون NASA lbfs مع Ns دون تحويل ويفسر عن طريق الخطأ الإدخال إلى lbfs كمدخل إلى Ns. منذ 1.5 أقل من 2 ، انهار المدار. هذا هو مضادات معروفة تسمى الهوس البدائي.

هاجس بدائيون

يظهر التثبيت على العناصر الأولية عندما نستخدم نوع البيانات البدائية لتمثيل قيمة في مجال المشكلة والسماح بمواقف مثل تلك الموضحة أعلاه. إذا كنت تمثل الرموز البريدية كأرقام وأرقام هواتف مثل سلاسل و Ns و lbfs كأرقام مزدوجة الدقة ، فهذا هو بالضبط ما يحدث.

سيكون أكثر أمانًا تحديد نوع بسيط من Ns :

 struct Ns { double value; }; bool operator<(const Ns& a, const Ns& b) { return a.value < b.value; } 

وبالمثل ، يمكنك تحديد نوع بسيط من lbfs :

 struct lbfs { double value; }; bool operator<(const lbfs& a, const lbfs& b) { return a.value < b.value; } 

الآن يمكنك تطبيق متغير آمن للنوع من trajectory_correction :

 //  ,   >= 2 N s void trajectory_correction(Ns momentum) { if (momentum < Ns{ 2 }) { disintegrate(); } /* ... */ } 

إذا قمت بالاتصال بـ lbfs ، كما في المثال أعلاه ، فإن التعليمات البرمجية ببساطة لا يتم تجميعها بسبب عدم التوافق:

 void main() { trajectory_correction(lbfs{ 1.5 }); } 

لاحظ كيف يتم رسم معلومات نوع القيمة ، والتي يشار إليها عادة في التعليقات ، ( 2 /*Ns */, /* lbfs */ ) الآن في نظام الكتابة ويتم التعبير عنها في الكود: ( Ns{ 2 }, lbfs{ 1.5 } ) .

بالطبع ، من الممكن توفير انخفاض في عدد lbfs إلى Ns في شكل عامل تشغيل صريح:

 struct lbfs { double value; explicit operator Ns() { return value * 4.448222; } }; 

مسلحين بهذه التقنية ، يمكنك استدعاء trajectory_correction باستخدام قالب ثابت:

 void main() { trajectory_correction(static_cast<Ns>(lbfs{ 1.5 })); } 

هنا يتم تحقيق صحة الكود عن طريق ضرب المعامل. يمكن أيضًا تنفيذ المدلى بها ضمنيًا (باستخدام الكلمة الأساسية الضمنية) ، وفي هذه الحالة سيتم تطبيق المدلى بها تلقائيًا. كقاعدة تجريبية ، يمكنك استخدام أحد بيثون coans هنا:
الصريح أفضل من الضمني
تتمثل مغزى هذه القصة في أنه على الرغم من أن لدينا اليوم آليات ذكية للتحقق من النوع ، إلا أنها لا تزال بحاجة إلى تقديم معلومات كافية للوقوف على هذا النوع من الأخطاء. تدخل هذه المعلومات في البرنامج إذا أعلنا أنواعًا مع الأخذ بعين الاعتبار خصوصيات مجالنا.

مساحة الدولة

المشكلة تحدث عندما ينتهي البرنامج في حالة سيئة . تساعد الأنواع في تضييق المجال لحدوثها. دعنا نحاول التعامل مع النوع كمجموعة من القيم الممكنة. على سبيل المثال ، bool هي المجموعة {true, false} ، حيث يمكن لمتغير من هذا النوع أن يأخذ إحدى هاتين القيمتين. وبالمثل ، uint32_t هي المجموعة {0 ...4294967295} . بالنظر إلى الأنواع بهذه الطريقة ، يمكننا تحديد مساحة حالة برنامجنا كمنتج لأنواع جميع المتغيرات الحية في وقت معين.

إذا كان لدينا متغير من النوع bool ومتغير من النوع uint32_t ، فستكون مساحة حالتنا {true, false} X {0 ...4294967295} . هذا يعني فقط أن كلا المتغيرين يمكن أن يكونا في أي حالة ممكنة لهما ، ولأن لدينا متغيرين ، يمكن أن ينتهي البرنامج في أي حالة مشتركة من هذين النوعين.

يصبح كل شيء أكثر إثارة للاهتمام إذا أخذنا في الاعتبار الوظائف التي تهيئ القيم:

 bool get_momentum(Ns& momentum) { if (!some_condition()) return false; momentum = Ns{ 3 }; return true; } 

في المثال أعلاه ، نأخذ Ns بالرجوع ونبدأ التهيئة إذا تم استيفاء بعض الشروط. ترجع الدالة true إذا تمت تهيئة القيمة بشكل صحيح. إذا تعذر على الدالة لسبب ما تعيين القيمة ، فستُرجع الدالة false .

بالنظر إلى هذا الموقف من وجهة نظر مساحة الدولة ، يمكننا أن نقول أن مساحة الدولة هي نتاج bool X Ns . إذا عادت الدالة إلى صواب ، فهذا يعني أن الدافع قد تم تعيينه ، وهي واحدة من القيم المحتملة للقيمة Ns . المشكلة هي: إذا أرجعت الدالة false ، فهذا يعني أنه لم يتم تعيين الدافع. بطريقة أو بأخرى ، ينتمي الزخم إلى مجموعة القيم المحتملة للقيمة Ns ، لكنها ليست قيمة صالحة. غالبًا ما يكون هناك أخطاء تبدأ فيها الحالة غير المقبولة التالية بالانتشار بطريق الخطأ:

 void example() { Ns momenum; get_momentum(momentum); trajectory_correction(momentum); } 

بدلاً من ذلك ، علينا ببساطة القيام بذلك:

 void example() { Ns momentum; if (get_momentum(momentum)) { trajectory_correction(momentum); } } 

ومع ذلك ، هناك طريقة أفضل يمكن القيام بها بهذا بالقوة:

 std::optional<Ns> get_momentum() { if (!some_condition()) return std::nullopt; return std::make_optional(Ns{ 3 }); } 

إذا كنت تستخدم optional ، فسوف تقل مساحة حالة هذه الوظيفة بشكل كبير: بدلاً من bool X Ns نحصل على Ns + 1 . ستُرجع هذه الوظيفة nullopt Ns أو nullopt صالحة للإشارة إلى عدم وجود قيمة. الآن ، ببساطة لا يمكننا امتلاك Ns غير صالح ينتشر في النظام. الآن أصبح من المستحيل نسيان التحقق من قيمة الإرجاع ، حيث لا يمكن تحويلها اختياريًا إلى Ns - سنحتاج إلى فكها خصيصًا:

 void example() { auto maybeMomentum = get_momentum(); if (maybeMomentum) { trajectory_correction(*maybeMomentum); } } 

في الأساس ، نحن نسعى جاهدين لوظائفنا لإرجاع نتيجة أو خطأ ، بدلاً من نتيجة وخطأ. وبالتالي ، فإننا نستبعد الشروط التي لدينا فيها أخطاء ، وكذلك نحن في مأمن من النتائج غير المقبولة ، والتي يمكن أن تتسرب إلى حسابات أخرى.

من وجهة النظر هذه ، يعتبر إلقاء الاستثناءات أمرًا طبيعيًا ، لأنه يتوافق مع المبدأ الموضح أعلاه: إما أن تقوم دالة بإرجاع نتيجة أو رمي استثناء.

RAII

RAII تعني اكتساب الموارد هو التهيئة ، ولكن إلى حد كبير يرتبط هذا المبدأ بإصدار الموارد. ظهر الاسم أولاً في C ++ ، ومع ذلك ، يمكن تنفيذ هذا النمط بأي لغة (راجع ، على سبيل المثال ، IDisposable من .NET). يوفر RAII التنظيف التلقائي للموارد.

ما هي الموارد؟ فيما يلي بعض الأمثلة: الذاكرة الديناميكية ، اتصالات قاعدة البيانات ، واصفات نظام التشغيل. من حيث المبدأ ، المورد هو شيء مأخوذ من العالم الخارجي ويخضع للعودة بعد أن لم نعد بحاجة إليه. نرجع المورد باستخدام العملية المناسبة: حرره ، احذفه ، أغلقه ، إلخ.

نظرًا لأن هذه الموارد خارجية ، فلا يتم التعبير عنها صراحة في نظامنا النوعي. على سبيل المثال ، إذا اخترنا جزءًا من الذاكرة الديناميكية ، فسوف نحصل على مؤشر يتعين علينا استدعاء delete :

 struct Foo {}; void example() { Foo* foo = new Foo(); /*  foo */ delete foo; } 

ولكن ماذا يحدث إذا نسينا القيام بذلك ، أو هل يمنعنا شيء من الاتصال delete ؟

 void example() { Foo* foo = new Foo(); throw std::exception(); delete foo; } 

في هذه الحالة ، لم نعد ندعو delete والحصول على تسرب الموارد. من حيث المبدأ ، هذا التنظيف اليدوي للموارد غير مرغوب فيه. بالنسبة للذاكرة الديناميكية ، لدينا unique_ptr لمساعدتنا على إدارتها:

 void example() { auto foo = std::make_unique<Foo>(); throw std::exception(); } 

إن unique_ptr الخاص unique_ptr عبارة عن كائن مكدس ، لذلك إذا unique_ptr النطاق (عندما ترمي الدالة استثناءً أو عندما يختتم المكدس عندما يتم طرح استثناء) ، يتم استدعاء المدمر الخاص به. هذا هو destructor الذي ينفذ استدعاء delete . وفقًا لذلك ، لم يعد علينا إدارة مورد الذاكرة - فنحن ننقل هذا العمل إلى برنامج المجمّع الذي يمتلكه وهو المسؤول عن إصداره.

توجد أغلفة مماثلة (أو يمكن إنشاؤها) لأي موارد أخرى (على سبيل المثال ، يمكن لف OS HANDLE من ويندوز في نوع ، وفي هذه الحالة سوف CloseHandle المدمر CloseHandle ).

الاستنتاج الرئيسي في هذه الحالة هو عدم إجراء التنظيف اليدوي للموارد ؛ إما أن تستخدم المجمّع الموجود ، أو إذا لم يكن هناك برنامج تغليف مناسب لسيناريوك المحدد ، فسنطبقه بنفسنا.

استنتاج

بدأنا هذه المقالة بمثال معروف يوضح أهمية الكتابة ، ثم درسنا ثلاثة جوانب مهمة لاستخدام أنواع للمساعدة في كتابة رمز أكثر أمانًا:

  • التصريح واستخدام أنواع أقوى (على عكس الهوس بالبدائية).
  • تقليل مساحة الدولة ، وإرجاع نتيجة أو خطأ ، وليس نتيجة أو خطأ.
  • RAII وإدارة الموارد التلقائية.

لذلك ، تساعد الأنواع كثيرًا في جعل الشفرة أكثر أمانًا وتكييفها لإعادة الاستخدام.

Source: https://habr.com/ru/post/ar460149/


All Articles