
تعدد
الآن دعنا نتحدث عن الجليد الرقيق. في الأقسام السابقة حول IDisposable ، لمسنا مفهومًا مهمًا للغاية لا يقوم فقط على مبادئ تصميم الأنواع التي يمكن التخلص منها ولكن أيضًا أي نوع بشكل عام. هذا هو مفهوم سلامة الكائن. هذا يعني أن الكائن في أي لحظة زمنية محددة في حالة محددة بدقة وأن أي إجراء بهذا الكائن يحول حالته إلى أحد الخيارات التي تم تحديدها مسبقًا أثناء تصميم نوع من هذا الكائن. بمعنى آخر ، يجب ألا يؤدي أي إجراء مع الكائن إلى تحويله إلى حالة غير محددة. ينتج عن هذا مشكلة في الأنواع المصممة في الأمثلة أعلاه. أنها ليست آمنة الموضوع. هناك فرصة لأن يتم استدعاء الأساليب العامة من هذه الأنواع عندما يكون تدمير كائن قيد التقدم. دعونا نحل هذه المشكلة ونقرر ما إذا كان ينبغي لنا حلها على الإطلاق.
تمت ترجمة هذا الفصل من اللغة الروسية بالاشتراك مع المؤلفين والمترجمين المحترفين . يمكنك مساعدتنا في الترجمة من الروسية أو الإنجليزية إلى أي لغة أخرى ، في المقام الأول إلى الصينية أو الألمانية.
وأيضًا ، إذا كنت تريد شكراً منا ، فإن أفضل طريقة للقيام بذلك هي منحنا نجمًا على github أو لتخزين المستودع
github / sidristij / dotnetbook .
public class FileWrapper : IDisposable { IntPtr _handle; bool _disposed; object _disposingSync = new object(); public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Seek(int position) { lock(_disposingSync) { CheckDisposed(); // Seek API call } } public void Dispose() { lock(_disposingSync) { if(_disposed) return; _disposed = true; } InternalDispose(); GC.SuppressFinalize(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { lock(_disposingSync) { if(_disposed) { throw new ObjectDisposedException(); } } } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods }
يجب _disposed
رمز التحقق _disposed
في Dispose () كقسم هام. في الواقع ، يجب تهيئة كود الطرق العامة بالكامل كقسم مهم. سيؤدي ذلك إلى حل مشكلة الوصول المتزامن إلى أسلوب عام من نوع مثيل وإلى طريقة إتلافه. ومع ذلك ، فإنه يجلب المشاكل الأخرى التي أصبحت قنبلة موقوتة:
- الاستخدام المكثف لأساليب مثيل الكتابة وكذلك إنشاء الكائنات وتدميرها سيؤدي إلى خفض الأداء بشكل ملحوظ. وذلك لأن أخذ قفل يستهلك الوقت. هذه المرة ضرورية لتخصيص جداول SyncBlockIndex ، والتحقق من سلسلة الرسائل الحالية والعديد من الأشياء الأخرى (سنتعامل معها في الفصل الخاص بموضوع multithreading). هذا يعني أنه سيتعين علينا التضحية بأداء الكائن طوال حياته من أجل "الميل الأخير" من حياته.
- حركة مرور ذاكرة إضافية لكائنات المزامنة.
- الخطوات الإضافية التي يجب أن يتخذها GC للذهاب إلى الرسم البياني للكائن.
الآن ، دعنا نذكر الشيء الثاني ، وفي رأيي ، أهم شيء. نحن نسمح بتدمير كائن وفي نفس الوقت نتوقع العمل به مرة أخرى. ماذا نأمل في هذه الحالة؟ أنه سوف تفشل؟ لأنه إذا تم تشغيل Dispos أولاً ، فإن الاستخدام التالي لأساليب الكائن سيؤدي بالتأكيد إلى ObjectDisposedException
. لذلك ، يجب عليك تفويض المزامنة بين مكالمات Dispose () والأساليب العامة الأخرى لنوع ما إلى جانب الخدمة ، أي إلى الكود الذي أنشأ مثيل فئة FileWrapper
. فذلك لأن الجانب المبتكر فقط هو الذي يعرف ما الذي سيفعله بمثيل من الفصل ومتى يتم تدميره. من ناحية أخرى ، يجب أن ينتج عن المكالمة Disخلص فقط أخطاء فادحة ، مثل OutOfMemoryException
، ولكن ليس IOException على سبيل المثال. هذا بسبب متطلبات بنية الفئات التي تنفذ IDisposable. هذا يعني أنه إذا تم استدعاء Dispos من أكثر من مؤشر ترابط واحد في وقت واحد ، فقد يحدث تدمير كيان من if(_disposed) return;
وقت واحد if(_disposed) return;
التحقق من if(_disposed) return;
). يعتمد ذلك على الموقف: إذا كان بالإمكان إصدار مورد عدة مرات ، فليست هناك حاجة لإجراء فحوصات إضافية. خلاف ذلك ، الحماية ضرورية:
// I don't show the whole pattern on purpose as the example will be too long // and will not show the essence class Disposable : IDisposable { private volatile int _disposed; public void Dispose() { if(Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) { // dispose } } }
مستويين من مبدأ التصميم المتاح
ما هو النمط الأكثر شيوعًا لتطبيق IDisposable
الذي يمكنك مقابلته في كتب .NET والإنترنت؟ ما النمط المتوقع منك خلال المقابلات لوظيفة جديدة محتملة؟ على الأرجح هذا واحد:
public class Disposable : IDisposable { bool _disposed; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if(disposing) { // here we release managed resources } // here we release unmanaged resources } protected void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } ~Disposable() { Dispose(false); } }
ما الخطأ في هذا المثال ولماذا لم نكتب مثل هذا من قبل؟ في الواقع ، هذا هو نمط جيد مناسب لجميع الحالات. ومع ذلك ، فإن استخدامه في كل مكان ليس أسلوبًا جيدًا في رأيي لأننا لا نتعامل مع الموارد غير المُدارة تقريبًا في الممارسة مما يجعل نصف النمط لا يخدم أي غرض. علاوة على ذلك ، نظرًا لأنه يدير في الوقت نفسه كل من الموارد المدارة وغير المدارة ، فإنه ينتهك مبدأ تقسيم المسؤولية. أعتقد أن هذا خطأ. دعونا ننظر في نهج مختلف قليلا. مبدأ التصميم المتاح . باختصار ، يعمل كما يلي:
يقسم التخلص إلى مستويين من الفصول:
- أنواع المستوى 0 تغلف مباشرة الموارد غير المدارة
- هم إما مجردة أو معبأة.
- يجب تمييز جميع الطرق:
- PrePrepareMethod ، بحيث يمكن تجميع طريقة عند تحميل نوع
- SecuritySafeCritical للحماية من مكالمة من التعليمات البرمجية ، والعمل تحت قيود
- ReliabilityContract (Consistency.WillNotCorruptState، Cer.Success / MayFail)] لوضع CER لطريقة وكل مكالماتها الفرعية
- يمكنهم الرجوع إلى أنواع المستوى 0 ، ولكن يجب زيادة عداد الكائنات المرجعية لضمان الترتيب الصحيح لإدخال "الميل الأخير"
- تتضمن أنواع المستوى 1 الموارد المدارة فقط
- أنها موروثة فقط من أنواع المستوى 1 أو تنفذ IDisposable مباشرة
- لا يمكن أن يرثوا أنواع المستوى 0 أو CriticalFinalizerObject
- يمكنهم تغليف الأنواع المدارة من المستوى 1 والمستوى 0
- يتم تطبيق IDisposable. التخلص من خلال تدمير الكائنات المغلفة بدءًا من أنواع المستوى 0 والانتقال إلى المستوى 1
- لا يقومون بتطبيق أداة نهائية لأنهم لا يتعاملون مع الموارد غير المدارة
- يجب أن تحتوي على خاصية محمية تتيح الوصول إلى أنواع المستوى 0.
لهذا السبب استخدمت القسم في نوعين من البداية: النوع الذي يحتوي على مورد مُدار والآخر به مورد غير مُدار. يجب أن تعمل بشكل مختلف.
طرق أخرى لاستخدام التخلص منها
كانت الفكرة وراء إنشاء IDisposable هي إطلاق موارد غير مُدارة. ولكن كما هو الحال مع العديد من الأنماط الأخرى ، من المفيد للغاية بالنسبة للمهام الأخرى ، على سبيل المثال إصدار إشارات إلى الموارد المدارة. على الرغم من أن إطلاق الموارد المدارة لا يبدو مفيدًا للغاية. أعني أنهم يطلق عليهم إدارة عن قصد ، لذلك سنسترخي مع ابتسامة بشأن مطوري C / C ++ ، أليس كذلك؟ ومع ذلك ، ليس كذلك. قد يكون هناك دائمًا موقف حيث نفقد إشارة إلى كائن ولكن في الوقت نفسه نعتقد أن كل شيء على ما يرام: سوف يقوم GC بجمع البيانات المهملة ، بما في ذلك كائننا. ومع ذلك ، اتضح أن الذاكرة تنمو. ندخل في برنامج تحليل الذاكرة ونرى أن هناك شيء آخر يحمل هذا الكائن. الشيء هو أنه يمكن أن يكون هناك منطق لالتقاط ضمني للإشارة إلى الكيان الخاص بك في كل من منصة .NET وبنية الطبقات الخارجية. نظرًا لأن الالتقاط ضمني ، يمكن للمبرمج أن يفقد ضرورة إصداره ومن ثم الحصول على تسرب للذاكرة.
المندوبين ، والأحداث
لنلقِ نظرة على هذا المثال الصناعي:
class Secondary { Action _action; void SaveForUseInFuture(Action action) { _action = action; } public void CallAction() { _action(); } } class Primary { Secondary _foo = new Secondary(); public void PlanSayHello() { _foo.SaveForUseInFuture(Strategy); } public void SayHello() { _foo.CallAction(); } void Strategy() { Console.WriteLine("Hello!"); } }
ما المشكلة التي يظهرها هذا الرمز؟ مخازن فئة ثانوية مفوض نوع Action
في حقل _action
مقبول في أسلوب SaveForUseInFuture
. بعد ذلك ، PlanSayHello
طريقة PlanSayHello
داخل الفصل Primary
بتمرير المؤشر إلى طريقة Strategy
إلى الفصل Secondary
. إنه أمر مثير للفضول ، لكن في هذا المثال ، إذا قمت بتمرير طريقة ثابتة أو طريقة مثيل ، فلن يتم تغيير SaveForUseInFuture
، ولكن سيتم الرجوع إلى مثيل فئة Primary
ضمنيًا أو لا يتم الرجوع إليه على الإطلاق. ظاهريا ، يبدو أنك وجهت أي طريقة للاتصال بها. ولكن في الواقع ، تم تصميم المفوض ليس فقط باستخدام مؤشر أسلوب ولكن أيضًا باستخدام المؤشر إلى مثيل لفئة. يجب أن يفهم الطرف المتصل أي نسخة من الفصل يجب أن يطلق عليها طريقة Strategy
! هذا هو مثيل الفئة Secondary
قد تم قبوله ضمنيًا ويحمل المؤشر إلى مثيل الفئة Primary
، على الرغم من أنه لم يتم الإشارة إليه صراحة. بالنسبة لنا ، يعني هذا فقط أننا إذا _foo
مؤشر _foo
مكان آخر _foo
الإشارة إلى Primary
، فلن يقوم GC بجمع الكائن Primary
، حيث سيحتفظ به Secondary
. كيف يمكننا تجنب مثل هذه الحالات؟ نحن بحاجة إلى نهج حازم لإصدار إشارة لنا. آلية مناسبة تماما لهذا الغرض هو IDisposable
// This is a simplified implementation class Secondary : IDisposable { Action _action; public event Action<Secondary> OnDisposed; public void SaveForUseInFuture(Action action) { _action = action; } public void CallAction() { _action?.Invoke(); } void Dispose() { _action = null; OnDisposed?.Invoke(this); } }
الآن المثال يبدو مقبولا. إذا تم نقل مثيل لفئة ما إلى جهة خارجية _action
الإشارة إلى _action
المفوض أثناء هذه العملية ، فسنقوم بتعيينها على الصفر وسيتم إعلام الطرف الثالث بتدمير المثيل وحذف الإشارة إليه. .
الخطر الثاني من التعليمات البرمجية التي يتم تشغيلها على المندوبين هو مبادئ أداء event
. لنلقِ نظرة على ما ينتج عنه:
// a private field of a handler private Action<Secondary> _event; // add/remove methods are marked as [MethodImpl(MethodImplOptions.Synchronized)] // that is similar to lock(this) public event Action<Secondary> OnDisposed { add { lock(this) { _event += value; } } remove { lock(this) { _event -= value; } } }
تخفي رسائل C # الأجزاء الداخلية للأحداث وتحتفظ بجميع الكائنات التي اشتركت في التحديث من خلال event
. إذا حدث خطأ ما ، OnDisposed
الإشارة إلى كائن موقّع في OnDisposed
بالكائن. إنه موقف غريب كما هو الحال بالنسبة للهندسة المعمارية ، فنحن لدينا مفهوم "مصدر الأحداث" الذي يجب ألا يحتفظ بأي شيء بطريقة منطقية. ولكن في الواقع ، يتم الاحتفاظ الكائنات المشتركة في التحديث ضمنيًا. بالإضافة إلى ذلك ، لا يمكننا تغيير شيء ما داخل مجموعة المندوبين هذه على الرغم من أن الكيان يخصنا. الشيء الوحيد الذي يمكننا القيام به هو حذف هذه القائمة عن طريق تعيين قيمة خالية لمصدر الأحداث.
الطريقة الثانية هي تطبيق أساليب add
/ remove
بشكل صريح ، حتى نتمكن من التحكم في مجموعة من المفوضين.
قد يظهر موقف ضمني آخر هنا. قد يبدو أنك إذا قمت بتعيين قيمة خالية لمصدر الأحداث ، فإن الاشتراك التالي في الأحداث سيتسبب في NullReferenceException
. أعتقد أن هذا سيكون أكثر منطقية.
ومع ذلك ، هذا ليس صحيحا. إذا اشترك رمز خارجي في الأحداث بعد مسح مصدر الأحداث ، فسيقوم FCL بإنشاء مثيل جديد لفئة Action وتخزينه في OnDisposed
. يمكن لهذا التورط في C # أن يضلل مبرمجًا: يجب أن ينتج عن التعامل مع الحقول الفارغة نوعًا من اليقظة بدلاً من الهدوء. نوضح هنا أيضًا الطريقة التي يمكن أن يؤدي بها إهمال مبرمج إلى حدوث تسرب للذاكرة.
الإغلاقات Lambdas
يعد استخدام السكر النحوي مثل لامبدا أمرًا خطيرًا بشكل خاص.
أود أن أتناول السكر النحوي ككل. أعتقد أنه يجب عليك استخدامه بعناية وبدقة فقط إذا كنت تعرف النتيجة بدقة. الأمثلة على تعبيرات lambda هي الإغلاقات والإغلاقات في التعبيرات والعديد من المصائب الأخرى التي يمكن أن تلحقها بنفسك.
بالطبع ، قد تقول إنك تعلم أن تعبير lambda يخلق إغلاقًا وقد يؤدي إلى خطر تسرب المورد. لكنه أنيق للغاية ، لطيف للغاية بحيث يصعب تجنب استخدام lambda بدلاً من تخصيص الطريقة بالكامل ، والتي سيتم وصفها في مكان مختلف عن المكان الذي سيتم استخدامه فيه. في الواقع ، لا ينبغي لك أن تشتري هذا الاستفزاز ، ولكن ليس كل شخص يمكنه المقاومة. لنلقِ نظرة على المثال التالي:
button.Clicked += () => service.SendMessageAsync(MessageType.Deploy);
موافق ، هذا الخط يبدو آمنا للغاية. ولكنه يخفي مشكلة كبيرة: الآن يشير button
متغير service
ضمنيًا ويحملها. حتى إذا قررنا أننا لم نعد بحاجة إلى service
، فسيظل button
يحتفظ بالمرجع أثناء وجود هذا المتغير. تتمثل إحدى الطرق لحل هذه المشكلة في استخدام نمط لإنشاء IDisposable
من أي Action
( System.Reactive.Disposables
):
// Here we create a delegate from a lambda Action action = () => service.SendMessageAsync(MessageType.Deploy); // Here we subscribe button.Clicked += action; // We unsubscribe var subscription = Disposable.Create(() => button.Clicked -= action); // where it is necessary subscription.Dispose();
أعترف ، هذا يبدو طويلاً ونفقد الغرض كله من استخدام تعبيرات lambda. يعد استخدام الطرق الخاصة الشائعة أكثر أمانًا وأبسطًا لالتقاط المتغيرات ضمنيًا.
حماية Threadabort
عند إنشاء مكتبة لمطور تابع لجهة خارجية ، لا يمكنك التنبؤ بسلوكها في تطبيق تابع لجهة أخرى. في بعض الأحيان ، يمكنك فقط تخمين ما قام به مبرمج لمكتبتك مما تسبب في نتيجة معينة. مثال واحد يعمل في بيئة ذات مؤشرات ترابط متعددة عندما يصبح تناسق تنظيف الموارد مشكلة حرجة. لاحظ أنه عندما نكتب طريقة Dispose()
، يمكننا ضمان عدم وجود استثناءات. ومع ذلك ، لا يمكننا ضمان أنه أثناء تشغيل الأسلوب Dispose()
لن يحدث ThreadAbortException
يؤدي إلى تعطيل مؤشر ترابط التنفيذ الخاص بنا. هنا يجب أن نتذكر أنه عند حدوث ThreadAbortException
، يتم تنفيذ جميع كتل catch / وأخيراً على أي حال (في نهاية كتلة catch / وأخيراً ، يحدث ThreadAbort بشكل أكبر). لذلك ، لضمان تنفيذ رمز معين باستخدام Thread.Abort ، تحتاج إلى التفاف قسم مهم في try { ... } finally { ... }
، انظر المثال أدناه:
void Dispose() { if(_disposed) return; _someInstance.Unsubscribe(this); _disposed = true; }
يمكن للمرء إحباط هذا في أي وقت باستخدام Thread.Abort
. إنه يدمر كائنًا جزئيًا ، على الرغم من أنه لا يزال بإمكانك التعامل معه في المستقبل. في نفس الوقت ، الكود التالي:
void Dispose() { if(_disposed) return; // ThreadAbortException protection try {} finally { _someInstance.Unsubscribe(this); _disposed = true; } }
محمي من هذا الإجهاض وسيعمل بسلاسة وبشكل مؤكد ، حتى إذا ظهر Thread.Abort
بين استدعاء طريقة Unsubscribe
وتنفيذ تعليماته.
النتائج
المزايا
حسنًا ، لقد تعلمنا الكثير عن هذا النمط البسيط. دعونا نحدد مزاياه:
- الميزة الرئيسية للنمط هي القدرة على تحرير الموارد بشكل محدد ، أي عند الحاجة إليها.
- الميزة الثانية هي إدخال طريقة مجربة للتحقق مما إذا كان هناك مثيل معين يتطلب تدمير مثيلاته بعد الاستخدام.
- إذا قمت بتطبيق النمط بشكل صحيح ، فإن النوع المصمم سيعمل بأمان من حيث الاستخدام بواسطة مكونات الطرف الثالث وكذلك من حيث تفريغ الموارد وتدميرها عند تعطل إحدى العمليات (على سبيل المثال بسبب نقص الذاكرة). هذه هي الميزة الأخيرة.
العيوب
في رأيي ، هذا النمط له عيوب أكثر من المزايا.
- فمن ناحية ، يرشد أي نوع يقوم بتنفيذ هذا النمط إلى أجزاء أخرى أنه في حالة استخدامه ، فإنه يأخذ نوعًا من العروض العامة. هذا ضمني لدرجة أنه كما هو الحال في العروض العامة ، لا يعرف مستخدم من نوع ما دائمًا أن النوع لديه هذه الواجهة. وبالتالي عليك اتباع مطالبات IDE (اكتب فترة ، Dis ... وتحقق مما إذا كانت هناك طريقة في قائمة الأعضاء المصفاة للفصل). إذا رأيت نمط التخلص ، فيجب عليك تنفيذه في الكود. في بعض الأحيان لا يحدث ذلك على الفور ، وفي هذه الحالة يجب عليك تطبيق نمط من خلال نظام أنواع يضيف وظائف. مثال جيد على ذلك أن
IEnumerator<T>
يستلزم تحديد IDisposable
. - عادة عندما تقوم بتصميم واجهة ، هناك حاجة لإدراج IDisposable في نظام واجهات النوع عندما يكون على إحدى الوراثة أن ترث IDisposable. في رأيي ، هذا يدمر الواجهات التي صممناها. أعني عند تصميم واجهة تقوم بإنشاء بروتوكول تفاعل أولاً. هذه مجموعة من الإجراءات التي يمكنك تنفيذها باستخدام شيء مخفي خلف الواجهة.
Dispose()
هي طريقة لتدمير مثيل لفئة. هذا يتناقض مع جوهر بروتوكول التفاعل . في الواقع ، هذه هي تفاصيل التنفيذ التي تسللت إلى الواجهة. - على الرغم من تحديد ذلك ، لا يعني التخلص () التدمير المباشر لكائن ما. سيظل الكائن موجودًا بعد تدميره ولكن في حالة أخرى. لجعله صحيحًا ، يجب أن يكون CheckDisposed () هو الأمر الأول لكل طريقة عامة. هذا يبدو وكأنه حل مؤقت أعطانا شخصًا قوله: "اخرج واضرب" ؛
- هناك أيضًا فرصة صغيرة للحصول على نوع يقوم بتنفيذ
IDisposable
خلال تطبيق صريح . أو يمكنك الحصول على نوع يقوم بتنفيذ المعرّف المتاح دون فرصة لتحديد من يجب عليه تدميره: أنت أو الجهة التي قدمته لك. نتج عن هذا مضادات لمكالمات متعددة من التخلص () تسمح بتدمير كائن مدمر ؛ - التنفيذ الكامل صعب ، ويختلف عن الموارد المدارة وغير المدارة. هنا تبدو محاولة تسهيل عمل المطورين من خلال GC محرجة. يمكنك تجاوز طريقة
virtual void Dispose()
وإدخال نوع DisposableObject يقوم بتنفيذ النموذج بالكامل ، لكن هذا لا يحل المشاكل الأخرى المرتبطة بالنمط ؛ - كقاعدة ، يتم تطبيق طريقة التخلص () Dispose () في نهاية الملف بينما يتم الإعلان عن ".ctor" في البداية. إذا عدّلت فصلًا دراسيًا أو قدمت موارد جديدة ، فمن السهل أن تنسى إضافة التخلص منها.
- أخيرًا ، من الصعب تحديد ترتيب التدمير في بيئة ذات مؤشرات ترابط متعددة عندما تستخدم نمطًا لرسومات بيانية للكائنات حيث تنفذ الكائنات هذا النمط كليًا أو جزئيًا. أعني المواقف التي يمكن أن يبدأ فيها Dispos () في نهايات مختلفة من الرسم البياني. من الأفضل هنا استخدام أنماط أخرى ، مثل نمط العمر.
- رغبة مطوري الأنظمة الأساسية في أتمتة التحكم في الذاكرة مع الواقع: تتفاعل التطبيقات مع التعليمات البرمجية غير المُدارة في كثير من الأحيان + تحتاج إلى التحكم في إصدار المراجع إلى الكائنات حتى يتمكن Garbage Collector من تجميعها. هذا يضيف ارتباكًا كبيرًا في فهم أسئلة مثل: "كيف يجب أن نطبق نمطًا صحيحًا"؟ "هل هناك نمط موثوق على الإطلاق"؟ ربما الدعوة
delete obj; delete[] arr;
delete obj; delete[] arr;
هو أبسط؟
تفريغ المجال والخروج من التطبيق
إذا وصلت إلى هذا الجزء ، فقد أصبحت أكثر ثقة في نجاح المقابلات الوظيفية المستقبلية. ومع ذلك ، لم نناقش جميع الأسئلة المرتبطة بهذا النمط البسيط ، كما قد يبدو. السؤال الأخير هو ما إذا كان سلوك التطبيق يختلف في حالة تجميع البيانات المهملة البسيطة ومتى يتم تجميع البيانات المهملة أثناء إلغاء تحميل المجال وأثناء الخروج من التطبيق. يمس هذا السؤال مجرد Dispose()
... ومع ذلك Dispose()
والانتهاء يسيران جنبًا إلى جنب ، ونادراً ما نلتقي بتطبيق لفئة تحتوي على الصيغة النهائية ولكن ليس لديها طريقة Dispose()
. لذلك ، دعونا تصف الانتهاء في قسم منفصل. نحن هنا فقط إضافة بعض التفاصيل الهامة.
أثناء إلغاء تحميل مجال التطبيق ، تقوم بإلغاء تحميل التجميعين اللذين تم تحميلهما في مجال التطبيق وجميع الكائنات التي تم إنشاؤها كجزء من المجال الذي سيتم إلغاء تحميله. في الواقع ، هذا يعني تنظيف (جمع بواسطة GC) من هذه الكائنات واستدعاء finalizers لهم. إذا كان منطق أداة النهاية ينتظر أن يتم إتلاف وضع اللمسات الأخيرة على كائنات أخرى في الترتيب الصحيح ، فيمكنك الانتباه إلى Environment.HasShutdownStarted
خاصية HasShutdownStarted التي تشير إلى إلغاء تحميل تطبيق من الذاكرة وإلى AppDomain.CurrentDomain.IsFinalizingForUnload()
يشير إلى أن هذا تم إلغاء تحميل النطاق وهو سبب الانتهاء. في حالة حدوث هذه الأحداث ، يصبح ترتيب الانتهاء من الموارد بشكل عام غير مهم. لا يمكننا تأخير إما إلغاء تحميل النطاق أو التطبيق كما يجب أن نفعل كل شيء في أسرع وقت ممكن.
هذه هي الطريقة التي يتم بها حل هذه المهمة كجزء من فئة LoaderAllocatorScout
// Assemblies and LoaderAllocators will be cleaned up during AppDomain shutdown in // an unmanaged code // So it is ok to skip reregistration and cleanup for finalization during appdomain shutdown. // We also avoid early finalization of LoaderAllocatorScout due to AD unload when the object was inside DelayedFinalizationList. if (!Environment.HasShutdownStarted && !AppDomain.CurrentDomain.IsFinalizingForUnload()) { // Destroy returns false if the managed LoaderAllocator is still alive. if (!Destroy(m_nativeLoaderAllocator)) { // Somebody might have been holding a reference on us via weak handle. // We will keep trying. It will be hopefully released eventually. GC.ReRegisterForFinalize(this); } }
أخطاء التنفيذ النموذجية
كما أوضحت لك أنه لا يوجد نمط عالمي لتطبيق IDisposable. علاوة على ذلك ، فإن بعض الاعتماد على التحكم التلقائي في الذاكرة يضلل الناس ويتخذون قرارات مربكة عند تنفيذ نمط ما. كامل .NET Framework مليء بالأخطاء في تنفيذه. لإثبات وجهة نظري ، دعونا ننظر إلى هذه الأخطاء باستخدام مثال .NET Framework بالضبط. تتوفر جميع التطبيقات من خلال: الاستخدامات القابلة للتعريف
فئة FileEntry cmsinterop.cs
هذا الرمز مكتوب على عجل لإغلاق المشكلة. من الواضح أن المؤلف أراد أن يفعل شيئًا لكنه غير رأيه وأبقى حلاً معيبًا
internal class FileEntry : IDisposable { // Other fields // ... [MarshalAs(UnmanagedType.SysInt)] public IntPtr HashValue; // ... ~FileEntry() { Dispose(false); } // The implementation is hidden and complicates calling the *right* version of a method. void IDisposable.Dispose() { this.Dispose(true); } // Choosing a public method is a serious mistake that allows for incorrect destruction of // an instance of a class. Moreover, you CANNOT call this method from the outside public void Dispose(bool fDisposing) { if (HashValue != IntPtr.Zero) { Marshal.FreeCoTaskMem(HashValue); HashValue = IntPtr.Zero; } if (fDisposing) { if( MuiMapping != null) { MuiMapping.Dispose(true); MuiMapping = null; } System.GC.SuppressFinalize(this); } } }
SemaphoreSlim فئة النظام / خيوط / SemaphoreSlim.cs
يوجد هذا الخطأ في أعلى أخطاء .NET Framework المتعلقة بـ IDisposable: SuppressFinalize للفئات التي لا يوجد فيها finalizer. انها شائعة جدا.
public void Dispose() { Dispose(true); // As the class doesn't have a finalizer, there is no need in GC.SuppressFinalize GC.SuppressFinalize(this); } // The implementation of this pattern assumes the finalizer exists. But it doesn't. // It was possible to do with just public virtual void Dispose() protected virtual void Dispose(bool disposing) { if (disposing) { if (m_waitHandle != null) { m_waitHandle.Close(); m_waitHandle = null; } m_lockObj = null; m_asyncHead = null; m_asyncTail = null; } }
استدعاء إغلاق + التخلص من بعض رمز المشروع NativeWatcher
في بعض الأحيان ، يسمي الناس كل من الإغلاق والتخلص هذا خطأ على الرغم من أنه لن ينتج عنه خطأ ، بينما لا ينتج عن التخلص الثاني استثناء.
في الواقع ، إغلاق هو نمط آخر لجعل الأمور أكثر وضوحا للناس. ومع ذلك ، فإنه جعل كل شيء أكثر وضوحا.
public void Dispose() { if (MainForm != null) { MainForm.Close(); MainForm.Dispose(); } MainForm = null; }
النتائج العامة
- IDposable هو معيار النظام الأساسي وجودة تنفيذه تؤثر على جودة التطبيق بأكمله. علاوة على ذلك ، في بعض الحالات ، يؤثر ذلك على سلامة تطبيقك الذي يمكن مهاجمته من خلال موارد غير مُدارة.
- يجب أن يكون تنفيذ IDisposable مثمرًا إلى أقصى حد. ينطبق هذا بشكل خاص على قسم الاستكمال ، الذي يعمل بالتوازي مع بقية الكود ، ويقوم بتحميل Garbage Collector.
- عند تطبيق IDisposable ، يجب ألا تستخدم Dispos () بشكل متزامن مع الأساليب العامة لفصل دراسي. لا يمكن أن يتماشى التدمير مع الاستخدام. يجب أخذ ذلك في الاعتبار عند تصميم نوع يستخدم كائن قابل للإزالة.
- ومع ذلك ، يجب أن تكون هناك حماية ضد استدعاء "التخلص ()" من خيطين في نفس الوقت. ينتج عن هذا العبارة التي تقول إن التخلص () يجب ألا ينتج عنه أخطاء.
- يجب فصل الأنواع التي تحتوي على موارد غير مُدارة عن الأنواع الأخرى. أعني إذا قمت بلف مورد غير مُدار ، فيجب عليك تخصيص نوع منفصل له. يجب أن يحتوي هذا النوع على الصيغة النهائية ويجب أن يتم توريثه من
SafeHandle / CriticalHandle / CriticalFinalizerObject
. سيؤدي فصل المسؤولية هذا إلى دعم محسّن لنظام الكتابة وسيعمل على تبسيط التنفيذ لتدمير مثيلات الأنواع من خلال Dispose (): لن تحتاج الأنواع التي تحتوي على هذا التطبيق إلى تطبيق أداة النهائية. - بشكل عام ، هذا النمط غير مريح في الاستخدام وكذلك في صيانة الكود. ربما ، يجب أن نستخدم نهج Inversion of Control عندما ندمر حالة الكائنات عبر نمط
Lifetime
. ومع ذلك ، سنتحدث عن ذلك في القسم التالي.
تمت ترجمة هذا الفصل من اللغة الروسية بالاشتراك مع المؤلفين والمترجمين المحترفين . يمكنك مساعدتنا في الترجمة من الروسية أو الإنجليزية إلى أي لغة أخرى ، في المقام الأول إلى الصينية أو الألمانية.
وأيضًا ، إذا كنت تريد شكراً منا ، فإن أفضل طريقة للقيام بذلك هي منحنا نجمًا على github أو لتخزين المستودع
github / sidristij / dotnetbook .