القضاء على جامع القمامة: طريقة RAII
نشرت: 2022-03-11في البداية ، كان هناك C. في C ، هناك ثلاثة أنواع من تخصيص الذاكرة: ثابت ، وتلقائي ، وديناميكي. المتغيرات الثابتة هي الثوابت المضمنة في الملف المصدر ، ولأنها عرفت الأحجام ولم تتغير أبدًا ، فهي ليست كلها مثيرة للاهتمام. يمكن اعتبار التخصيص التلقائي بمثابة تخصيص مكدس - يتم تخصيص المساحة عند إدخال كتلة معجمية ، ويتم تحريرها عند الخروج من تلك الكتلة. أهم ميزة لها مرتبطة مباشرة بذلك. حتى C99 ، كانت المتغيرات المخصصة تلقائيًا مطلوبة لمعرفة أحجامها في وقت الترجمة. هذا يعني أن أي سلسلة ، وقائمة ، وخريطة ، وأي بنية مشتقة منها يجب أن تعيش في كومة ، في ذاكرة ديناميكية.
تم تخصيص الذاكرة الديناميكية وتحريرها بشكل صريح بواسطة المبرمج باستخدام أربع عمليات أساسية: malloc و realloc و calloc و free. أول اثنين من هذه لا تؤدي أي تهيئة على الإطلاق ، قد تحتوي الذاكرة على cruft. كل منهم ماعدا الحرة يمكن أن تفشل. في هذه الحالة ، يقومون بإرجاع مؤشر فارغ ، يكون الوصول إليه سلوكًا غير محدد ؛ في أفضل الأحوال ، ينفجر برنامجك. في أسوأ الحالات ، يبدو أن برنامجك يعمل لفترة من الوقت ، حيث يعالج البيانات المهملة قبل الانفجار.
القيام بالأشياء بهذه الطريقة هو نوع من الألم لأنك ، المبرمج ، المسؤول الوحيد عن الحفاظ على مجموعة من الثوابت التي تتسبب في انفجار برنامجك عند انتهاكه. يجب أن يكون هناك استدعاء malloc قبل الوصول إلى المتغير. يجب عليك التحقق من أن malloc عاد بنجاح قبل استخدام المتغير الخاص بك. يجب أن توجد مكالمة مجانية واحدة بالضبط لكل malloc في مسار التنفيذ. إذا كانت صفرًا ، فإن الذاكرة تتسرب. إذا كان هناك أكثر من واحد ، فإن برنامجك ينفجر. قد لا تكون هناك محاولات وصول إلى المتغير بعد تحريره. دعنا نرى مثالاً لما يبدو عليه هذا في الواقع:
int main() { char *str = (char *) malloc(7); strcpy(str, "toptal"); printf("char array = \"%s\" @ %u\n", str, str); str = (char *) realloc(str, 11); strcat(str, ".com"); printf("char array = \"%s\" @ %u\n", str, str); free(str); return(0); } $ make runc gcc -oc cc ./c char * (null terminated): toptal @ 66576 char * (null terminated): toptal.com @ 66576هذا الرمز ، على الرغم من بساطته ، يحتوي بالفعل على نموذج مضاد واحد وقرار واحد مشكوك فيه. في الحياة الواقعية ، يجب ألا تكتب أبدًا عدد البايتات على أنها حرفية ، ولكن بدلاً من ذلك استخدم حجم الدالة. وبالمثل ، نخصص مصفوفة char * لحجم السلسلة التي نحتاجها بالضبط مرتين (واحد أكثر من طول السلسلة ، لحساب النهاية الخالية) ، وهي عملية مكلفة إلى حد ما. قد يقوم برنامج أكثر تعقيدًا بإنشاء مخزن مؤقت أكبر للسلسلة ، مما يسمح لحجم السلسلة بالنمو.
اختراع RAII: أمل جديد
كل تلك الإدارة اليدوية كانت غير سارة ، على أقل تقدير. في منتصف الثمانينيات ، اخترع Bjarne Stroustrup نموذجًا جديدًا للغته الجديدة ، C ++. أطلق عليه اسم `` اكتساب الموارد هو التهيئة '' ، وكانت الأفكار الأساسية كما يلي: يمكن تحديد الكائنات ليكون لها مُنشئون ومدمرون يتم استدعاؤهم تلقائيًا في الأوقات المناسبة بواسطة المترجم ، وهذا يوفر طريقة أكثر ملاءمة لإدارة الذاكرة كائنًا معينًا تتطلب ، وهذه التقنية مفيدة أيضًا للموارد التي ليست ذاكرة.
هذا يعني أن المثال أعلاه ، في C ++ ، أكثر نظافة:
int main() { std::string str = std::string ("toptal"); std::cout << "string object: " << str << " @ " << &str << "\n"; str += ".com"; std::cout << "string object: " << str << " @ " << &str << "\n"; return(0); } $ g++ -o ex_1 ex_1.cpp && ./ex_1 string object: toptal @ 0x5fcaf0 string object: toptal.com @ 0x5fcaf0لا توجد إدارة يدوية للذاكرة في الأفق! يتم إنشاء كائن السلسلة ، وله طريقة محملة بشكل زائد ، ويتم إتلافها تلقائيًا عند إنهاء الوظيفة. لسوء الحظ ، يمكن أن تؤدي هذه البساطة نفسها إلى مضاعفات أخرى. لنلقِ نظرة على مثال بشيء من التفصيل:
vector<string> read_lines_from_file(string &file_name) { vector<string> lines; string line; ifstream file_handle (file_name.c_str()); while (file_handle.good() && !file_handle.eof()) { getline(file_handle, line); lines.push_back(line); } file_handle.close(); return lines; } int main(int argc, char* argv[]) { // get file name from the first argument string file_name (argv[1]); int count = read_lines_from_file(file_name).size(); cout << "File " << file_name << " contains " << count << " lines."; return 0; } $ make cpp && ./c++ makefile g++ -o c++ c++.cpp File makefile contains 38 lines.كل هذا يبدو واضحًا إلى حد ما. يتم تعبئة خطوط المتجه وإعادتها واستدعائها. ومع ذلك ، كوننا مبرمجين أكفاء يهتمون بالأداء ، فإن شيئًا ما حول هذا يزعجنا: في بيان الإرجاع ، يتم نسخ المتجه إلى متجه جديد بسبب دلالات القيمة في اللعب ، قبل وقت قصير من تدميره.
لم يعد هذا صحيحًا تمامًا في C ++ الحديثة بعد الآن. قدمت C ++ 11 مفهوم دلالات الحركة ، حيث يتم ترك الأصل في حالة صالحة (بحيث لا يزال من الممكن تدميره بشكل صحيح) ولكن في حالة غير محددة. تعد استدعاءات الإرجاع حالة سهلة جدًا للمترجم لتحسين نقل الدلالات ، لأنه يعلم أنه سيتم تدمير الأصل قبل وقت قصير من أي وصول إضافي. ومع ذلك ، فإن الغرض من هذا المثال هو توضيح سبب اختراع الناس لمجموعة كاملة من اللغات التي تم جمعها بالقمامة في أواخر الثمانينيات وأوائل التسعينيات ، وفي تلك الأوقات لم تكن دلالات حركة C ++ متاحة.
بالنسبة للبيانات الكبيرة ، يمكن أن يصبح هذا مكلفًا. دعونا نحسن هذا ، ونعيد المؤشر فقط. هناك بعض التغييرات في بناء الجملة ، ولكن بخلاف ذلك فهي نفس الكود:
في الواقع ، المتجه هو مؤشر للقيمة: هيكل صغير نسبيًا يحتوي على مؤشرات للعناصر الموجودة في الكومة. بالمعنى الدقيق للكلمة ، ليست مشكلة إعادة المتجه ببساطة. سيعمل المثال بشكل أفضل إذا تم إرجاع مصفوفة كبيرة. نظرًا لأن محاولة قراءة ملف في مصفوفة مخصصة مسبقًا ستكون غير منطقية ، فإننا نستخدم المتجه بدلاً من ذلك. فقط تظاهر بأنه هيكل بيانات كبير بشكل غير عملي ، من فضلك.
vector<string> * read_lines_from_file(string &file_name) { vector<string> * lines; string line; ifstream file_handle (file_name.c_str()); while (file_handle.good() && !file_handle.eof()) { getline(file_handle, line); lines->push_back(line); } file_handle.close(); return lines; } $ make cpp && ./c++ makefile g++ -o c++ c++.cpp Segmentation fault (core dumped)أوتش! الآن بعد أن أصبحت الخطوط مؤشرًا ، يمكننا أن نرى أن المتغيرات التلقائية تعمل كما هو معلن عنه: يتم تدمير المتجه مع مغادرة نطاقه ، تاركًا المؤشر يشير إلى موقع أمامي في المكدس. خطأ التجزئة هو مجرد محاولة للوصول إلى ذاكرة غير قانونية ، ولذا كان من المفترض حقًا أن نتوقع ذلك. مع ذلك ، نريد استعادة سطور الملف من وظيفتنا بطريقة ما ، والشيء الطبيعي هو ببساطة نقل المتغير الخاص بنا خارج المكدس إلى الكومة. يتم ذلك باستخدام الكلمة الرئيسية الجديدة. يمكننا ببساطة تحرير سطر واحد من ملفنا ، حيث نحدد الأسطر:
vector<string> * lines = new vector<string>; $ make cpp && ./c++ makefile g++ -o c++ c++.cpp File makefile contains 38 lines.لسوء الحظ ، على الرغم من أن هذا يبدو أنه يعمل بشكل مثالي ، إلا أنه لا يزال به عيب: إنه يتسبب في تسرب الذاكرة. في C ++ ، يجب حذف المؤشرات إلى كومة الذاكرة المؤقتة يدويًا بعد أن لم تعد هناك حاجة إليها ؛ إذا لم يكن الأمر كذلك ، تصبح هذه الذاكرة غير متاحة بمجرد خروج المؤشر الأخير من النطاق ، ولا يتم استردادها حتى يديرها نظام التشغيل عند انتهاء العملية. تستخدم لغة C ++ الاصطلاحية هنا علامة فريدة من نوعها ، والتي تنفذ السلوك المطلوب. يحذف الكائن المشار إليه عندما يقع المؤشر خارج النطاق. ومع ذلك ، لم يكن هذا السلوك جزءًا من اللغة حتى C ++ 11.
في هذا المثال ، يمكن إصلاح ذلك بسهولة:
vector<string> * read_lines_from_file(string &file_name) { vector<string> * lines = new vector<string>; string line; ifstream file_handle (file_name.c_str()); while (file_handle.good() && !file_handle.eof()) { getline(file_handle, line); lines->push_back(line); } file_handle.close(); return lines; } int main(int argc, char* argv[]) { // get file name from the first argument string file_name (argv[1]); vector<string> * file_lines = read_lines_from_file(file_name); int count = file_lines->size(); delete file_lines; cout << "File " << file_name << " contains " << count << " lines."; return 0; }لسوء الحظ ، مع توسع البرامج إلى ما بعد نطاق الألعاب ، يصبح من الصعب بسرعة التفكير في مكان وزمان حذف المؤشر بالضبط. عندما تقوم دالة بإرجاع مؤشر ، هل تمتلكها الآن؟ هل يجب حذفه بنفسك عند الانتهاء منه ، أم أنه ينتمي إلى بعض هياكل البيانات التي سيتم تحريرها جميعًا مرة واحدة في وقت لاحق؟ أخطأ بطريقة ما وتسرب الذاكرة ، وأخطأ في الآخر وأفسدت بنية البيانات المعنية والأخرى على الأرجح ، أثناء محاولتهم الإشارة إلى مؤشرات مرجعية لم تعد صالحة الآن.
"في جامع القمامة يا فتى الذباب!"
جامعي القمامة ليسوا تقنية جديدة. تم اختراعها في عام 1959 من قبل جون مكارثي من أجل Lisp. مع Smalltalk-80 في عام 1980 ، بدأ جمع القمامة في الظهور في الاتجاه السائد. ومع ذلك ، فقد مثلت التسعينيات الازدهار الحقيقي لهذه التقنية: بين عامي 1990 و 2000 ، تم إصدار عدد كبير من اللغات ، وكلها تستخدم جمع القمامة من نوع أو آخر: Haskell ، Python ، Lua ، Java ، JavaScript ، Ruby ، OCaml ، و C # من بين أشهرها.
ما هو جمع القمامة؟ باختصار ، إنها مجموعة من التقنيات المستخدمة لأتمتة إدارة الذاكرة اليدوية. غالبًا ما تكون متاحة كمكتبة للغات ذات إدارة يدوية للذاكرة مثل C و C ++ ، لكنها أكثر شيوعًا في اللغات التي تتطلب ذلك. الميزة الكبرى هي أن المبرمج ببساطة لا يحتاج إلى التفكير في الذاكرة ؛ كل شيء مجردة. على سبيل المثال ، مكافئ Python لكود قراءة الملفات أعلاه هو ببساطة:
def read_lines_from_file(file_name): lines = [] with open(file_name) as fp: for line in fp: lines.append(line) return lines if __name__ == '__main__': import sys file_name = sys.argv[1] count = len(read_lines_from_file(file_name)) print("File {} contains {} lines.".format(file_name, count)) $ python3 python3.py makefile File makefile contains 38 lines. تظهر مصفوفة الأسطر عند تعيينها لأول مرة وإعادتها دون نسخها إلى نطاق الاستدعاء. يتم تنظيفها بواسطة جامع القمامة في وقت ما بعد خروجها من هذا النطاق ، لأن التوقيت غير محدد. ملاحظة مثيرة للاهتمام هي أنه في Python ، فإن RAII للموارد غير المتعلقة بالذاكرة ليس اصطلاحيًا. إنه مسموح - كان بإمكاننا كتابة fp = open(file_name) بدلاً من استخدام كتلة with ، والسماح لـ GC بالتنظيف بعد ذلك. لكن النمط الموصى به هو استخدام مدير السياق عندما يكون ذلك ممكنًا بحيث يمكن إصدارها في الأوقات الحتمية.

على الرغم من أنه من الجيد تجريد إدارة الذاكرة ، إلا أن هناك تكلفة. في حساب جمع البيانات المهملة المرجعي ، تكتسب جميع عمليات الخروج المتغيرة والنطاق تكلفة صغيرة لتحديث المراجع. في أنظمة التحديد والمسح ، على فترات غير متوقعة ، يتم إيقاف تنفيذ البرنامج بالكامل بينما يقوم GC بتنظيف الذاكرة. غالبًا ما يسمى هذا بحدث Stop-the-world. تطبيقات مثل Python ، التي تستخدم كلا النظامين ، تعاني من كلا العقوبتين. تقلل هذه المشكلات من ملاءمة اللغات التي تم جمع البيانات المهملة فيها للحالات التي يكون فيها الأداء أمرًا بالغ الأهمية ، أو تكون التطبيقات في الوقت الفعلي ضرورية. يمكن للمرء أن يرى عقوبة الأداء قيد التنفيذ حتى في برامج الألعاب هذه:
$ make cpp && time ./c++ makefile g++ -o c++ c++.cpp File makefile contains 38 lines. real 0m0.016s user 0m0.000s sys 0m0.015s $ time python3 python3.py makefile File makefile contains 38 lines. real 0m0.041s user 0m0.015s sys 0m0.015sيستغرق إصدار Python ما يقرب من ثلاثة أضعاف الوقت الفعلي الذي تستغرقه نسخة C ++. في حين أنه لا يمكن أن يُعزى كل هذا الاختلاف إلى جمع القمامة ، إلا أنه لا يزال كبيرًا.
الملكية: RAII Awakens
هل هذه هي النهاية إذن؟ هل يجب أن تختار جميع لغات البرمجة بين الأداء وسهولة البرمجة؟ رقم! يستمر البحث في لغة البرمجة ، وقد بدأنا في رؤية التطبيقات الأولى للجيل القادم من نماذج اللغة. تعتبر اللغة التي تسمى Rust ذات أهمية خاصة ، والتي تعد ببيئة عمل شبيهة ببايثون وسرعة تشبه C مع وضع مؤشرات متدلية ومؤشرات فارغة ومثل هذا مستحيل - لن يتم تجميعها. كيف يمكنها تقديم تلك الادعاءات؟
تسمى التقنية الأساسية التي تسمح بهذه الادعاءات المثيرة للإعجاب مدقق الاستعارة ، وهو مدقق ثابت يعمل على التجميع ، ويرفض التعليمات البرمجية التي قد تسبب هذه المشكلات. ومع ذلك ، قبل الخوض في التعمق في الآثار المترتبة ، سنحتاج إلى التحدث عن المتطلبات الأساسية.
ملكية
تذكر في مناقشتنا للمؤشرات في C ++ ، لقد تطرقنا إلى مفهوم الملكية ، والذي يعني بعبارات تقريبية "من المسؤول عن حذف هذا المتغير". الصدأ يضفي الطابع الرسمي على هذا المفهوم ويقويه. كل رابط متغير له ملكية المورد الذي يربطه ، ويضمن مدقق الاستعارة وجود ارتباط واحد له ملكية عامة للمورد. وهذا يعني أن المقتطف التالي من كتاب Rust لن يتم تجميعه:
let v = vec![1, 2, 3]; let v2 = v; println!("v[0] is: {}", v[0]); error: use of moved value: `v` println!("v[0] is: {}", v[0]); ^تنقل المهام في Rust دلالات بشكل افتراضي - فهي تنقل الملكية. من الممكن إعطاء نسخ دلالات لنوع ما ، وقد تم ذلك بالفعل للأولويات الرقمية ، لكنه غير معتاد. نتيجة لهذا ، اعتبارًا من السطر الثالث من الكود ، يمتلك v2 المتجه المعني ولا يمكن الوصول إليه بعد ذلك باسم v. لماذا يعد هذا مفيدًا؟ عندما يكون لكل مورد مالك واحد بالضبط ، فإن له أيضًا لحظة واحدة يكون فيها خارج النطاق ، والتي يمكن تحديدها في وقت الترجمة. وهذا يعني بدوره أن Rust يمكن أن يفي بوعد RAII ، حيث يقوم بتهيئة الموارد وتدميرها بشكل حاسم بناءً على نطاقها ، دون استخدام أداة تجميع القمامة أو مطالبة المبرمج بتحرير أي شيء يدويًا.
قارن هذا بمرجع عد جمع القمامة. في تطبيق RC ، تحتوي جميع المؤشرات على جزأين على الأقل من المعلومات: الكائن المشار إليه وعدد المراجع لهذا الكائن. يتم إتلاف الكائن عندما يصل هذا العدد إلى 0. يضاعف هذا متطلبات الذاكرة للمؤشر ويضيف تكلفة صغيرة لاستخدامه ، حيث يتم زيادة العدد وتناقصه وفحصه تلقائيًا. يوفر نظام ملكية Rust نفس الضمان ، وهو أن الكائنات يتم تدميرها تلقائيًا عند نفاد المراجع ، ولكنها تفعل ذلك دون أي تكلفة لوقت التشغيل. يتم تحليل ملكية كل عنصر وإدخال استدعاءات التدمير في وقت الترجمة.
الاقتراض
إذا كانت دلالات النقل هي الطريقة الوحيدة لتمرير البيانات ، فستصبح أنواع إرجاع الوظائف معقدة للغاية وسريعة جدًا. إذا أردت كتابة دالة تستخدم متجهين لإنتاج عدد صحيح ، والذي لم يدمر المتجهات بعد ذلك ، فسيتعين عليك تضمينها في القيمة المعادة. في حين أن هذا ممكن تقنيًا ، إلا أنه من السيء استخدام:
fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) { // do stuff with v1 and v2 // hand back ownership, and the result of our function (v1, v2, 42) } let v1 = vec![1, 2, 3]; let v2 = vec![1, 2, 3]; let (v1, v2, answer) = foo(v1, v2);بدلاً من ذلك ، لدى Rust مفهوم الاقتراض. يمكنك كتابة نفس الوظيفة على هذا النحو ، وسوف تستعير الإشارة إلى المتجهات ، وتعيدها إلى المالك عندما تنتهي الوظيفة:
fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 { // do stuff 42 } let v1 = vec![1, 2, 3]; let v2 = vec![1, 2, 3]; let answer = foo(&v1, &v2);يعيد الإصداران 1 و v2 ملكيتهما إلى النطاق الأصلي بعد عودة fn foo ، ويخرجان عن النطاق ويتم تدميرهما تلقائيًا عند خروج النطاق المحتوي.
ومن الجدير بالذكر هنا أن هناك قيودًا على الاقتراض ، يفرضها مدقق الاستعارة في وقت التجميع ، والتي يضعها كتاب Rust Book بإيجاز شديد:
يجب أن يستمر أي اقتراض في نطاق لا يزيد عن نطاق المالك. ثانيًا ، قد يكون لديك نوع أو آخر من هذين النوعين من الاقتراضات ، ولكن ليس كلاهما في نفس الوقت:
مرجع واحد أو أكثر (& T) لمورد
مرجع واحد قابل للتغيير بالضبط (& mut T)
هذا جدير بالملاحظة لأنه يشكل جانبًا هامًا من جوانب حماية Rust ضد سباقات البيانات. من خلال منع الوصول المتعدد القابل للتغيير إلى مورد معين في وقت الترجمة ، فإنه يضمن أنه لا يمكن كتابة التعليمات البرمجية حيث تكون النتيجة غير محددة لأنها تعتمد على أي مؤشر ترابط وصل إلى المورد أولاً. هذا يمنع مشكلات مثل إبطال المكرر والاستخدام بعد مجانًا.
مدقق الاقتراض من الناحية العملية
الآن بعد أن عرفنا بعض ميزات Rust ، دعنا نلقي نظرة على كيفية قيامنا بتنفيذ نفس عداد خط الملفات الذي رأيناه من قبل:
fn read_lines_from_file(file_name: &str) -> io::Result<Vec<String>> { // variables in Rust are immutable by default. The mut keyword allows them to be mutated. let mut lines = Vec::new(); let mut buffer = String::new(); if let Ok(mut fp) = OpenOptions::new().read(true).open(file_name) { // We enter this block only if the file was successfully opened. // This is one way to unwrap the Result<T, E> type Rust uses instead of exceptions. // fp.read_to_string can return an Err. The try! macro passes such errors // upwards through the call stack, or continues otherwise. try!(fp.read_to_string(&mut buffer)); lines = buffer.split("\n").map(|s| s.to_string()).collect(); } Ok(lines) } fn main() { // Get file name from the first argument. // Note that args().nth() produces an Option<T>. To get at the actual argument, we use // the .expect() function, which panics with the given message if nth() returned None, // indicating that there weren't at least that many arguments. Contrast with C++, which // segfaults when there aren't enough arguments, or Python, which raises an IndexError. // In Rust, error cases *must* be accounted for. let file_name = env::args().nth(1).expect("This program requires at least one argument!"); if let Ok(file_lines) = read_lines_from_file(&file_name) { println!("File {} contains {} lines.", file_name, file_lines.len()); } else { // read_lines_from_file returned an error println!("Could not read file {}", file_name); } } بالإضافة إلى العناصر التي تم التعليق عليها بالفعل في الكود المصدري ، فإن الأمر يستحق المتابعة وتتبع أعمار المتغيرات المختلفة. file_name و file_lines حتى نهاية main () ؛ يتم استدعاء مدمراتها في ذلك الوقت دون تكلفة إضافية ، باستخدام نفس آلية المتغيرات التلقائية لـ C ++. عند استدعاء read_lines_from_file ، يتم إقراض اسم file_name بشكل ثابت لتلك الوظيفة طوال مدتها. داخل read_lines_from_file ، يعمل buffer بنفس الطريقة ، ويتم إتلافه عندما يقع خارج النطاق. من ناحية أخرى ، تستمر lines ويتم إرجاعها بنجاح إلى main . لماذا ا؟
أول شيء يجب ملاحظته هو أنه نظرًا لأن Rust هي لغة قائمة على التعبيرات ، فقد لا يبدو استدعاء الإرجاع مثل واحد في البداية. إذا أغفل السطر الأخير للدالة الفاصلة المنقوطة اللاحقة ، فسيكون هذا التعبير هو القيمة المرجعة. الشيء الثاني هو أن القيم المرتجعة تحصل على معالجة خاصة. من المفترض أنهم يريدون أن يعيشوا على الأقل مثل متصل الوظيفة. الملاحظة الأخيرة هي أنه بسبب دلالات النقل المتضمنة ، لا توجد نسخة ضرورية لتحويل Ok(lines) إلى Ok(file_lines) ، يقوم المترجم ببساطة بعمل النقطة المتغيرة في الجزء المناسب من الذاكرة.
"فقط في النهاية تدرك القوة الحقيقية لـ RAII."
تعد الإدارة اليدوية للذاكرة كابوسًا ابتكر المبرمجون طرقًا لتجنبه منذ اختراع المترجم. كان RAII نمطًا واعدًا ، لكنه معطل في C ++ لأنه ببساطة لم يعمل مع الكائنات المخصصة للكدمات دون بعض الحلول الفردية. ونتيجة لذلك ، كان هناك انفجار في لغات جمع القمامة في التسعينيات ، والتي صممت لجعل الحياة أكثر متعة للمبرمج حتى على حساب الأداء.
ومع ذلك ، هذه ليست الكلمة الأخيرة في تصميم اللغة. باستخدام مفاهيم جديدة وقوية للملكية والاقتراض ، تمكن Rust من دمج أسس النطاق لأنماط RAII مع أمان الذاكرة الخاص بجمع البيانات المهملة ؛ كل ذلك دون الحاجة إلى مطالب جامع القمامة بإيقاف العالم ، مع تقديم ضمانات أمان لم يتم رؤيتها بأي لغة أخرى. هذا هو مستقبل برمجة النظم. بعد كل شيء ، "الخطأ هو إنسان ، لكن المترجمين لا ينسون أبدًا."
مزيد من القراءة على مدونة Toptal Engineering:
- WebAssembly / البرنامج التعليمي حول الصدأ: معالجة صوت مثالية
- تسرب ذاكرة الصيد في جافا
