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

في الوقت نفسه ، في مدونة المؤلف ، وجدنا مقالًا مكتوبًا ، على ما يبدو ، في المراحل الأولى من العمل على الكتاب ويسمح بإحداث انطباع بمادته. نقترح مناقشة مدى أهمية أفكار المؤلف والكتاب بأكمله
المريخ المناخ المدارتحطمت المركبة الفضائية المريخية المدارية خلال الهبوط وانهارت في الغلاف الجوي للمريخ ، حيث أعطى مكون برنامج لوكهيد قيمة الزخم ، ويقاس بالثواني بقوة الجنيه ، في حين أن المكون الآخر الذي طورته ناسا أخذ قيمة الزخم في نيوتن ثانية.
يمكنك تخيل المكون الذي طورته ناسا في الشكل التالي تقريبًا:
يمكنك أيضًا أن تتخيل أن مكون Lockheed يسمى الكود أعلاه كما يلي:
void main() { trajectory_correction(1.5 ); }
تبلغ قوة الجنيه الثانية (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
:
إذا قمت بالاتصال بـ
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); } }
في الأساس ، نحن نسعى جاهدين لوظائفنا لإرجاع نتيجة أو خطأ ، بدلاً من نتيجة وخطأ. وبالتالي ، فإننا نستبعد الشروط التي لدينا فيها أخطاء ، وكذلك نحن في مأمن من النتائج غير المقبولة ، والتي يمكن أن تتسرب إلى حسابات أخرى.
من وجهة النظر هذه ، يعتبر إلقاء الاستثناءات أمرًا طبيعيًا ، لأنه يتوافق مع المبدأ الموضح أعلاه: إما أن تقوم دالة بإرجاع نتيجة أو رمي استثناء.
RAIIRAII تعني اكتساب الموارد هو التهيئة ، ولكن إلى حد كبير يرتبط هذا المبدأ بإصدار الموارد. ظهر الاسم أولاً في C ++ ، ومع ذلك ، يمكن تنفيذ هذا النمط بأي لغة (راجع ، على سبيل المثال ،
IDisposable
من .NET). يوفر RAII التنظيف التلقائي للموارد.
ما هي الموارد؟ فيما يلي بعض الأمثلة: الذاكرة الديناميكية ، اتصالات قاعدة البيانات ، واصفات نظام التشغيل. من حيث المبدأ ، المورد هو شيء مأخوذ من العالم الخارجي ويخضع للعودة بعد أن لم نعد بحاجة إليه. نرجع المورد باستخدام العملية المناسبة: حرره ، احذفه ، أغلقه ، إلخ.
نظرًا لأن هذه الموارد خارجية ، فلا يتم التعبير عنها صراحة في نظامنا النوعي. على سبيل المثال ، إذا اخترنا جزءًا من الذاكرة الديناميكية ، فسوف نحصل على مؤشر يتعين علينا استدعاء
delete
:
struct Foo {}; void example() { Foo* foo = new 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 وإدارة الموارد التلقائية.
لذلك ، تساعد الأنواع كثيرًا في جعل الشفرة أكثر أمانًا وتكييفها لإعادة الاستخدام.