المُنهي

في علوم الحاسوب ، تُعرف الدالة النهائية ( finalizer ) بأنها دالة خاصة تُنفذ عملية إنهاء الكائن ، وعادةً ما تكون نوعًا من أنواع التنظيف. تُنفذ الدالة النهائية أثناء تدمير الكائن ، قبل تحرير الذاكرة المخصصة له ، وهي مكملة للدالة الأولية (initializer ) التي تُنفذ أثناء إنشاء الكائن ، بعد تخصيص الذاكرة . يُنصح البعض بشدة بعدم استخدام الدوال النهائية نظرًا لصعوبة استخدامها الأمثل والتعقيد الذي تُضيفه، ويُقترح بدلاً منها بدائل، أبرزها نمط التخلص (dispose ) [ 1 ] (انظر: مشاكل الدوال النهائية ).

يُستخدم مصطلح "المُنهي" غالبًا مع لغات البرمجة التي تستخدم جمع البيانات المهملة ، مثل لغات البرمجة كائنية التوجه (كمثال Smalltalk ) واللغات الوظيفية (كمثال ML) . ويُقابل ذلك " المُدمِّر" ، وهو دالة تُستدعى لإنهاء الكائن في اللغات ذات دورات حياة الكائنات المحددة (كمثال C++) . [ 2 ] [ 3 ] وهما مصطلحان حصريان عمومًا: فإما أن تحتوي اللغة على مُنهيات (إذا كانت عملية جمع البيانات المهملة تتم تلقائيًا) أو مُدمِّرات (إذا كانت إدارة الذاكرة تتم يدويًا)، ولكن في حالات نادرة، قد تحتوي اللغة على كليهما، كما هو الحال في C++/CLI و D ، وفي حالة عدّ المراجع (بدلاً من تتبع عملية جمع البيانات المهملة)، يختلف المصطلح. في الاستخدام التقني، قد يُستخدم مصطلح "المُنهي" أيضًا للإشارة إلى المُدمِّرات، لأنها تُنفِّذ عملية الإنهاء أيضًا، وتُجرى بعض الفروق الدقيقة بينهما - انظر المصطلحات . كما يُشير مصطلح "نهائي" إلى فئة لا يُمكن توريثها ؛ وهذا أمر غير ذي صلة.

مصطلحات

تختلف المصطلحات المستخدمة في مصطلحي "المنهي" و "الإنهاء" مقابل "المدمر" و "التدمير" بين المؤلفين، وتكون غير واضحة في بعض الأحيان.

في الاستخدام الشائع، يكون المدمر عبارة عن طريقة يتم استدعاؤها بشكل حتمي عند تدمير الكائن، والنموذج الأصلي هو مدمرات C++؛ بينما يتم استدعاء المُنهي بشكل غير حتمي بواسطة جامع القمامة، والنموذج الأصلي هو طرق Java .finalize

تختلف المصطلحات المستخدمة في لغات البرمجة التي تُطبّق آلية جمع البيانات المهملة عبر عدّ المراجع ، فبعض اللغات مثل Objective-C و Perl تستخدم المُدمِّر ، بينما تستخدم لغات أخرى مثل Python المُنهي (وفقًا للمواصفات، تُجمع البيانات المهملة في Python، لكن تطبيق CPython المرجعي منذ الإصدار 2.0 يستخدم مزيجًا من عدّ المراجع وجمع البيانات المهملة). ويعكس هذا أن عدّ المراجع يُنتج دورة حياة شبه حتمية للكائنات: فالكائنات التي لا تُشكّل جزءًا من دورة، تُدمَّر بشكل حتمي عندما ينخفض ​​عدد المراجع إلى الصفر، أما الكائنات التي تُشكّل جزءًا من دورة، فتُدمَّر بشكل غير حتمي، كجزء من آلية منفصلة لجمع البيانات المهملة.

في بعض الاستخدامات التقنية الضيقة، يُعتبر كل من المُنشئ والمُدمِّر مصطلحين على مستوى اللغة، ويعنيان الدوال المُعرَّفة في فئة ، بينما يُعتبر كل من المُهيئ والمُنهي مصطلحين على مستوى التنفيذ، ويعنيان الدوال المُستدعاة أثناء إنشاء الكائن أو تدميره . فعلى سبيل المثال، أشارت المواصفات الأصلية للغة C# إلى "المُدمِّرات"، على الرغم من أن C# تُدار بواسطة جمع البيانات المهملة، بينما أشارت مواصفات البنية التحتية للغة المشتركة (CLI)، وتنفيذ بيئة التشغيل الخاصة بها باسم وقت تشغيل اللغة المشتركة (CLR)، إلى "المُنهيات". وينعكس هذا في ملاحظات لجنة لغة C#، التي جاء فيها جزئيًا: "يُترجم مُصرِّف C# المُدمِّرات إلى ... [على الأرجح] مُنهيات مثيل". [ 4 ] [ 5 ] هذا المصطلح مُربك، ولذلك تُشير الإصدارات الأحدث من مواصفات C# إلى الدالة على مستوى اللغة باسم "المُنهيات". [ 6 ]

لغة أخرى لا تميز بين المصطلحات هي لغة D. على الرغم من أن فئات D تخضع لعملية جمع البيانات المهملة، إلا أن وظائف التنظيف الخاصة بها تسمى دوال التدمير. [ 7 ]

يستخدم

تُستخدم عملية الإنهاء في الغالب للتنظيف، لتحرير الذاكرة أو الموارد الأخرى: لإلغاء تخصيص الذاكرة المُخصصة عبر إدارة الذاكرة اليدوية ؛ لمسح المراجع في حال استخدام عدّ المراجع (إنقاص عدد المراجع)؛ لتحرير الموارد، خاصةً في نمط RAII ( اكتساب الموارد هو التهيئة )؛ أو لإلغاء تسجيل كائن. يختلف مقدار الإنهاء اختلافًا كبيرًا بين اللغات، من الإنهاء المكثف في لغة C++، التي تعتمد على إدارة الذاكرة اليدوية، وعدّ المراجع، ودورات حياة الكائنات المحددة؛ إلى عدم وجود إنهاء في كثير من الأحيان في لغة Java، التي تتميز بدورات حياة الكائنات غير المحددة، وغالبًا ما تُنفذ باستخدام جامع قمامة تتبعي. من الممكن أيضًا أن يكون هناك إنهاء صريح (يحدده المستخدم) قليل أو معدوم، ولكن يوجد إنهاء ضمني كبير، يُنفذه المُترجم أو المُفسر أو وقت التشغيل؛ وهذا شائع في حالة عدّ المراجع التلقائي، كما هو الحال في تطبيق CPython المرجعي للغة Python، أو في عدّ المراجع التلقائي في تطبيق Apple للغة Objective-C ، حيث يقوم كلاهما بكسر المراجع تلقائيًا أثناء الإنهاء. يمكن أن يتضمن المُنهي أي كود برمجي. ومن الاستخدامات المعقدة بشكل خاص إعادة الكائن تلقائيًا إلى مجموعة الكائنات .

يُعدّ تحرير الذاكرة أثناء الإنهاء أمرًا شائعًا في لغات مثل C++ حيث تُعتبر إدارة الذاكرة اليدوية معيارًا أساسيًا، ولكنه يحدث أيضًا في اللغات المُدارة عندما يتم تخصيص الذاكرة خارج الكومة المُدارة (خارج نطاق اللغة)؛ في Java، يحدث هذا مع واجهة Java الأصلية (JNI) ByteBufferوالكائنات في عمليات الإدخال/الإخراج الجديدة (NIO). قد يُسبب هذا الأخير مشاكل نظرًا لعدم قدرة جامع البيانات المهملة على تتبع هذه الموارد الخارجية، وبالتالي لن يتم جمعها بالقدر الكافي، وقد يُؤدي إلى أخطاء نفاد الذاكرة نتيجة استنفاد الذاكرة غير المُدارة - يُمكن تجنب ذلك من خلال التعامل مع الذاكرة الأصلية كمورد واستخدام نمط التخلص ، كما هو موضح أدناه.

تُعدّ دوال الإنهاء (Finalizers) أقل ضرورةً واستخدامًا من دوال التدمير (Destructors). فهي أقل ضرورةً لأن عملية جمع البيانات المهملة تُؤتمت إدارة الذاكرة ، وأقل استخدامًا لأنها لا تُنفّذ عادةً بشكلٍ حتمي - فقد لا يتم استدعاؤها في الوقت المناسب، أو حتى على الإطلاق، ولا يُمكن التنبؤ ببيئة التنفيذ - وبالتالي، يجب إجراء أي عملية تنظيف بطريقة حتمية باستخدام طريقة أخرى، غالبًا يدويًا عبر نمط التخلص (dispose ). والجدير بالذكر أن كلاً من لغتي جافا وبايثون لا تضمنان استدعاء دوال الإنهاء، وبالتالي لا يُمكن الاعتماد عليها في عملية التنظيف.

نظراً لقلة تحكم المبرمج في تنفيذها، يُنصح عادةً بتجنب استخدام دوال الإنهاء (Finalizers) إلا في أبسط العمليات. وعلى وجه الخصوص، فإن العمليات التي تُنفذ عادةً في دوال التدمير (Destructors) لا تُناسب دوال الإنهاء. ومن الأنماط البرمجية السيئة الشائعة كتابة دوال الإنهاء كما لو كانت دوال تدمير، وهو أمر غير ضروري وغير فعال، نظراً للاختلافات بينهما. وهذا شائعٌ بشكل خاص بين مبرمجي لغة C++ ، حيث تُستخدم دوال التدمير بكثرة في لغة C++، وفقاً لأسلوب " استحواذ الموارد هو التهيئة" (RAII).

بناء الجملة

تشمل لغات البرمجة التي تستخدم المُنهيات C++/CLI و C# و Clean و Go و Java و JavaScript و Python . ويختلف بناء الجملة اختلافًا كبيرًا باختلاف اللغة.

في لغة جافا، الدالة النهائية هي دالة تسمى finalize، والتي تقوم بتجاوز Object.finalizeالدالة . [ 8 ]

في لغة جافا سكريبت، يسمح FinalizationRegistry بطلب رد نداء عند جمع كائن بواسطة جامع البيانات المهملة.

في لغة بايثون، الدالة النهائية هي دالة تسمى __del__.

في لغة بيرل، الدالة النهائية هي طريقة تسمى DESTROY.

فئة UML في لغة C# تحتوي على مُنشئ ومُنهي.

في لغة C#، الدالة النهائية (التي كانت تُسمى "المدمر" في الإصدارات السابقة من المعيار) هي دالة يكون اسمها اسم الفئة ~مسبوقًا بـ `<اسم الفئة>`، كما في المثال التالي: `< اسم الفئة~Foo >`. وهذا هو نفس بناء جملة المُدمر في لغة C++ . وكانت هذه الدوال تُسمى في الأصل "المدمرات" قياسًا على لغة C++، على الرغم من اختلاف سلوكها، ولكن أُعيد تسميتها إلى "الدوال النهائية" بسبب الالتباس الذي سببه هذا الاسم. [ 6 ]

في لغة C++/CLI، التي تحتوي على كل من المدمرات والمنهيات، فإن المدمر هو طريقة يكون اسمها اسم الفئة ~مسبوقًا بـ، كما في ~Foo(كما في C#)، والمنهي هو طريقة يكون اسمها اسم الفئة !مسبوقًا بـ، كما في !Foo.

في لغة Go، يتم تطبيق المُنهيات على مؤشر واحد عن طريق استدعاء runtime.SetFinalizerالدالة الموجودة في المكتبة القياسية. [ 9 ]

تطبيق

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

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

إذا أُعيد إحياء كائن، يبرز تساؤل آخر حول ما إذا كان سيتم استدعاء مُنهيه مرة أخرى عند تدميره لاحقًا - على عكس المُدمِّرات، قد يتم استدعاء مُنهيات الكائنات عدة مرات. إذا تم استدعاء مُنهيات الكائنات المُعاد إحياؤها، فقد تُعيد الكائنات إحياء نفسها بشكل متكرر وتصبح غير قابلة للتدمير؛ يحدث هذا في تطبيق CPython للغة بايثون قبل الإصدار 3.4، وفي لغات CLR مثل C#. لتجنب ذلك، في العديد من اللغات، بما في ذلك جافا، وأوبجكتيف-سي (على الأقل في تطبيقات أبل الحديثة)، وبايثون بدءًا من الإصدار 3.4، يتم إنهاء الكائنات مرة واحدة على الأكثر، مما يتطلب تتبع ما إذا كان الكائن قد تم إنهاؤه بالفعل.

في حالات أخرى، ولا سيما لغات CLR مثل C#، يتم تتبع الإنهاء بشكل منفصل عن الكائنات نفسها، ويمكن تسجيل الكائنات أو إلغاء تسجيلها بشكل متكرر من أجل الإنهاء.

مشاكل

بحسب طريقة التنفيذ، قد تتسبب الدوال النهائية في عدد كبير من المشاكل، ولذلك تحذر منها العديد من الجهات المختصة بشدة. [ 10 ] [ 11 ] وتشمل هذه المشاكل ما يلي: [ 10 ]

  • قد لا يتم استدعاء الجهات النهائية في الوقت المناسب، أو على الإطلاق، لذلك لا يمكن الاعتماد عليها في الحفاظ على الحالة، أو إطلاق الموارد الشحيحة، أو القيام بأي شيء آخر ذي أهمية.
  • قد تؤدي عمليات الإنهاء إلى إعادة إنشاء الكائن ، وهو أمر غالباً ما يكون خطأ برمجياً، واحتمالية حدوثه بحد ذاتها تبطئ وتعقد عملية جمع البيانات المهملة بشكل كبير.
  • يتم تشغيل عمليات الإنهاء بناءً على جمع البيانات المهملة، والذي يعتمد بشكل عام على إدارة ضغط الذاكرة - لا يتم تشغيلها في حالة ندرة الموارد الأخرى، وبالتالي فهي غير مناسبة لإدارة الموارد النادرة الأخرى.
  • لا يتم تشغيل عمليات الإنهاء بترتيب محدد، ولا يمكنها الاعتماد على ثوابت الفئة (لأنها قد تشير إلى كائنات أخرى تم إنهاؤها بالفعل).
  • قد تؤدي عمليات الإنهاء البطيئة إلى تأخير عمليات الإنهاء الأخرى.
  • لا يمكن التعامل مع الاستثناءات داخل عمليات الإنهاء بشكل عام، لأن عملية الإنهاء يتم تشغيلها في بيئة غير محددة، وقد يتم تجاهلها أو تسبب إنهاء البرنامج بشكل غير متحكم فيه.
  • قد تقوم عمليات الإنهاء بالإشارة إلى الكائنات الحية وإنهاءها عن طريق الخطأ، مما ينتهك ثوابت البرنامج.
  • قد تتسبب عمليات الإنهاء في مشاكل التزامن، حتى في البرامج المتسلسلة (أحادية الخيوط)، عندما يتم الإنهاء في خيوط منفصلة. [ 12 ]
  • قد تتسبب عمليات الإنهاء في حدوث حالات جمود إذا تم استخدام آليات التزامن مثل الأقفال، وذلك بسبب عدم تشغيلها بترتيب محدد وربما تشغيلها بشكل متزامن.
  • لا يمكن للمُنهيات التي يتم تشغيلها أثناء إنهاء البرنامج الاعتماد على بيئة وقت التشغيل المعتادة، وبالتالي قد تفشل بسبب الافتراضات غير الصحيحة - ولهذا السبب غالبًا لا يتم تشغيل المُنهيات أثناء الإنهاء.

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

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

إدارة الموارد

من الممارسات الخاطئة الشائعة استخدام دوال الإنهاء لتحرير الموارد، قياسًا على أسلوب "اكتساب الموارد هو التهيئة " (RAII) في لغة C++: حيث يتم اكتساب المورد في دالة التهيئة (الدالة البانية)، ثم تحريره في دالة الإنهاء (الدالة الهدامة). هذا الأسلوب غير فعال لعدة أسباب. أولها، أن دوال الإنهاء قد لا تُستدعى أبدًا، وحتى عند استدعائها، قد لا يتم ذلك في الوقت المناسب، مما يؤدي عادةً إلى تسرب الموارد . ثانيًا، لا تُستدعى دوال الإنهاء بترتيب محدد، بينما غالبًا ما يتطلب تحرير الموارد ترتيبًا معينًا، غالبًا ما يكون عكس ترتيب اكتسابها. ثالثًا، بما أن دوال الإنهاء تُستدعى حسب تقدير جامع البيانات المهملة، فغالبًا ما تُستدعى فقط عند انخفاض ضغط الذاكرة المُدارة (عندما تكون الذاكرة المُدارة المتاحة قليلة)، بغض النظر عن ضغط الموارد. فإذا كانت الموارد الشحيحة محجوزة بواسطة جامع البيانات المهملة، ولكن الذاكرة المُدارة المتاحة وفيرة، فقد لا يتم جمع البيانات المهملة، وبالتالي لن يتم استعادة هذه الموارد.

لذا، بدلاً من استخدام دوال الإنهاء لإدارة الموارد تلقائيًا، في اللغات التي تعتمد على جمع البيانات المهملة، يجب إدارة الموارد يدويًا، عادةً باستخدام نمط التخلص (dispose) . في هذه الحالة، قد يتم الحصول على الموارد في دالة التهيئة (initializer)، التي تُستدعى صراحةً عند إنشاء الكائن، ولكن يتم تحريرها في دالة التخلص (dispose). يمكن استدعاء دالة التخلص (dispose) صراحةً، أو ضمنيًا بواسطة بنيات لغوية مثل C# using، أو Java try(مع الموارد)، أو Python with.

مع ذلك، في بعض الحالات، يُستخدم كل من نمط التخلص (dispose) والمُنهيات (finalizers) لتحرير الموارد. ويُلاحظ هذا غالبًا في لغات CLR مثل C#، حيث تُستخدم الإنهاءات كآلية احتياطية للتخلص: فعند الحصول على مورد، يُوضع الكائن المُستَحصِل في قائمة انتظار الإنهاءات، بحيث يُحرَّر المورد عند تدمير الكائن، حتى لو لم يُحرَّر المورد يدويًا.

أعمار الأجسام الحتمية وغير الحتمية

في اللغات ذات دورات حياة الكائنات المحددة، ولا سيما لغة C++، تتم إدارة الموارد عادةً بربط دورة حياة امتلاك الموارد بدورة حياة الكائن، حيث يتم الحصول على الموارد أثناء التهيئة وتحريرها أثناء الإنهاء؛ وهذا ما يُعرف باسم " الحصول على الموارد هو التهيئة " (RAII). يضمن هذا أن يكون امتلاك الموارد ثابتًا في الفئة ، وأن يتم تحرير الموارد فورًا عند تدمير الكائن.

مع ذلك، في اللغات ذات دورات حياة الكائنات غير الحتمية - والتي تشمل جميع اللغات الرئيسية التي تستخدم جمع البيانات المهملة، مثل C# وJava وPython - لا يُجدي هذا الأسلوب نفعًا، لأن عملية الإنهاء قد لا تتم في الوقت المناسب أو قد لا تتم على الإطلاق، وبالتالي قد لا تُحرر الموارد لفترة طويلة أو حتى لا تُحرر أبدًا، مما يُسبب تسربًا للموارد . في هذه اللغات، تُدار الموارد عادةً يدويًا عبر نمط التخلص (dispose) : قد يتم الحصول على الموارد أثناء التهيئة، ولكن يتم تحريرها باستدعاء disposeدالة. ومع ذلك، يُعد استخدام الإنهاء لتحرير الموارد في هذه اللغات نمطًا سيئًا شائعًا ، وسيؤدي نسيان استدعاء الدالة disposeإلى تسرب الموارد.

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

إحياء الكائن

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

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

يُستخدم نهج مختلف في إطار عمل .NET ، وخاصةً في لغتي C# و Visual Basic (.NET) ، حيث تتم متابعة عملية الإنهاء بواسطة "قائمة انتظار" بدلاً من الكائن نفسه. في هذه الحالة، إذا تم تحديد مُنهي من قِبل المستخدم، فسيتم إنهاء الكائن مرة واحدة فقط افتراضيًا (يتم وضعه في قائمة الانتظار عند الإنشاء، ثم يُزال منها بعد الإنهاء)، ولكن يمكن تغيير ذلك عن طريق استدعاء GCالوحدة النمطية. يمكن منع الإنهاء باستدعاء GC.SuppressFinalizeالدالة التي تُزيل الكائن من قائمة الانتظار، أو إعادة تنشيطه باستدعاء GC.ReRegisterForFinalizeالدالة التي تُضيفه إليها. تُستخدم هذه الأساليب بشكل خاص عند استخدام الإنهاء لإدارة الموارد كمكمل لنمط التخلص، أو عند تنفيذ مجمع كائنات .

قارن ذلك بالتهيئة

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

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

تُهيأ المتغيرات عادةً في بداية دورة حياتها، ولكن لا تُنهي في نهايتها - مع ذلك، إذا كان للمتغير كائن كقيمة، فقد يُنهي الكائن . في بعض الحالات، تُنهي المتغيرات أيضًا: تسمح امتدادات GCC بإنهاء المتغيرات.

التواصل معfinally

كما يتضح من التسمية، فإنّ "الإنهاء" والبنية finallyيؤديان غرضين متشابهين: تنفيذ إجراء نهائي، عادةً ما يكون تنظيفًا، بعد انتهاء عملية أخرى. ويكمن الاختلاف بينهما في توقيت حدوثهما؛ إذ finallyتُنفَّذ العبارة عندما يغادر تنفيذ البرنامج جسم tryالعبارة المرتبطة بها، ويحدث هذا أثناء فكّ المكدس، وبالتالي يوجد مكدس من finallyالعبارات المعلقة، مرتبةً حسب الترتيب. أما الإنهاء فيحدث عند تدمير كائن، وهو ما يعتمد على طريقة إدارة الذاكرة، وعمومًا توجد مجموعة من الكائنات تنتظر الإنهاء، غالبًا في الكومة، ولا يشترط أن يتم ذلك بترتيب محدد.

مع ذلك، تتطابق هذه الحالات في بعض الأحيان. في لغة C++، يكون تدمير الكائن حتميًا، finallyويمكن محاكاة سلوك جملة `request` من خلال متغير محلي يحمل كائنًا كقيمة، ويكون نطاقه كتلة `request` تُطابق جسم جملة ` tryrequest`. يتم إنهاء الكائن (تدميره) عند انتهاء التنفيذ من هذا النطاق، تمامًا كما لو كانت هناك finallyجملة `request`. لهذا السبب، لا تحتوي C++ على finallyبنية `request`، والفرق هو أن الإنهاء مُعرَّف في تعريف الصنف كدالة تدمير، وليس عند موقع الاستدعاء في finallyجملة `request`.

على النقيض، في حالة finallyعبارة في روتين فرعي ، كما هو الحال في مولد بايثون، قد لا ينتهي الروتين الفرعي أبدًا - بل يتوقف فقط - وبالتالي finallyلا تُنفذ العبارة في التنفيذ العادي. إذا فُسِّرت حالات الروتين الفرعي ككائنات، finallyفيمكن اعتبار العبارة بمثابة مُنهي للكائن، وبالتالي يمكن تنفيذها عند جمع النسخة بواسطة جامع البيانات المهملة. في مصطلحات بايثون، يُعرَّف الروتين الفرعي بأنه دالة مولدة ، بينما تُسمى نسخته مُكرِّرًا مولدًا، وبالتالي finallyتصبح العبارة في الدالة المولدة مُنهيًا في المُكرِّرات المولدة المُنشأة من هذه الدالة.

تاريخ

يعود مفهوم الإنهاء كخطوة منفصلة في تدمير الكائنات إلى مونتغمري (1994) ، [ 13 ] قياسًا على التمييز السابق بين التهيئة في بناء الكائنات في مارتن وأوديل (1992) . [ 14 ] استخدمت المراجع السابقة مصطلح "التدمير" لهذه العملية، دون التمييز بين الإنهاء وإلغاء التخصيص، وتستخدم لغات البرمجة التي تعود إلى تلك الفترة، مثل C++ وPerl، مصطلح "التدمير". كما استُخدم مصطلحا "الإنهاء" و"الإنهاء النهائي" في كتاب أنماط التصميم (1994) المؤثر. [ أ ] [ 15 ] احتوى إصدار جافا عام 1995 على finalizeدوال ساهمت في انتشار المصطلح وربطه بجمع البيانات المهملة، ومنذ ذلك الحين، تُجري اللغات عمومًا هذا التمييز وتستخدم مصطلح "الإنهاء النهائي"، لا سيما في سياق جمع البيانات المهملة.

ملحوظات

  1. نُشر عام 1994، مع حقوق نشر تعود لعام 1995.

مراجع

  1. جاغر، بيري، وسيستوفت 2007 ، ص 542 ، "في لغة C++، يتم استدعاء المُدمِّر بطريقة مُحدَّدة، بينما في لغة C# ، لا يتم استدعاء المُنهي بطريقة مُحدَّدة. للحصول على سلوك مُحدَّد من لغة C#، ينبغي استخدام Dispose.
  2. بوهم، هانز-ج. (2002). المُدمِّرات، والمُنهيات، والتزامن . ندوة حول مبادئ لغات البرمجة (POPL). مؤرشف من الأصل بتاريخ 8 يوليو 2015. تم الاطلاع عليه بتاريخ 7 يوليو 2015 .
  3. جاغر، بيري، وسيستوفت 2007 ، ص 542 ، دوال التدمير في لغة C++ مقابل دوال الإنهاء في لغة C#: دوال التدمير في لغة C++ محددة، بمعنى أنها تُنفذ في نقاط زمنية معروفة، وبترتيب معروف، ومن خيط معالجة معروف. ولذلك فهي تختلف دلاليًا اختلافًا كبيرًا عن دوال الإنهاء في لغة C#، التي تُنفذ في نقاط زمنية غير معروفة، وبترتيب غير معروف، ومن خيط معالجة غير معروف، ووفقًا لتقدير جامع البيانات المهملة. 
  4. النص الكامل: "سنستخدم مصطلح "المدمر" للإشارة إلى العضو الذي يُنفذ عند استعادة نسخة من الكائن. يمكن أن تحتوي الفئات على مدمرات، بينما لا يمكن للهياكل ذلك. على عكس لغة C++، لا يمكن استدعاء المدمر بشكل صريح. التدمير عملية غير حتمية - لا يمكنك معرفة متى سيتم تنفيذ المدمر بشكل موثوق، إلا إذا قلت إنه يُنفذ في مرحلة ما بعد تحرير جميع المراجع إلى الكائن. يتم استدعاء المدمرات في سلسلة الوراثة بالترتيب، من الأكثر انحدارًا إلى الأقل انحدارًا. لا حاجة (ولا توجد طريقة) للفئة المشتقة لاستدعاء المدمر الأساسي بشكل صريح. يقوم مُترجم C# بتجميع المدمرات إلى تمثيل CLR المناسب. بالنسبة لهذا الإصدار، ربما يعني ذلك مُنهيًا للنسخة يتم تمييزه في البيانات الوصفية. قد يوفر CLR مُنهيات ثابتة في المستقبل؛ لا نرى أي عائق أمام استخدام C# للمنهيات الثابتة."، 12 مايو 1999.
  5. ما الفرق بين المُدمِّر والمُنهي؟، إريك ليبرت، مدونة إريك ليبرت: مغامرات رائعة في البرمجة، ٢١ يناير ٢٠١٠
  6. 1 2 جاغر، بيري، وسيستوفت 2007 ، ص 542 ، "في الإصدار السابق من هذا المعيار، كان ما يُشار إليه الآن باسم "المُنهي" يُسمى "المُدمِّر". وقد أظهرت التجربة أن مصطلح "المُدمِّر" تسبب في حدوث ارتباك، وغالبًا ما أدى إلى توقعات خاطئة، خاصةً للمبرمجين الذين يعرفون لغة C++. في لغة C++، يتم استدعاء المُدمِّر بطريقة مُحدَّدة، بينما في لغة C#، لا يتم استدعاء المُنهي بطريقة مُحدَّدة. للحصول على سلوك مُحدَّد من لغة C#، يجب استخدام " Dispose.
  7. مدمرات الفئة مدمرات الفئة في د
  8. java.lang، كائن الفئة: الإنهاء
  9. "حزمة وقت التشغيل - وقت التشغيل - PKG.go.dev" .
  10. 1 2 3 " MET12-J. لا تستخدم المُنهيات "، دروف موهيندرا، معيار CERT Oracle للبرمجة الآمنة للغة جافا ، 05. الأساليب (MET). مؤرشف بتاريخ 2014-05-04 في Wayback Machine
  11. object.__del__(self) ، مرجع لغة بايثون ، 3. نموذج البيانات : "...__del__()يجب أن تقوم الطرق بالحد الأدنى المطلق اللازم للحفاظ على الثوابت الخارجية."
  12. هانز ج. بوهم، الإنهاء، الخيوط، ونموذج الذاكرة القائم على تقنية جافا™، مؤتمر جافا وان، 2005.
  13. مونتغمري 1994 ، ص 120 ، "كما هو الحال مع إنشاء الكائنات، يمكن أن يستفيد تصميم إنهاء الكائنات من تنفيذ عمليتين لكل فئة - عملية إنهاء وعملية إنهاء . تعمل عملية الإنهاء على قطع الارتباطات مع الكائنات الأخرى، مما يضمن سلامة بنية البيانات." 
  14. مونتغمري 1994 ، ص 119 ، "ضع في اعتبارك تنفيذ إنشاء مثيلات الفئة كعملية إنشاء وتهيئة، كما اقترح مارتن وأوديل. الأولى تخصص مساحة تخزين للكائنات الجديدة، والثانية تبني الكائن ليلتزم بالمواصفات والقيود." 
  15. "لكل فئة جديدة تكلفة تنفيذ ثابتة (التهيئة، الإنهاء، إلخ)."، " المدمر في لغة C++، عملية يتم استدعاؤها تلقائيًا لإنهاء كائن على وشك الحذف."

للمزيد من القراءة