نمط التخلص

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

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

تحفيز

تغليف الموارد في كائنات

إن تغليف الموارد في كائنات هو الشكل الموجه للكائنات للتغليف ، وهو أساس نمط التخلص.

تُمثَّل الموارد عادةً بواسطة مُعرِّفات (مراجع مجردة)، وهي في الغالب أعداد صحيحة، تُستخدم للتواصل مع نظام خارجي يُوفِّر المورد. على سبيل المثال، تُوفِّر أنظمة التشغيل (وتحديدًا نظام الملفات ) الملفات، والتي تُمثِّل في العديد من الأنظمة الملفات المفتوحة بواسطة مُعرِّف ملف (عدد صحيح يُمثِّل الملف).

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

على سبيل المثال، في لغة C، تُمثَّل الملفات بكائنات من FILEنوع (يُطلق عليها خطأً " مقابض الملفات ": وهي تجريد على مستوى اللغة)، حيث تُخزِّن هذه الكائنات مقبضًا (نظام تشغيل) للملف (مثل مُعرِّف الملف )، بالإضافة إلى معلومات إضافية مثل وضع الإدخال/الإخراج (قراءة، كتابة) وموقع الملف في التدفق. تُنشأ هذه الكائنات باستدعاء fopen(في مصطلحات البرمجة الكائنية، دالة إنشاء )، والتي تحصل على المورد وتُعيد مؤشرًا إليه؛ ويتم تحرير المورد باستدعاء دالة fcloseعلى مؤشر إلى FILEالكائن. [ 1 ] في الكود:

#include <stdio.h>FILE * f = fopen ( filename , mode ); // قم بتنفيذ إجراء ما على الملف f. fclose ( f );

لاحظ أن fcloseهذه دالة ذات FILE*مُعامل. في البرمجة كائنية التوجه، تُعتبر هذه دالة مُعرّفة في كائن ملف، كما هو الحال في بايثون.

من typing استورد TextIOf : TextIO = open ( filename ) # قم بتنفيذ إجراء ما باستخدام f. f . close ()

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

الإفراج الفوري

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

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

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

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

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

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

خروج مبكر

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

على سبيل المثال:

من typing استورد Any و TextIOدالة ( اسم الملف : سلسلة نصية ) -> أي نوع : f = فتح ( اسم الملف ) إذا كان a : إرجاع x f . إغلاق () إرجاع y

إذا عادت الدالة عند أول قيمة إرجاع، فلن يتم إغلاق الملف أبدًا وسيتم تسريب المورد.

من typing استورد TextIOdef func ( filename : str ) -> None : f : TextIO = open ( filename ) g ( f ) # قم بتنفيذ إجراء ما على f قد يؤدي إلى ظهور استثناء. f . close ()

إذا أثار الكود المتداخل استثناءً، فإن الدالة تخرج مبكرًا ولا يتم إغلاق الملف أبدًا، وبالتالي يتم تسريب المورد.

يمكن معالجة كليهما بواسطة try...finallyبنية تضمن تنفيذ عبارة finally دائمًا عند الخروج:

من typing استورد TextIOdef func ( filename : str ) -> None : try : f : TextIO = open ( filename ) # Do something. finally : f . close ()

بشكل عام:

Resource resource = getResource (); try { // تم الحصول على المورد؛ نفّذ الإجراءات اللازمة عليه. ... } finally { // حرّر المورد، حتى في حال حدوث استثناء. resource.dispose ( ); }

try...finallyيُعد هذا التركيب ضروريًا لضمان سلامة الاستثناءات بشكل صحيح ، حيث أن finallyالكتلة تُمكّن من تنفيذ منطق التنظيف بغض النظر عما إذا تم طرح استثناء أم لا في tryالكتلة.

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

تراكيب اللغة

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

في لغة C++ ، يتم التخلص من الموارد تقليديًا من خلال نمط اكتساب الموارد هو التهيئة (RAII)، مثل استخدام المؤشرات الذكية . بمجرد خروج مورد من النطاق، يتم استدعاء دالة التدمير الخاصة به.

تتميز لغة C #using بالعبارة [ 2 ] التي تستدعي تلقائيًا Dispose()الطريقة على كائن ينفذ System.IDisposableالواجهة :

باستخدام ( المورد resource = الحصول على المورد ()) { // تنفيذ الإجراءات على المورد. // ... }

وهو ما يعادل:

باستخدام النظام ؛Resource resource = GetResource () try { // تنفيذ الإجراءات على المورد. // ... } finally { // قد لا يكون المورد قد تم الحصول عليه، أو قد يكون قد تم تحريره بالفعل if ( resource != null ) { (( IDisposable ) resource ) .Dispose (); } }

وبالمثل، تحتوي لغة بايثونwith على عبارة يمكن استخدامها لتحقيق تأثير مماثل مع كائن مدير السياق . يتطلب بروتوكول مدير السياق تنفيذ __enter__طرق __exit__يتم استدعاؤها تلقائيًا بواسطة withبنية العبارة، وذلك لمنع تكرار التعليمات البرمجية الذي قد يحدث مع نمط try/ finally. [ 3 ]

باستخدام مدير سياق الموارد ( ) كمورد : # تنفيذ الإجراءات على المورد. ... # تنفيذ إجراءات أخرى حيث يكون من المضمون إلغاء تخصيص المورد. ...

قدمت لغة جافا صيغة جديدة تسمى try-with-resources في الإصدار 7 من جافا. [ 4 ] يمكن استخدامها على الكائنات التي تنفذ java.lang.AutoCloseableالواجهة (التي تحدد الطريقة close()):

استيراد java.io.IOException ؛ استيراد java.io.OutputStream ؛try ( OutputStream config = new OutputStream ( "configs/config.txt" )) { // تنفيذ إجراء ما باستخدام 'config' } catch ( IOException e ) { // معالجة الاستثناء// يتم إغلاق المورد 'config' تلقائيًا }

في لغة Rust ، يتم تنفيذ دلالات حذف الكائنات باستخدام سمة std::ops::Drop، والتي تقوم بحذف الكائن تلقائيًا بعد خروجه من نطاق التطبيق. ويمكن استدعاء هذه السمة يدويًا std::mem::drop().

مشاكل

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

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

علاوة على ذلك، من الممكن استدعاء disposeكائن أكثر من مرة. مع أن هذا قد يشير إلى خطأ برمجي (إذ يجب التخلص من كل كائن يحتوي على مورد مرة واحدة فقط )، إلا أنه من الأبسط والأكثر موثوقية، وبالتالي يُفضّل عادةً، أن disposeتكون الدالة `dispose` مُكرّرة (بمعنى أن "استدعاءها عدة مرات يُعادل استدعاءها مرة واحدة"). [ 5 ] يُمكن تطبيق ذلك بسهولة باستخدام نفس disposedالحقل المنطقي والتحقق منه في شرط حماية في بداية الدالة dispose، وفي هذه الحالة يتم إرجاع القيمة فورًا، بدلًا من إثارة استثناء. [ 5 ] تُميّز جافا بين الأنواع القابلة للتخلص (تلك التي تُنفّذ AutoCloseable[ 6 ] ) والأنواع القابلة للتخلص حيث تكون الدالة `dispose` مُكرّرة (النوع الفرعي Closeable[ 7 ] ).

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

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

انظر أيضاً

ملحوظات

  1. في البرمجة القائمة على الفئات ، يتم تعريف الطرق في فئة، باستخدام معلمة ضمنيةthisأوselfمعلمة صريحة، بدلاً من كونها دوال تأخذ معلمة صريحة.

مراجع

  1. مرجع تعريفات القواعد، مواصفات يونكس الموحدة ، الإصدار 5 من مجموعة Open Groupstdio.h  
  2. مايكروسوفت MSDN: عبارة using (مرجع C#)
  3. غيدو فان روسوم ، نيك كوغلان (13 يونيو 2011). "PEP 343: عبارة "with"" . مؤسسة برمجيات بايثون.
  4. برنامج تعليمي لجافا من أوراكل: عبارة try-with-resources
  5. 1 2 3 "نمط التخلص" .
  6. قابل للإغلاق التلقائي
  7. قابل للإغلاق
  8. "واجهة قابلة للاستخدام لمرة واحدة" . تم الاطلاع عليه بتاريخ 2024-12-09 .

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