رضوى العربي

التقاط الاعتراضات (Catching)

يُمكن للشيفرة -بل يَنبغي لها- أن تُبلِّغ عن اعتراضات (Exceptions) في بعض الظروف الاستثنائية. مثلًا:

ينبغي لمُستَدعِي ما (caller) التقاط (catch) الاعتراضات المُحتملة والتعامل معها فقط عندما:

  • يستطيع إصلاح الظرف الاستثنائي أو التعافي منه بشكل مناسب.
  • إضافة معلومات عن السياق الذي حَدَثَ خلاله الاعتراض، قبل أن يَقوم بإعادة التبلِّيغ عنه مرة أُخْرى. (تُلْتقَط الاعتراضات المُعاد إلقائها عن طريق مُلتقِطي الاعتراضات بمُستوى أعلى بمَكْدَس الاستدعاءات call stack)

إذا أردت التقاط الاعتراضات المُلقاة من شيفرة معينة، اِستخدِم الصيغة التالية:

try 
{ 

} 
catch (ExceptionType) 
{ 

}‎

تُتضمَّن الشيفرة ذاتها داخل كتلة try، بينما تُتضمَّن الشيفرة المسئولة عن التقاط اعتراض معين والتعامل معه بكتلة catch الخاصة به، كالمثال التالي:

Console.Write("Please enter a filename: ");
string filename = Console.ReadLine();
Stream fileStream;

try
{
    fileStream = File.Open(filename);
}
catch (FileNotFoundException)
{
    Console.WriteLine("File '{0}' could not be found.", filename);
}

لا يُعدّ عدم التقاط اعتراض معين خطأً بالضرورة، إذا كنت تنوي التقاطه والتعامل معه بمُستوى أعلى بمَكْدَس الاستدعاءات (call stack).

إعادة التبليغ (Re-throwing)

إذا أردت تَنفيذ أمر مُعين في حالة حدوث اعتراض، ولكنك في نفس الوقت لا تستطيع إكمال تَّنفيذ الشيفرة بسبب هذا الاعتراض، فببساطة يُمكنك التقاط الاعتراض ثم إعادة التبليغ عنه بعد تَّنفيذ الأمر الذي تُريده، وبذلك سيلتقِطه مُلتقِط الاعتراضات التالي بحسب مَكْدَس الاستدعاءات (call stack).

هناك عدة طرائق للقيام بذلك، بعضها جيد والآخر سيء:

في المثال التالي، سيُبلَّغ عن اعتراض من نوع DivideByZeroException. تَلتقِط كتلة catch الاعتراض وتَستخدِم الكلمة المفتاحية throw -بمُفردها وبدون تحدّيد قيمة الاعتراض- لإعادة التبليغ عن الاعتراض الذي قد اِلتَقَطته للتو. تُعدّ هذه الطريقة سليمة حيث يتم فيها الاحتفاظ بتعقبات المكدس (stack trace) بشكل سليم.

private static void AskTheUltimateQuestion()
{
    try
    {
        var x = 42;
        var y = x / (x - x); // will throw a DivideByZeroException
    }
    catch (DivideByZeroException)
    {
        Console.WriteLine("Dividing by zero would destroy the universe.");
        throw;
    }
}

static void Main()
{
    try
    {
        AskTheUltimateQuestion();
    }
    catch
    {
    }
}

كالمثال السابق، تَلتقِط كتلة catch الاعتراض، لكن هنا تُعِيد التبليغ عنه باستخدام throw ex. في الواقع، رغم شيوع هذه الطريقة فهي غير سليمة؛ لأنها تُغيّر من تعقبات المكدس (stack trace)، التي ستُشير الآن إلى السطر الذي أعاد التبليغ عن الاعتراض بدلًا من الإشارة إلى المكان الأصلي المُتسبب به، لذلك لا تَستخدِم هذه الطريقة.

private static void AskTheUltimateQuestion()
{
    try
    {
        var x = 42;
        var y = x / (x - x); // will throw a DivideByZeroException

        Console.WriteLine("The secret to life, the universe, and everything is {1}", y);
    }
    catch (Exception ex)
    {
        Console.WriteLine("Something else horrible happened. The exception: " + ex.Message);

        throw ex;
    }
}

static void Main()
{
    try
    {
        AskTheUltimateQuestion();
    }
    catch
    {
    }
}

في الأمثلة السابقة، أُعيدّ التبليغ عن اعتراضات من نفس النوع، أما إذا أردت أن تُغيّر نوع الاعتراض مع تَضمِين الاعتراض الأصلي بداخله، استخدِم التعبير throw new ExceptionType. في هذه الحالة، ستُشير تعقبات المكدس (stack trace) إلى السطر المُتسبب بالاعتراض الخارجي. ولكن يُمكنك بسهولة استخدام الخاصية InnerException للولوج إلى تعقبات المكدس (stack trace) الخاصة بالاعتراض الداخلي والتي ستُشير إلى السطر الأصلي المُتسبب به. كالمثال التالي:

private static void AskTheUltimateQuestion()
{
    try
    {
        var x = 42;
        Console.WriteLine("The secret to life, the universe, and everything is {1}", y);
    }
    catch (FormatException ex)
    {
        throw new InvalidOperationException("Watch your format string indexes.", ex);
    }
}

static void Main()
{
    try
    {
        AskTheUltimateQuestion();
    }
    catch
    {
    }
}

ترشيح الاعتراضات

تُرَشَح الاعتراضات بناءً على النوع. مثلًا في المثال التالي، إذا بُلِّغ عن اعتراض ما فسيتم تَّنفيذ أول كتلة catch مُتوافقة مع نوع الاعتراض. يُمكنك إضافة كتلة catch بالنهاية للتعامل مع النوع Exception، بحيث يُلجَئ إليها إذا لم تَتوافق معه كتلة أُخْرى أكثر تحدّيدًا:

private static void AskTheUltimateQuestion()
{
    try
    {
        var x = 42;
        var y = x / (x - x); 
        Console.WriteLine("The secret to life, the universe, and everything is {1}", y);
    }
    catch (DivideByZeroException)
    {
    }
    catch (FormatException ex)
    {
    }
    catch (Exception ex)
    {
    }
}

static void Main()
{
    try
    {
        AskTheUltimateQuestion();
    }
    catch
    {
    }
}

لا تُرَشَح الاعتراضات فقط بناءً على النوع، فبدءًا من الإصدار 6 للغة c#‎، يُرشِح العَامِل when الاعتراضات بناءً على خاصياتها كذلك. كالتالي:

try
{
    // ...
}
catch (Exception e) when (e.InnerException != null) // Any condition can go in here.
{
    // ...
}

يُشبه ذلك استخدام جمل شرطية بداخل كتلة catch، لكن دون التسبب بأي تَغيِير بالمَكْدَس في حالة عدم تَحقُّق الشرط.

إعادة التبليغ عن اعتراض بواسطة تابع آخر

قد تَحتاج أحيانًا إلى التقاط اعتراض وإعادة التبليغ عنه بواسطة تابع أو خيط (thread) آخر، وفي ذات الوقت تريد أن تحتفظ برصة التتبع الأصلية. يُستخدَم التابع ExceptionDispatchInfo.Capture للقيام بذلك كالتالي:

using System.Runtime.ExceptionServices;
void Main()
{
    ExceptionDispatchInfo capturedException = null;
    try
    {
        throw new Exception();
    }
    catch (Exception ex)
    {
        capturedException = ExceptionDispatchInfo.Capture(ex);
    }

    Foo(capturedException);
}
void Foo(ExceptionDispatchInfo exceptionDispatchInfo)
{
    // Do stuff
    if (capturedException != null)
    {
        // Exception stack trace will show it was thrown from Main() and not from Foo()
        exceptionDispatchInfo.Throw();
    }
}

استخدام كتلة finally

تُنفَّذ كتلة finally {...}‎ دائما بغض النظر عما إذا كان قد تم التبليغ عن اعتراض أم لا أثناء تَّنفيذ الشيفرة الموجودة بكتلة try {...}‎ -يُستثني من ذلك حدوث اعتراض من نوع StackOverflowException أو إذا اُستدْعِيَ التابع Environment.FailFast()‎-، لذلك هي ملائمة لتحرير أيّ موارد (resources) حُجزت بكتلة try بطريقة آمنة.

Console.Write("Please enter a filename: ");
string filename = Console.ReadLine();
Stream fileStream = null;
try
{
    fileStream = File.Open(filename);
}
catch (FileNotFoundException)
{
    Console.WriteLine("File '{0}' could not be found.", filename);
}
finally
{
    if (fileStream != null)
    {
        fileStream.Dispose();
    }
}

ترجمة -وبتصرف- للفصل Exceptions والفصل ForEach من كتاب ‎.NET Framework Notes for Professionals





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن