هذه ترجمة الجزء الأول من المقالة. تم كتابة المقال في عام 2008. بعد 10 سنوات ، فقدت أهميتها تقريبًا.
الإفراج الحتمي عن الموارد - الحاجة
على مدار أكثر من 20 عامًا من الخبرة في البرمجة ، قمت أحيانًا بتطوير لغتي الخاصة لحل المشكلات. تراوحت بين اللغات البسيطة والضرورية والتعبيرات العادية المتخصصة للأشجار. عند إنشاء اللغات ، هناك العديد من التوصيات ولا يجب انتهاك بعض القواعد البسيطة. أحدهم:
لا تقم أبدًا بإنشاء لغة استثناء لا يوجد فيها إصدار محدد للموارد.
خمن ما هي التوصيات التي لا يتبعها وقت تشغيل .NET ، ونتيجة لذلك ، فإن جميع اللغات تعتمد عليه؟
سبب وجود هذه القاعدة هو أن الإفراج القطعي عن الموارد ضروري لإنشاء برامج مدعومة . يوفر الإصدار المحدد من الموارد نقطة معينة يتأكد فيها المبرمج من تحرير المورد. هناك طريقتان لكتابة برامج موثوقة: النهج التقليدي هو الإفراج عن الموارد في أقرب وقت ممكن والنهج الحديث هو الإفراج عن الموارد لفترة غير محددة. ميزة النهج الحديث هي أن المبرمج لا يحتاج إلى تحرير الموارد بشكل صريح. العيب هو أنه من الأصعب بكثير كتابة تطبيق موثوق به ، هناك العديد من الأخطاء الدقيقة. لسوء الحظ ، تم إنشاء وقت تشغيل .NET باستخدام نهج حديث.
يدعم .NET الإصدار غير القطعي للموارد باستخدام طريقة Finalize
، والتي لها معنى خاص. IDisposable
للموارد ، أضافت Microsoft أيضًا واجهة IDisposable
(والفئات الأخرى ، التي سنناقشها لاحقًا). ومع ذلك ، لوقت التشغيل IDisposable
هي واجهة عادية ، مثل أي شخص آخر. تخلق حالة "الدرجة الثانية" بعض الصعوبات.
في C # ، يمكن تنفيذ "الإفراج القطعي للفقراء" باستخدام try
finally
try
أو using
(وهو نفس الشيء تقريبًا). تناقش Microsoft منذ فترة طويلة ما إذا كنت تريد القيام بعمليات حساب الارتباط أم لا ، ويبدو لي أنه تم اتخاذ قرار خاطئ. ونتيجة لذلك ، من أجل الإفراج finally
عن الموارد ، تحتاج إلى استخدام الخرقاء finally
/ using
بنيات أو استدعاء مباشر لـ IDisposable.Dispose
. IDisposable.Dispose
، وهو أمر محفوف بالأخطاء. بالنسبة لمبرمج C ++ الذي اعتاد على استخدام shared_ptr<T>
كلا الخيارين ليسا جذابين. (توضح الجملة الأخيرة حيث أن المؤلف لديه مثل هذه العلاقة - تقريبًا.
سهل المنال
IDisposable
هو حل للإفراج عن الموارد بشكل حاسم من قبل Misoftro. الأول للحالات التالية:
- أي نوع يمتلك موارد مُدارة (
IDisposable
). يجب أن يمتلك النوع بالضرورة ، أي إدارة وقت الحياة والموارد ، وليس مجرد الرجوع إليها. - أي نوع يمتلك موارد غير مُدارة.
- أي نوع يمتلك موارد مُدارة وغير مُدارة.
- أي نوع موروث من فصل
IDisposable
يطبق IDisposable
. لا أوصي بالوراثة من الفصول التي تمتلك موارد غير مُدارة. من الأفضل استخدام مرفق.
يساعد IDisposable
في تحرير الموارد بشكل حاسم ، ولكن لديه مشاكله الخاصة.
الصعوبات IDisposable - سهولة الاستخدام
الأشياء القابلة للاستخدام هي IDisposable
لاستخدامها مرهقة للغاية. يجب أن يتم لف استخدام كائن في بناء using
. الخبر السيئ هو أن C # لا يسمح using
مع نوع لا ينفذ IDisposable
. لذلك ، يجب على المبرمج الرجوع إلى الوثائق في كل مرة لفهم ما إذا كان من الضروري الكتابة using
، أو الكتابة using
كل مكان ، ثم محو حيث يقسم المترجم.
إن C ++ المُدار أفضل بكثير في هذا الصدد. وهو يدعم دلالات المكدس للأنواع المرجعية ، والتي تعمل فقط عند using
الأنواع عند الضرورة. يمكن أن يستفيد C # من القدرة على الكتابة using
أي نوع.
يمكن حل هذه المشكلة. أدوات تحليل التعليمات البرمجية. لجعل الأمور أسوأ ، إذا نسيت استخدامها ، يمكن للبرنامج اجتياز الاختبارات ، ولكن تعطل أثناء العمل "في الحقول".
بدلاً من حساب الارتباطات ، تواجه IDisposable
مشكلة أخرى - تحديد المالك. عندما تخرج النسخة الأخيرة من shared_ptr<T>
في C ++ عن نطاقها ، يتم تحرير الموارد على الفور ، ولا داعي للتفكير فيمن يجب تحريرها. على العكس من ذلك ، يمكن إجبار المبرمج على تحديد من "يملك" الكائن ومسؤول عن إطلاقه. في بعض الأحيان تكون الملكية واضحة: عندما يقوم أحد الكائنات بتغليف كائن آخر وينفذ نفسه قابل IDisposable
، يكون بالتالي مسؤولاً عن إطلاق الكائنات الفرعية. في بعض الأحيان يتم تحديد عمر كائن بواسطة كتلة التعليمات البرمجية ، ويستخدم المبرمج ببساطة using
حول هذا الكتلة. ومع ذلك ، هناك العديد من الحالات التي يمكن فيها استخدام كائن في عدة أماكن ويصعب تحديد عمره (على الرغم من أن العدد المرجعي في هذه الحالة سيكون جيدًا).
الصعوبات القابلة للتطبيق - التوافق العكسي
إن إضافة IDisposable
إلى الفصل وإزالة IDisposable
من قائمة الواجهات المنفذة يعد تغييرًا كبيرًا. لن يقوم رمز العميل الذي لا يتوقع IDisposable
بتحرير الموارد إذا قمت بإضافة IDisposable
إلى إحدى الفصول الدراسية التي تم تمريرها بالرجوع إلى واجهة أو فئة أساسية.
واجهت مايكروسوفت نفسها هذه المشكلة. IEnumerator
موروث من IDisposable
، و IEnumerator<T>
موروث. إذا قمت بتمرير IEnumerator<T>
الرمز الذي يتلقى IEnumerator
، فلن يتم استدعاء Dispose
.
هذه ليست نهاية العالم ، لكنها تعطي بعض الجوهر الثانوي لـ IDisposable
.
الصعوبات التي يمكن تحديدها - تصميم التسلسل الهرمي الطبقي
أكبر عيب ناتج عن IDisposable
في مجال تصميم التسلسل الهرمي هو أن كل فئة وواجهة يجب أن تتوقع ما إذا كان IDisposable
أحفاده.
إذا لم ترث IDisposable
، ولكن الطبقات التي تنفذ الواجهة تقوم أيضًا بتطبيق IDisposable
، فإن الشفرة النهائية إما ستتجاهل الإصدار المحدد ، أو يجب أن تتحقق مما إذا كان الكائن ينفذ IDisposable
. ولكن لهذا ، لن يكون من الممكن استخدام البناء باستخدام وسيكون عليك كتابة try
قبيحة finally
.
باختصار ، تعقيد IDisposable
تطوير البرمجيات القابلة لإعادة الاستخدام. السبب الرئيسي هو انتهاك أحد مبادئ التصميم الكينوني - فصل الواجهة والتنفيذ. يجب أن يكون الإفراج عن الموارد تفاصيل التنفيذ. قررت Microsoft أن تجعل الإصدار النهائي من الموارد واجهة من الدرجة الثانية.
أحد الحلول غير الجميلة هو جعل جميع الفصول تطبق IDisposable
، ولكن في الغالبية العظمى من الفصول ، IDisposable.Dispose
، لن يفعل IDisposable.Dispose
أي شيء. لكن هذا ليس جميلًا جدًا.
صعوبة أخرى مع IDisposable
هي المجموعات. بعض المجموعات "تمتلك" أشياء فيها ، والبعض الآخر لا يمتلكها. ومع ذلك ، فإن المجموعات نفسها لا تنفذ IDisposable
. يجب أن يتذكر المبرمج استدعاء IDisposable.Dispose
من الكائنات الموجودة في المجموعة ، أو إنشاء أحفاده الخاصة من فئات المجموعة التي تطبق IDisposable
ليعني الملكية.
الصعوبات القابلة للتحديد - حالة "خاطئة" إضافية
يمكن استدعاء IDisposable
بشكل صريح في أي وقت ، بغض النظر عن عمر الكائن. أي ، تتم إضافة حالة "تم تحريرها" إلى كل كائن ، حيث يوصى بإلقاء ObjectDisposedException
. التحقق من الحالة ورمي الاستثناءات هو حساب إضافي.
بدلاً من التحقق من كل عطس ، من الأفضل التفكير في الوصول إلى الكائن في حالة "تحرير" ك "سلوك غير محدد" كمكالمة للذاكرة المحررة.
صعوبات يمكن التخلص منها - لا ضمانات
IDisposable
هو مجرد واجهة. يدعم الفصل الذي يطبق IDisposable
الإصدار الحتمي ، لكنه لا يضمنه . بالنسبة لرمز العميل ، لا بأس بعدم الاتصال Dispose
. لذلك ، يجب أن تدعم الطبقة التي تطبق IDisposable
الإفراج الحتمية وغير الحتمية.
التعقيدات القابلة للتطبيق - التنفيذ المعقد
تقدم Microsoft نمطًا لتطبيق IDisposable
. (في السابق ، كان هناك نمط رهيب بشكل عام ، ولكن في الآونة الأخيرة نسبيًا ، بعد ظهور .NET 4 ، تم تصحيح الوثائق ، بما في ذلك تحت تأثير هذه المقالة. في الإصدارات القديمة من كتب .NET ، يمكنك العثور على الإصدار القديم. - تقريبًا. )
- لا يمكن استدعاء
IDisposable.Dispose
على الإطلاق ، لذلك يجب أن يتضمن الفصل أداة نهائية لتحرير الموارد. - يمكن استدعاء
IDisposable.Dispose
عدة مرات ويجب أن يعمل بدون آثار جانبية مرئية. لذلك ، من الضروري إضافة علامة تحقق مما إذا كانت الطريقة قد تم استدعاؤها بالفعل أم لا. - يتم استدعاء أداة الإنهاء النهائية في خيط منفصل ويمكن استدعاؤها قبل
IDisposable.Dispose
. استخدام GC.SuppressFinalize
لتجنب مثل هذه "الأجناس".
بالإضافة إلى:
- تسمى أدوات الإنهاء ، بما في ذلك الكائنات التي ترمي استثناء في المنشئ. لذلك ، يجب أن يعمل رمز الإصدار مع كائنات تمت تهيئتها جزئيًا.
- يتطلب تطبيق
IDisposable
في فئة موروثة من CriticalFinalizerObject
بنيات غير تافهة. void Dispose(bool disposing)
هو طريقة فيروسية ويجب تنفيذها في منطقة التنفيذ المقيدة ، والتي تتطلب استدعاء RuntimeHelpers.PrepareMethod
.
الصعوبات التي يمكن تحديدها - غير مناسبة لمنطق الإنجاز
إيقاف تشغيل كائن - غالبًا ما يحدث في البرامج في خيوط متوازية أو غير متزامنة. على سبيل المثال ، يستخدم الفصل مؤشر ترابط منفصل ويريد إكماله باستخدام ManualResetEvent
. يمكن القيام بذلك في IDisposable.Dispose
، ولكن يمكن أن يؤدي إلى خطأ إذا تم استدعاء الرمز في أداة الإنهاء.
لفهم القيود في أداة الإنهاء ، تحتاج إلى فهم كيفية عمل جامع القمامة. فيما يلي رسم تخطيطي مبسط يتم فيه حذف العديد من التفاصيل المتعلقة بالأجيال ، والروابط الضعيفة ، وإحياء الأشياء ، وجمع القمامة في الخلفية ، وما إلى ذلك.
يستخدم جامع البيانات المهملة .NET خوارزمية علامة المسح. بشكل عام ، يبدو المنطق كما يلي:
- قم بإيقاف جميع سلاسل المحادثات
- خذ جميع الكائنات الجذرية: المتغيرات على المكدس ، الحقول الثابتة ، كائنات
GCHandle
، قائمة انتظار GCHandle
. في حالة تفريغ مجال التطبيق (إنهاء البرنامج) ، يعتبر أن المتغيرات في المكدس والحقول الثابتة ليست جذور. - راجع جميع الروابط من الكائنات بشكل متكرر ووضع علامة عليها على أنها "قابلة للوصول".
- قم
GC.SuppressFinalize
عبر جميع الكائنات الأخرى التي تحتوي على مواد مدمرة (اللمسات النهائية) ، وأعلن أنها قابلة للوصول ، وضعها في قائمة انتظار GC.SuppressFinalize
( GC.SuppressFinalize
يخبر GC بعدم القيام بذلك). يتم وضع الكائنات في قائمة الانتظار في ترتيب غير متوقع.
في الخلفية ، يعمل تيار (أو عدة) من الإنهاء:
- يأخذ كائن من قائمة الانتظار ويبدأ اللمسات النهائية الخاصة به. من الممكن تشغيل العديد من اللمسات النهائية لكائنات مختلفة في نفس الوقت.
- تتم إزالة الكائن من قائمة الانتظار ، وإذا لم يشر إليه أي شخص آخر ، فسيتم مسحه في مجموعة البيانات المهملة التالية.
الآن يجب أن يكون واضحًا لماذا من المستحيل الوصول إلى الموارد المُدارة من أداة الإنهاء - أنت لا تعرف بأي ترتيب يُطلق على أدوات الإنهاء. حتى استدعاء IDisposable.Dispose
كائن آخر من أداة الإنهاء يمكن أن يؤدي إلى خطأ ، حيث قد يعمل رمز إصدار المورد في مؤشر ترابط آخر.
هناك بعض الاستثناءات عندما يمكنك الوصول إلى الموارد المُدارة من أداة الإنهاء:
- يتم الانتهاء من الكائنات الموروثة من
CriticalFinalizerObject
بعد الانتهاء من الكائنات غير الموروثة من هذه الفئة. هذا يعني أنه يمكنك استدعاء ManualResetEvent
من أداة ManualResetEvent
حتى يتم توريث الفئة من CriticalFinalizerObject
- بعض الكائنات والأساليب خاصة ، مثل وحدة التحكم وبعض طرق سلسلة العمليات. يمكن استدعاؤها من اللمسات الأخيرة حتى لو انتهى البرنامج.
في الحالة العامة ، من الأفضل عدم الوصول إلى الموارد المدارة من اللمسات الأخيرة. ومع ذلك ، فإن منطق الإنجاز ضروري للبرمجيات غير العادية. في Windows.Forms
يحتوي على منطق إكمال في أسلوب Application.Exit
. عندما تقوم بتطوير مكتبة المكونات الخاصة بك ، فإن أفضل شيء يمكنك القيام به هو إكمال منطق الاكتمال مع IDisposable
. الإنهاء العادي في حالة استدعاء IDisposable.Dispose
والطوارئ خلاف ذلك.
واجهت مايكروسوفت أيضا هذه المشكلة. تمتلك فئة StreamWriter
كائن Stream
(اعتمادًا على معلمات المُنشئ في أحدث إصدار - تقريبًا لكل. ). StreamWriter.Close
يقوم Stream.Close
المخزن المؤقت ويستدعي Stream.Close
(يحدث أيضًا إذا تم تغليفه using
- تقريبًا لكل. ). إذا لم StreamWriter
إغلاق StreamWriter
، فلن يتم مسح المخزن المؤقت ويتم فقدان محادثة البيانات. لم تقم Microsoft ببساطة بإعادة تعريف أداة الإنهاء ، وبالتالي "حل" مشكلة الإنجاز. مثال رائع على الحاجة إلى منطق الإنجاز.
أوصي بالقراءة
تأتي الكثير من المعلومات حول .NET الداخلية في هذه المقالة من CLR لـ Jeffrey Richter عبر C #. إذا لم يكن لديك بعد ، فقم بشرائه . بجدية. هذه هي المعرفة اللازمة لأي مبرمج C #.
استنتاج من المترجم
لن يواجه معظم مبرمجي .NET المشكلات الموضحة في هذه المقالة مطلقًا. سوف تتطور .NET لزيادة مستوى التجريد وتقليل الحاجة إلى "التلاعب" في الموارد غير المدارة. ومع ذلك ، هذه المقالة مفيدة لأنها تصف التفاصيل العميقة للأشياء البسيطة وتأثيرها على تصميم الكود.
سيكون الجزء التالي مناقشة تفصيلية لكيفية العمل مع الموارد المدارة وغير المدارة في .NET مع مجموعة من الأمثلة.