النمط القابل للتصرف (مبدأ التصميم القابل للتصرف) نقطة 1


نمط المتاح (مبدأ التصميم المتاح)


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


إذا سألت ما الذي يمكن معرفته ، فستقول بالتأكيد أنه كذلك


public interface IDisposable { void Dispose(); } 

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


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

وأيضًا ، إذا كنت تريد شكراً منا ، فإن أفضل طريقة للقيام بذلك هي منحنا نجمًا على github أو لتخزين المستودع github / sidristij / dotnetbook .

هناك اعتقاد خاطئ بأن IDisposable يعمل على إطلاق الموارد غير المدارة. هذا صحيح جزئياً فقط وللفهم ، تحتاج فقط إلى تذكر أمثلة الموارد غير المدارة. هل فئة File مورد غير مُدار؟ لا. ربما DbContext هو مورد غير مدار؟ لا ، مرة أخرى. مورد غير مُدار هو شيء لا ينتمي إلى نظام نوع .NET. شيء لم تنشئه المنصة ، شيء موجود خارج نطاقه. مثال بسيط هو مقبض ملف مفتوح في نظام التشغيل. المقبض هو رقم يحدد بشكل فريد ملفًا مفتوحًا - لا ، ليس بواسطتك - بواسطة نظام التشغيل. أي أن جميع هياكل التحكم (مثل موضع ملف في نظام الملفات ، وشظايا الملف في حالة التجزئة وغيرها من معلومات الخدمة ، وأرقام الاسطوانة ، الرأس أو قطاع محرك الأقراص الصلبة) موجودة داخل نظام تشغيل ولكن ليس منصة. NET. المورد غير المُدار الوحيد الذي يتم تمريره إلى النظام الأساسي .NET هو رقم IntPtr. يتم التفاف هذا الرقم بواسطة FileSafeHandle ، والذي بدوره يتم لفه بواسطة ملف فئة. يعني أن فئة الملف ليست موردًا غير مُدار من تلقاء نفسها ، ولكنها تستخدم طبقة إضافية في شكل IntPtr لتضمين مورد غير مُدار - مقبض الملف المفتوح. كيف تقرأ هذا الملف؟ باستخدام مجموعة من الطرق في WinAPI أو Linux OS.


المثال الأول لمصادر غير مُدارة هو التزامن بدائل التزامن في برامج متعددة مؤشرات الترابط أو متعددة المعالجات. هنا تنتمي إلى صفائف البيانات التي يتم تمريرها من خلال P / Invoke وأيضًا المزامير أو الإشارات.


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

حسنا الآن غطينا الموارد غير المدارة. لماذا نحتاج إلى استخدام IDisposable في هذه الحالات؟ لأن .NET Framework ليس لديه فكرة عما يحدث خارج منطقته. إذا قمت بفتح ملف باستخدام OS API ، فلن يعرف .NET أي شيء عنه. إذا قمت بتخصيص نطاق ذاكرة لاحتياجاتك الخاصة (على سبيل المثال باستخدام VirtualAlloc) ، فلن يعرف .NET أي شيء أيضًا. إذا لم تكن تعرف ذلك ، فلن تُصدر الذاكرة التي تشغلها مكالمة VirtualAlloc. أو لن يغلق ملفًا تم فتحه مباشرةً عبر مكالمة OS OS. هذه يمكن أن تسبب عواقب مختلفة وغير متوقعة. يمكنك الحصول على OutOfMemory إذا قمت بتخصيص الكثير من الذاكرة دون تحريرها (على سبيل المثال فقط عن طريق تعيين مؤشر إلى قيمة خالية). أو ، إذا قمت بفتح ملف على مشاركة ملف من خلال نظام التشغيل دون إغلاقه ، فستقفل الملف على مشاركة الملف لفترة طويلة. يعتبر مثال مشاركة الملفات جيدًا بشكل خاص حيث سيظل القفل على جانب IIS حتى بعد إغلاق اتصال بالخادم. ليس لديك حقوق لإطلاق القفل وعليك أن تطلب من المسؤولين أداء iisreset أو إغلاق المورد يدويًا باستخدام برنامج خاص.
يمكن أن تصبح هذه المشكلة على خادم بعيد مهمة معقدة لحلها.


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


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


الطريقة الأولى هي التفاف مثيل using(...){ ... } . هذا يعني أنك تعلّم إتلاف كائن بعد انتهاء الكتلة المرتبطة باستخدام ، أي استدعاء Dispose (). الطريقة الثانية هي تدمير الكائن ، عندما ينتهي عمره ، مع الإشارة إلى الكائن الذي نريد إطلاقه. لكن .NET ليس لديه سوى طريقة اللمسات الأخيرة التي تنطوي على التدمير التلقائي للكائن ، أليس كذلك؟ ومع ذلك ، فإن اللمسات الأخيرة ليست مناسبة على الإطلاق لأننا لا نعرف متى سيتم استدعاؤها. في هذه الأثناء ، نحتاج إلى إطلاق كائن في وقت معين ، على سبيل المثال فقط بعد الانتهاء من العمل باستخدام ملف مفتوح. لهذا السبب نحتاج أيضًا إلى تطبيق IDisposable وندعو Dispos لإطلاق جميع الموارد التي نملكها. وبالتالي ، نحن نتبع البروتوكول ، وهذا مهم للغاية. لأنه إذا اتبعه شخص ما ، يجب على جميع المشاركين فعل نفس الشيء لتجنب المشاكل.


طرق مختلفة لتنفيذ IDisposable


دعونا نلقي نظرة على تطبيقات IDisposable من البسيط إلى المعقد. الأول والأبسط هو استخدام IDisposable كما هو:


 public class ResourceHolder : IDisposable { DisposableResource _anotherResource = new DisposableResource(); public void Dispose() { _anotherResource.Dispose(); } } 

هنا ، نقوم بإنشاء مثيل لمورد تم إصداره بواسطة Dispos (). الشيء الوحيد الذي يجعل هذا التطبيق غير متناسق هو أنه لا يزال بإمكانك العمل مع المثيل بعد تدميره بواسطة Dispose() :


 public class ResourceHolder : IDisposable { private DisposableResource _anotherResource = new DisposableResource(); private bool _disposed; public void Dispose() { if(_disposed) return; _anotherResource.Dispose(); _disposed = true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } } 

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


 public class FileWrapper : IDisposable { IntPtr _handle; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { CloseHandle(_handle); } [DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)] private static extern IntPtr CreateFile(String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError=true)] private static extern bool CloseHandle(IntPtr hObject); } 

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


دعنا نقول على الفور لماذا قد يحدث هذا الخطأ:


  • إذا استخدمنا استخدام using(obj) { ... } ، فقد يظهر استثناء في مقطع داخلي من التعليمات البرمجية. يتم اكتشاف هذا الاستثناء بواسطة كتلة أخيرة ، والتي لا يمكننا رؤيتها (هذا هو السكر النحوي لـ C #). يستدعي هذا الحظر التخلص ضمنيًا. ومع ذلك ، هناك حالات عندما لا يحدث هذا. على سبيل المثال ، لا يتم catch أو التقاطها finally StackOverflowException . يجب أن تتذكر هذا دائمًا. لأنه إذا أصبحت بعض مؤشرات الترابط متكررة StackOverflowException في مرحلة ما ، فسينسى .NET الموارد التي استخدمها ولكن لم يتم إصدارها. لا يعرف كيفية تحرير الموارد غير المدارة. سيبقون في الذاكرة حتى يصدرهم نظام التشغيل ، أي عند الخروج من أحد البرامج ، أو حتى بعد مرور بعض الوقت على انتهاء التطبيق.
  • إذا اتصلنا Dispose () من Dispos () آخر. مرة أخرى ، قد يحدث فشلنا في الوصول إليه. ليس هذا هو الحال بالنسبة لمطور التطبيق الغائب ، الذي نسي استدعاء Dispose (). إنها مسألة الاستثناءات. ومع ذلك ، هذه ليست فقط الاستثناءات التي تعطل مؤشر ترابط تطبيق. نحن هنا نتحدث عن جميع الاستثناءات التي ستمنع خوارزمية من استدعاء التخلص الخارجي () الذي سيطلق على التخلص لدينا ().

كل هذه الحالات سوف تخلق موارد غير مدارة مع وقف التنفيذ. ذلك لأن جامع القمامة لا يعرف أنه يجب عليه جمعها. كل ما يمكن القيام به عند الفحص التالي هو اكتشاف أن المرجع الأخير إلى رسم بياني FileWrapper بنوع FileWrapper قد فقد. في هذه الحالة ، سيتم إعادة تخصيص الذاكرة للكائنات ذات المراجع. كيف يمكننا منع ذلك؟


يجب أن ننفذ صيغة نهائية للكائن. تم تسمية "finalizer" بهذه الطريقة عن قصد. إنه ليس مدمرًا لأنه قد يبدو بسبب طرق مماثلة لاستدعاء عناصر نهائية في C # ومدمرات في C ++. الفرق هو أنه سيتم استدعاء أداة الاستكمال النهائي على أي حال ، على عكس المخرب (وكذلك Dispose() ). يتم استدعاء finalizer عند بدء Garbage Collection (الآن يكفي معرفة ذلك ، ولكن الأمور أكثر تعقيدًا بعض الشيء). يتم استخدامه لإصدار مضمون للموارد إذا حدث خطأ ما . يجب علينا تطبيق أداة نهائية لإطلاق الموارد غير المُدارة. مرة أخرى ، لأنه يتم استدعاء النهائي في بداية GC ، نحن لا نعرف متى يحدث هذا بشكل عام.


دعنا نوسع كودنا:


 public class FileWrapper : IDisposable { IntPtr _handle; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { InternalDispose(); GC.SuppressFinalize(this); } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods } 

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


 public class FileWrapper : IDisposable { IntPtr _handle; bool _disposed; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { if(_disposed) return; _disposed = true; InternalDispose(); GC.SuppressFinalize(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods } 

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


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


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


لماذا هذا مهم؟ لماذا تنفق الكثير من الجهد على أولئك الذين يزولون؟ لأنه يمكن تعليق الموارد غير المدارة في النظام لفترة طويلة. حتى بعد إعادة تشغيل جهاز الكمبيوتر. إذا قام المستخدم بفتح ملف من مشاركة ملف في التطبيق الخاص بك ، فسيتم تأمين السابق بواسطة مضيف بعيد وإصداره في المهلة أو عند تحرير أحد الموارد عن طريق إغلاق الملف. إذا تعطل تطبيقك عند فتح الملف ، فلن يتم إصداره حتى بعد إعادة التشغيل. يجب عليك الانتظار لفترة طويلة حتى يقوم المضيف البعيد بإصداره. أيضا ، يجب أن لا تسمح الاستثناءات في النهائي. يؤدي هذا إلى تعجيل تسريع CLR وتطبيق لأنه لا يمكنك التفاف مكالمة النهائية في محاولة ... catch . أعني ، عندما تحاول إطلاق مورد ، يجب أن تكون متأكدًا من إمكانية إصداره. الحقيقة الأخيرة وليس الأقل أهمية: إذا قام CLR بإلغاء تحميل مجال بشكل غير طبيعي ، فسيتم أيضًا استدعاء "finalizers" للأنواع المشتقة من CriticalFinalizerObject ، على عكس تلك الموروثة مباشرة من الكائن .


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

وأيضًا ، إذا كنت تريد أن تقول "شكرًا" ، فإن أفضل طريقة يمكنك اختيارها هي منحنا نجمًا على github أو مستودع forking https://github.com/sidristij/dotnetbook

Source: https://habr.com/ru/post/ar443958/


All Articles