
مقدمة
حان الوقت للحديث عن الاستثناءات أو المواقف الاستثنائية. قبل أن نبدأ ، دعونا ننظر إلى التعريف. ما هو الوضع الاستثنائي؟
هذا موقف يجعل تنفيذ التعليمات البرمجية الحالية أو اللاحقة غير صحيحة. أعني مختلفة عن الطريقة التي صمم بها أو قصد منها. مثل هذا الموقف يهدد سلامة التطبيق أو جزء منه ، على سبيل المثال كائن. أنه يجلب التطبيق إلى حالة غير عادية أو استثنائية.
ولكن لماذا نحتاج إلى تحديد هذه المصطلحات؟ لأنها سوف تبقينا في بعض الحدود. إذا لم نتبع المصطلحات ، فيمكننا الابتعاد عن المفهوم المصمم الذي قد يؤدي إلى العديد من المواقف الغامضة. دعونا نرى بعض الأمثلة العملية:
struct Number { public static Number Parse(string source) { // ... if(!parsed) { throw new ParsingException(); } // ... } public static bool TryParse(string source, out Number result) { // .. return parsed; } }
هذا المثال يبدو غريبا بعض الشيء ، وهذا لسبب ما. أنا جعلت هذا الرمز مصطنعًا قليلاً لإظهار أهمية المشكلات التي تظهر فيه. أولاً ، دعونا ننظر إلى طريقة Parse
. لماذا يجب أن يلقي استثناء؟
- لأن المعلمة التي تقبلها عبارة عن سلسلة ، ولكن ناتجها رقم ، وهو نوع قيمة. لا يمكن أن يشير هذا الرقم إلى صحة الحسابات: إنه موجود فقط. بمعنى آخر ، لا توجد وسيلة في واجهتها للتواصل مع مشكلة محتملة.
- من ناحية أخرى ، تتوقع الطريقة سلسلة صحيحة تحتوي على عدد ولا توجد أحرف زائدة عن الحاجة. إذا لم يتضمن ، فهناك مشكلة في المتطلبات الأساسية للطريقة: لقد مر الرمز الذي يستدعي طريقتنا ببيانات خاطئة.
وبالتالي ، يكون الموقف عندما تحصل هذه الطريقة على سلسلة تحتوي على بيانات غير صحيحة أمرًا استثنائيًا لأن الطريقة لا يمكنها إرجاع قيمة صحيحة ولا أي شيء. وبالتالي ، فإن الطريقة الوحيدة هي رمي استثناء.
يمكن أن يشير المتغير الثاني للأسلوب إلى بعض المشكلات المتعلقة ببيانات الإدخال: القيمة المرجعة هنا هي boolean
مما يشير إلى التنفيذ الناجح لهذه الطريقة. لا تحتاج هذه الطريقة إلى استخدام استثناءات للإشارة إلى أي مشاكل: حيث يتم تغطية جميعها بقيمة الإرجاع false
.
نظرة عامة
قد يبدو التعامل مع الاستثناءات سهلاً مثل ABC: نحتاج فقط إلى وضع مجموعات تجريبية وانتظار الأحداث المقابلة. ومع ذلك ، أصبح هذا البساطة ممكنًا بسبب العمل الهائل الذي قامت به فرق CLR و CoreCLR التي توحدت جميع الأخطاء التي تأتي من جميع الاتجاهات والمصادر في CLR. لفهم ما سوف نتحدث عنه بعد ذلك ، دعونا نلقي نظرة على الرسم التخطيطي:

يمكننا أن نرى أنه داخل .NET Framework الكبيرة ، يوجد عالمان: كل شيء ينتمي إلى CLR وكل شيء لا ، بما في ذلك جميع الأخطاء المحتملة التي تظهر في Windows وأجزاء أخرى من العالم غير الآمن.
- تعتبر معالجة الاستثناءات الهيكلية (SEH) طريقة قياسية لمعالجة Windows الاستثناءات. عندما يتم استدعاء أساليب
unsafe
ويتم طرح الاستثناءات ، يوجد تحويل CLR غير آمن للاستثناءات في كلا الاتجاهين: من غير الآمن إلى CLR والخلف. وذلك لأن CLR يمكنها استدعاء طريقة غير آمنة يمكنها استدعاء طريقة CLR بدورها. - تعد معالجة الاستثناءات المتجهات (VEH) من جذور SEH وتتيح لك وضع معالجاتك في الأماكن التي قد يتم طرح استثناءات فيها. على وجه الخصوص ، فإنه يستخدم لوضع
FirstChanceException
. - تظهر استثناءات COM + عندما يكون مصدر المشكلة مكون COM. في هذه الحالة ، يجب تحويل طبقة بين COM وأسلوب .NET خطأ COM إلى استثناء .NET.
- وبطبيعة الحال ، مغلفة ل HRESULT. يتم تقديمها لتحويل نموذج WinAPI (رمز الخطأ موجود في قيمة الإرجاع ، في حين يتم الحصول على قيم الإرجاع باستخدام معلمات الطريقة) إلى نموذج استثناءات لأنه استثناء قياسي في .NET.
من ناحية أخرى ، هناك لغات فوق CLI لكل منها أكثر أو أقل من وظائف لمعالجة الاستثناءات. على سبيل المثال ، كان في الآونة الأخيرة VB.NET أو F # وظيفة أكثر ثراءً في التعامل مع التعبير عنها في عدد من المرشحات التي لم تكن موجودة في C #.
رموز الإرجاع مقابل استثناء
بشكل منفصل ، يجب أن أذكر نموذجًا لمعالجة أخطاء التطبيق باستخدام رموز الإرجاع. فكرة إرجاع خطأ ببساطة واضحة وواضحة. علاوة على ذلك ، إذا تعاملنا مع الاستثناءات كمشغل goto
، يصبح استخدام رموز الإرجاع أكثر منطقية: في هذه الحالة ، يرى مستخدم الطريقة احتمال حدوث أخطاء ويمكنه فهم الأخطاء التي قد تحدث. ومع ذلك ، دعونا لا نخمن ما هو أفضل ولماذا ، ولكن ناقش مشكلة الاختيار باستخدام نظرية مسبب جيدًا.
لنفترض أن جميع الطرق لها واجهات للتعامل مع الأخطاء. ثم ستبدو جميع الأساليب:
public bool TryParseInteger(string source, out int result); public DialogBoxResult OpenDialogBox(...); public WebServiceResult IWebService.GetClientsList(...); public class DialogBoxResult : ResultBase { ... } public class WebServiceResult : ResultBase { ... }
واستخدامها سيبدو:
public ShowClientsResult ShowClients(string group) { if(!TryParseInteger(group, out var clientsGroupId)) return new ShowClientsResult { Reason = ShowClientsResult.Reason.ParsingFailed }; var webResult = _service.GetClientsList(clientsGroupId); if(!webResult.Successful) { return new ShowClientsResult { Reason = ShowClientsResult.Reason.ServiceFailed, WebServiceResult = webResult }; } var dialogResult = _dialogsService.OpenDialogBox(webResult.Result); if(!dialogResult.Successful) { return new ShowClientsResult { Reason = ShowClientsResult.Reason.DialogOpeningFailed, DialogServiceResult = dialogResult }; } return ShowClientsResult.Success(); }
قد تعتقد أن هذه الشفرة محمّلة بمعالجة الأخطاء. ومع ذلك ، أود منك أن تعيد النظر في موقفك: كل شيء هنا هو محاكاة لآلية تلقي الاستثناءات وتعالجها.
كيف يمكن لأي طريقة الإبلاغ عن مشكلة؟ يمكن أن تفعل ذلك باستخدام واجهة للإبلاغ عن الأخطاء. على سبيل المثال ، في طريقة TryParseInteger
يتم تمثيل هذه الواجهة بقيمة إرجاع: إذا كان كل شيء على ما يرام ، TryParseInteger
الطريقة إلى true
. إذا لم يكن موافق ، فسوف يعود false
. ومع ذلك ، هناك عيب هنا: يتم إرجاع القيمة الحقيقية عبر معلمة out int result
. العيب هو أن قيمة الإرجاع من ناحية منطقية ومن خلال التصور لها جوهر "قيمة الإرجاع" أكثر من تلك الخاصة بالمعلمة out
. من ناحية أخرى ، نحن لا نهتم دائمًا بالأخطاء ، بل إذا كانت السلسلة مخصصة لـ التحليل يأتي من خدمة أنشأتها هذه السلسلة ، لسنا بحاجة للتحقق من وجود أخطاء: ستكون السلسلة صحيحة دائمًا وجيدة للتحليل ، ولكن لنفترض أننا نتبع تطبيقًا آخر للأسلوب:
public int ParseInt(string source);
ثم ، هناك سؤال: إذا كانت السلسلة بها أخطاء ، فماذا يجب أن تفعل الطريقة؟ يجب أن يعود الصفر؟ لن يكون هذا صحيحًا: لا يوجد صفر في السلسلة. في هذه الحالة ، لدينا تضارب في المصالح: المتغير الأول يحتوي على الكثير من التعليمات البرمجية ، في حين أن المتغير الثاني لا يوجد لديه وسيلة للإبلاغ عن الأخطاء. ومع ذلك ، من السهل فعلاً تحديد وقت استخدام رموز الإرجاع ومتى تستخدم الاستثناءات.
إذا كان الحصول على خطأ معيارًا ، فاختر رمز الإرجاع. على سبيل المثال ، من الطبيعي أن تصادف خوارزمية تحليل النص أخطاء في نص ، ولكن إذا حصلت خوارزمية أخرى تعمل مع سلسلة محللة على خطأ من محلل ، فقد تكون حرجة أو ، بعبارة أخرى ، استثنائية.
جرب اللحاق بالاختصار
تغطي مجموعة try
قسمًا يتوقع فيه المبرمج الحصول على موقف حرج يتم التعامل معه كمعيار من خلال الكود الخارجي. بمعنى آخر ، إذا اعتبر رمز ما أن حالته الداخلية غير متسقة استنادًا إلى بعض القواعد ورمي استثناءً ، يمكن للنظام الخارجي ، الذي لديه رؤية أوسع للوضع نفسه ، التقاط هذا الاستثناء باستخدام كتلة catch
وتطبيع تنفيذ رمز التطبيق . وبالتالي ، يمكنك تقنين الاستثناءات في هذا القسم من التعليمات البرمجية عن طريق التقاطها . أعتقد أنها فكرة مهمة تبرر الحظر المفروض على التقاط جميع try-catch(Exception ex){ ...}
فقط في حالة .
هذا لا يعني أن الاستثناءات الجذرية تتناقض مع بعض الإيديولوجية. أقول أنه يجب عليك فقط التقاط الأخطاء التي تتوقعها من قسم معين من التعليمات البرمجية. على سبيل المثال ، لا يمكنك توقع جميع أنواع الاستثناءات الموروثة من ArgumentException
أو لا يمكنك الحصول على NullReferenceException
، لأنه غالبًا ما يعني وجود مشكلة في التعليمات البرمجية الخاصة بك بدلاً من وجودها في واحدة تسمى. لكن من المناسب توقع أنك لن تتمكن من فتح ملف مقصود. حتى لو كنت متأكدًا أنك ستكون قادرًا بنسبة 200٪ ، لا تنسَ التحقق.
finally
كتلة معروفة أيضا. انها مناسبة لجميع الحالات التي تغطيها كتل try-catch
. باستثناء العديد من المواقف الخاصة النادرة ، ستعمل هذه الكتلة دائمًا . لماذا تم تقديم مثل هذا الضمان للأداء؟ لتنظيف تلك الموارد ومجموعات الكائنات التي تم تخصيصها أو التقاطها في كتلة try
والتي تتحمل هذه الكتلة المسؤولية عنها.
غالبًا ما يتم استخدام هذه الكتلة بدون كتلة catch
عندما لا نهتم بالخطأ الذي خرق خوارزمية ، لكننا بحاجة إلى تنظيف جميع الموارد المخصصة لهذه الخوارزمية. دعونا نلقي نظرة على مثال بسيط: خوارزمية نسخ الملفات تحتاج إلى ملفين مفتوحين ونطاق ذاكرة لمخزن مؤقت للنقد. تخيل أننا خصصنا الذاكرة وفتحنا ملفًا واحدًا ، لكن لم نتمكن من فتح ملف آخر. لربط كل شيء في "معاملة" واحدة بشكل تلقائي ، نضع كل العمليات الثلاث في كتلة واحدة try
(كتغيير للتنفيذ) مع موارد يتم تنظيفها في finally
. قد يبدو كمثال مبسط ولكن الأهم هو إظهار الجوهر.
ما C # تفتقر فعلا هو كتلة fault
الذي يتم تفعيله كلما حدث خطأ. انها مثل finally
على المنشطات. إذا كان لدينا هذا ، فيمكننا ، على سبيل المثال ، إنشاء نقطة إدخال واحدة لتسجيل الحالات الاستثنائية:
try { //... } fault exception { _logger.Warn(exception); }
شيء آخر يجب أن أتطرق إليه في هذه المقدمة هو مرشحات الاستثناء. ليست ميزة جديدة على النظام الأساسي .NET ولكن قد يكون مطورو C # جديدًا عليها: ظهرت تصفية الاستثناء فقط في v. 6.0. يجب أن تعمل عوامل التصفية على تطبيع الموقف عندما يكون هناك نوع واحد من الاستثناء يجمع بين عدة أنواع من الأخطاء. يجب أن يساعدنا ذلك عندما نريد التعامل مع سيناريو معين ولكن يجب أن نلاحظ مجموعة كاملة من الأخطاء أولاً وتصفيتها لاحقًا. بالطبع ، أقصد رمز النوع التالي:
try { //... } catch (ParserException exception) { switch(exception.ErrorCode) { case ErrorCode.MissingModifier: // ... break; case ErrorCode.MissingBracket: // ... break; default: throw; } }
حسنًا ، يمكننا الآن إعادة كتابة هذا الرمز بشكل صحيح:
try { //... } catch (ParserException exception) when (exception.ErrorCode == ErrorCode.MissingModifier) { // ... } catch (ParserException exception) when (exception.ErrorCode == ErrorCode.MissingBracket) { // ... }
التحسن هنا ليس في عدم وجود switch
. أعتقد أن هذا التصميم الجديد أفضل في عدة أشياء:
when
استخدام التصفية ، نلاحظ بالضبط ما نريده وهو صحيح من حيث الأيديولوجية ؛- يصبح الرمز أكثر قابلية للقراءة في هذا النموذج الجديد. من خلال البحث عن الكود ، يمكن لعقلك أن يحدد الكتل اللازمة لمعالجة الأخطاء بسهولة أكبر لأنه يبحث في البداية عن
catch
وليس switch-case
؛ - آخر وليس آخراً: المقارنة الأولية هي قبل دخول كتلة الصيد. هذا يعني أنه إذا قمنا بتخمينات خاطئة بشأن المواقف المحتملة ، فإن هذا التصميم سيعمل بشكل أسرع من
switch
في حالة طرح استثناء مرة أخرى.
تقول العديد من المصادر أن الميزة المميزة لهذا الرمز هي أن التصفية تحدث قبل فك المكدس. يمكنك رؤية ذلك في المواقف التي لا توجد فيها مكالمات أخرى باستثناء المعتاد بين المكان الذي يتم فيه طرح استثناء والمكان الذي يحدث فيه فحص التصفية.
static void Main() { try { Foo(); } catch (Exception ex) when (Check(ex)) { ; } } static void Foo() { Boo(); } static void Boo() { throw new Exception("1"); } static bool Check(Exception ex) { return ex.Message == "1"; }

يمكنك أن ترى من الصورة أن تتبع المكدس لا يحتوي فقط على أول مكالمة من Main
كنقطة لجذب استثناء ، ولكن مكدس كامل قبل نقطة رمي استثناء بالإضافة إلى الثاني يدخل في Main
عبر رمز غير مدار. يمكننا أن نفترض أن هذا الرمز هو بالضبط رمز لإلقاء استثناءات في مرحلة التصفية واختيار معالج نهائي. ومع ذلك ، لا يمكن التعامل مع جميع المكالمات دون فك المكدس . أعتقد أن التوحيد المفرط للمنصة يولد الكثير من الثقة به. على سبيل المثال ، عندما يستدعي مجال ما أسلوبًا من مجال آخر ، يكون شفافًا تمامًا من حيث الشفرة. ومع ذلك ، فإن الطريقة التي تستدعي بها أساليب العمل هي قصة مختلفة تمامًا. سوف نتحدث عنها في الجزء التالي.
التسلسل
لنبدأ من خلال النظر في نتائج تشغيل التعليمة البرمجية التالية (لقد أضفت نقل مكالمة عبر الحدود بين مجالين للتطبيق).
class Program { static void Main() { try { ProxyRunner.Go(); } catch (Exception ex) when (Check(ex)) { ; } } static bool Check(Exception ex) { var domain = AppDomain.CurrentDomain.FriendlyName; // -> TestApp.exe return ex.Message == "1"; } public class ProxyRunner : MarshalByRefObject { private void MethodInsideAppDomain() { throw new Exception("1"); } public static void Go() { var dom = AppDomain.CreateDomain("PseudoIsolated", null, new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory }); var proxy = (ProxyRunner) dom.CreateInstanceAndUnwrap(typeof(ProxyRunner).Assembly.FullName, typeof(ProxyRunner).FullName); proxy.MethodInsideAppDomain(); } } }
يمكننا أن نرى أن تكديس الرقائق يحدث قبل أن نصل إلى التصفية. دعونا نلقي نظرة على لقطات. يتم أخذ الأول قبل إنشاء استثناء:

والثاني هو بعد ذلك:

دعونا دراسة تتبع المكالمات قبل وبعد تصفية الاستثناءات. ماذا يحدث هنا؟ يمكننا أن نرى أن مطوري المنصات قاموا بشيء يبدو للوهلة الأولى وكأنه حماية لنطاق فرعي. يتم قطع التتبع بعد الطريقة الأخيرة في سلسلة الاتصال وبعد ذلك يتم النقل إلى مجال آخر. لكنني أعتقد أن هذا يبدو غريبا. لفهم سبب حدوث ذلك ، دعونا نتذكر القاعدة الرئيسية للأنواع التي تنظم التفاعل بين المجالات. يجب أن ترث هذه الأنواع MarshalByRefObject
وتكون قابلة للتسلسل. ومع ذلك ، على الرغم من صرامة أنواع الاستثناء C # يمكن أن يكون من أي نوع. ماذا يعني ذلك؟ هذا يعني أن المواقف قد تحدث عندما يمكن اكتشاف استثناء داخل مجال فرعي في مجال رئيسي. أيضًا ، إذا كان كائن البيانات الذي يمكنه الدخول في موقف استثنائي يحتوي على بعض الطرق الخطيرة من حيث الأمان ، فيمكن استدعاؤه في مجال الأصل. لتجنب ذلك ، يتم إجراء تسلسل الاستثناء أولاً ثم يعبر الحدود بين مجالات التطبيق ويظهر مرة أخرى مع مكدس جديد. دعونا التحقق من هذه النظرية:
[StructLayout(LayoutKind.Explicit)] class Cast { [FieldOffset(0)] public Exception Exception; [FieldOffset(0)] public object obj; } static void Main() { try { ProxyRunner.Go(); Console.ReadKey(); } catch (RuntimeWrappedException ex) when (ex.WrappedException is Program) { ; } } static bool Check(Exception ex) { var domain = AppDomain.CurrentDomain.FriendlyName; // -> TestApp.exe return ex.Message == "1"; } public class ProxyRunner : MarshalByRefObject { private void MethodInsideAppDomain() { var x = new Cast {obj = new Program()}; throw x.Exception; } public static void Go() { var dom = AppDomain.CreateDomain("PseudoIsolated", null, new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory }); var proxy = (ProxyRunner)dom.CreateInstanceAndUnwrap(typeof(ProxyRunner).Assembly.FullName, typeof(ProxyRunner).FullName); proxy.MethodInsideAppDomain(); } }
بالنسبة إلى رمز C # يمكن أن يلقي استثناءًا من أي نوع (لا أريد أن أتعرض للتعذيب باستخدام MSIL) لقد أجريت خدعة في هذا المثال بإلقاء نوع على نوع غير قابل للمقارنة ، لذلك يمكننا طرح استثناء من أي نوع ، ولكن المترجم أعتقد أننا نستخدم نوع Exception
. نقوم بإنشاء مثيل لنوع Program
، وهو أمر غير قابل للتسلسل بالتأكيد ، ونلقي استثناءًا باستخدام هذا النوع كحمل عمل. والخبر السار هو أنك تحصل على برنامج مجمّع باستثناءات غير استثناء لـ RuntimeWrappedException
التي ستقوم بتخزين مثيل لكائن نوع Program
الداخل وسنكون قادرين على الحصول على هذا الاستثناء. ومع ذلك ، هناك أخبار سيئة تدعم فكرتنا: استدعاء proxy.MethodInsideAppDomain();
سيتم إنشاء SerializationException
:

وبالتالي ، لا يمكنك نقل مثل هذا الاستثناء بين المجالات لأنه لا يمكن إجراء تسلسل له. هذا ، بدوره ، يعني أن استخدام عوامل تصفية الاستثناء لطرق الالتفاف في مجالات أخرى سيؤدي على أي حال إلى تكديس unrolling على الرغم من أن التسلسل يبدو غير ضروري مع إعدادات FullTrust
فرعي.
يجب أن نولي اهتمامًا إضافيًا لسبب ضرورة إجراء تسلسل بين النطاقات. في مثالنا المصطنع ، نقوم بإنشاء نطاق فرعي لا يحتوي على أي إعدادات. وهذا يعني أنه يعمل بطريقة FullTrust. تثق CLR في محتواها تمامًا ولا تقوم بتشغيل أي عمليات فحص إضافية. ومع ذلك ، عند إدراج إعداد أمان واحد على الأقل ، ستختفي الثقة الكاملة وسيبدأ CLR في التحكم في كل ما يحدث داخل نطاق فرعي. لذلك ، عندما يكون لديك مجال موثوق به بالكامل ، فلن تحتاج إلى إجراء تسلسل. أعترف ، نحن لسنا بحاجة لحماية أنفسنا. ولكن التسلسل موجود ليس فقط للحماية. يقوم كل مجال بتحميل جميع التجميعات اللازمة مرة ثانية وإنشاء نسخها. وبالتالي ، فإنه يخلق نسخ من جميع الأنواع وجميع VMTs. بالطبع ، عند تمرير كائن من مجال إلى مجال ، ستحصل على نفس الكائن. لكن VMTs الخاصة به لن تكون خاصة به ولا يمكن تحويل هذا الكائن إلى نوع آخر. بمعنى آخر ، إذا أنشأنا Boo
نوع Boo
وحصلنا عليه في مجال آخر ، فلن يعمل صب (Boo)boo
. في هذه الحالة ، سيؤدي التسلسل وإلغاء التسلسل إلى حل المشكلة لأن الكائن موجود في مجالين في وقت واحد. سيكون موجودًا مع جميع بياناته حيث تم إنشاؤه وسيكون موجودًا في مجال الاستخدام ككائن وكيل ، مما يضمن أن أساليب الكائن الأصلي تسمى.
عن طريق نقل كائن متسلسل بين المجالات ، تحصل على نسخة كاملة من الكائن من مجال واحد في مجال آخر مع الاحتفاظ ببعض الترسيم في الذاكرة. ومع ذلك ، فإن ترسيم الحدود هذا خيالي. يتم استخدامه فقط لتلك الأنواع غير الموجودة في Shared AppDomain
. وبالتالي ، إذا رميت شيئًا غير قابل للتسلسل كاستثناء ، ولكن من تطبيق Shared AppDomain
، فلن تحصل على خطأ في إجراء تسلسل (يمكننا محاولة رمي Action
بدلاً من Program
). ومع ذلك ، ستحدث عملية إلغاء المكدس على أي حال في هذه الحالة: حيث يجب أن يعمل كلا الخيارين بطريقة قياسية. بحيث لن يتم الخلط بين أحد.
تمت ترجمة هذا الفصل من اللغة الروسية بالاشتراك مع المؤلفين والمترجمين المحترفين . يمكنك مساعدتنا في الترجمة من الروسية أو الإنجليزية إلى أي لغة أخرى ، في المقام الأول إلى الصينية أو الألمانية.
وأيضًا ، إذا كنت تريد شكراً منا ، فإن أفضل طريقة للقيام بذلك هي منحنا نجمًا على github أو لتخزين المستودع
github / sidristij / dotnetbook .