لماذا لا يتم الإعلان عن المتغيرات في "المحاولة" في النطاق في "التقاط" أو "أخيرًا"؟

StackOverflow https://stackoverflow.com/questions/94977

سؤال

في C# وفي Java (وربما لغات أخرى أيضًا)، لا تكون المتغيرات المعلنة في كتلة "try" في نطاق كتل "catch" أو "أخيرًا" المقابلة.على سبيل المثال، لا يتم ترجمة التعليمات البرمجية التالية:

try {
  String s = "test";
  // (more code...)
}
catch {
  Console.Out.WriteLine(s);  //Java fans: think "System.out.println" here instead
}

في هذا الكود، يحدث خطأ في وقت الترجمة عند الإشارة إلى s في كتلة الالتقاط، لأن s موجود فقط في النطاق في كتلة المحاولة.(في Java، خطأ الترجمة هو "لا يمكن حله"؛في C#، يكون "الاسم غير موجود في السياق الحالي".)

يبدو أن الحل العام لهذه المشكلة هو بدلاً من ذلك الإعلان عن المتغيرات قبل كتلة المحاولة مباشرة، بدلاً من داخل كتلة المحاولة:

String s;
try {
  s = "test";
  // (more code...)
}
catch {
  Console.Out.WriteLine(s);  //Java fans: think "System.out.println" here instead
}

ومع ذلك، على الأقل بالنسبة لي، (1) يبدو هذا حلاً صعبًا، و(2) يؤدي إلى أن يكون للمتغيرات نطاق أكبر مما أراده المبرمج (ما تبقى من الطريقة بالكامل، بدلاً من سياقها فقط). حاول الالتقاط أخيرًا).

سؤالي هو، ما هي الأسباب المنطقية وراء قرار تصميم اللغة هذا (في Java وC# و/أو في أي لغات أخرى قابلة للتطبيق)؟

هل كانت مفيدة؟

المحلول

شيئان:

  1. بشكل عام، تحتوي Java على مستويين فقط من النطاق:العالمية والوظيفة.ولكن، حاول/قبض على استثناء (لا يقصد التورية).عندما يتم طرح استثناء ويحصل كائن الاستثناء على متغير مخصص له، فإن متغير الكائن هذا يكون متاحًا فقط ضمن قسم "الالتقاط" ويتم إتلافه بمجرد اكتمال عملية الالتقاط.

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

    
    try
    {
        throw new ArgumentException("some operation that throws an exception");
        string s = "blah";
    }
    catch (e as ArgumentException)
    {  
        Console.Out.WriteLine(s);
    }
    

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

نصائح أخرى

كيف يمكنك التأكد من أنك وصلت إلى جزء الإعلان في كتلة الالتقاط الخاصة بك؟ماذا لو أن إنشاء مثيل يلقي الاستثناء؟

تقليديًا، في اللغات ذات النمط C، ما يحدث داخل الأقواس المتعرجة يبقى داخل الأقواس المتعرجة.أعتقد أن وجود عمر متغير يمتد عبر نطاقات كهذه سيكون أمرًا غير بديهي بالنسبة لمعظم المبرمجين.يمكنك تحقيق ما تريد من خلال إحاطة كتل المحاولة/الالتقاط/الأخيرة داخل مستوى آخر من الأقواس.على سبيل المثال

... code ...
{
    string s = "test";
    try
    {
        // more code
    }
    catch(...)
    {
        Console.Out.WriteLine(s);
    }
}

يحرر:أعتقد أن كل قاعدة يفعل لديك استثناء.ما يلي صالح لـ C++:

int f() { return 0; }

void main() 
{
    int y = 0;

    if (int x = f())
    {
        cout << x;
    }
    else
    {
        cout << x;
    }
}

نطاق x هو الشرط، ثم جملة، وعبارة أخرى.

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

static void TryCatchFinally()
{
    StreamReader sr = null;
    try
    {
        sr = new StreamReader(path);
        Console.WriteLine(sr.ReadToEnd());
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
    finally
    {
        if (sr != null)
        {
            sr.Close();
        }
    }
}

سيتم تجميع هذا إلى شيء مشابه لما يلي في MSIL:

.method private hidebysig static void  TryCatchFinallyDispose() cil managed
{
  // Code size       53 (0x35)    
  .maxstack  2    
  .locals init ([0] class [mscorlib]System.IO.StreamReader sr,    
           [1] class [mscorlib]System.Exception ex)    
  IL_0000:  ldnull    
  IL_0001:  stloc.0    
  .try    
  {    
    .try    
    {    
      IL_0002:  ldsfld     string UsingTest.Class1::path    
      IL_0007:  newobj     instance void [mscorlib]System.IO.StreamReader::.ctor(string)    
      IL_000c:  stloc.0    
      IL_000d:  ldloc.0    
      IL_000e:  callvirt   instance string [mscorlib]System.IO.TextReader::ReadToEnd()
      IL_0013:  call       void [mscorlib]System.Console::WriteLine(string)    
      IL_0018:  leave.s    IL_0028
    }  // end .try
    catch [mscorlib]System.Exception 
    {
      IL_001a:  stloc.1
      IL_001b:  ldloc.1    
      IL_001c:  callvirt   instance string [mscorlib]System.Exception::ToString()    
      IL_0021:  call       void [mscorlib]System.Console::WriteLine(string)    
      IL_0026:  leave.s    IL_0028    
    }  // end handler    
    IL_0028:  leave.s    IL_0034    
  }  // end .try    
  finally    
  {    
    IL_002a:  ldloc.0    
    IL_002b:  brfalse.s  IL_0033    
    IL_002d:  ldloc.0    
    IL_002e:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()    
    IL_0033:  endfinally    
  }  // end handler    
  IL_0034:  ret    
} // end of method Class1::TryCatchFinallyDispose

ماذا نرى؟تحترم MSIL الكتل - فهي في جوهرها جزء من التعليمات البرمجية الأساسية التي يتم إنشاؤها عند تجميع C# الخاص بك.النطاق ليس فقط محددًا بشكل صارم في مواصفات C#، بل هو أيضًا في مواصفات CLR وCLS.

يحميك النطاق، لكن يتعين عليك في بعض الأحيان التغلب عليه.مع مرور الوقت، تعتاد على ذلك، وتبدأ في الشعور بأنه طبيعي.وكما قال الجميع، ما يحدث في الكتلة يبقى في تلك الكتلة.هل تريد مشاركة شيء ما؟عليك أن تذهب خارج الكتل ...

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

كما أشار ravenspoint، يتوقع الجميع أن تكون المتغيرات محلية بالنسبة للكتل التي تم تعريفها فيها. try يقدم كتلة وهكذا يفعل catch.

إذا كنت تريد المتغيرات المحلية لكليهما try و catch, ، حاول تضمين كليهما في كتلة:

// here is some code
{
    string s;
    try
    {

        throw new Exception(":(")
    }
    catch (Exception e)
    {
        Debug.WriteLine(s);
    }
}

الجواب البسيط هو أن لغة C ومعظم اللغات التي ورثت بناء جملتها هي ذات نطاق حظر.هذا يعني أنه إذا تم تعريف المتغير في كتلة واحدة، أي داخل { }، فهذا هو نطاقه.

الاستثناء، بالمناسبة، هو جافا سكريبت، الذي يحتوي على بناء جملة مماثل، ولكن يتم تحديد نطاقه الوظيفي.في JavaScript، يكون المتغير المعلن في كتلة المحاولة موجودًا في نطاق كتلة الالتقاط، وفي كل مكان آخر في الوظيفة التي تحتوي عليها.

@burkhard لديه سؤال حول سبب الإجابة بشكل صحيح، ولكن كملاحظة أردت إضافتها، في حين أن مثال الحل الموصى به جيد بنسبة 99.9999+% من الوقت، إلا أنه ليس ممارسة جيدة، فمن الأكثر أمانًا التحقق من وجود قيمة فارغة قبل الاستخدام يتم إنشاء شيء ما داخل كتلة المحاولة، أو تهيئة المتغير لشيء ما بدلاً من مجرد الإعلان عنه قبل كتلة المحاولة.على سبيل المثال:

string s = String.Empty;
try
{
    //do work
}
catch
{
   //safely access s
   Console.WriteLine(s);
}

أو:

string s;
try
{
    //do work
}
catch
{
   if (!String.IsNullOrEmpty(s))
   {
       //safely access s
       Console.WriteLine(s);
   }
}

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

الجواب، كما أشار الجميع، هو إلى حد كبير "هذه هي الطريقة التي يتم بها تعريف الكتل".

هناك بعض المقترحات لجعل الكود أجمل.يرى ذراع

 try (FileReader in = makeReader(), FileWriter out = makeWriter()) {
       // code using in and out
 } catch(IOException e) {
       // ...
 }

الإغلاق ومن المفترض أن تعالج هذا أيضا.

with(FileReader in : makeReader()) with(FileWriter out : makeWriter()) {
    // code using in and out
}

تحديث: يتم تنفيذ ARM في Java 7. http://download.java.net/jdk7/docs/technotes/guides/language/try-with-resources.html

وفقًا للقسم الذي يحمل عنوان "كيفية طرح الاستثناءات والتقاطها" في الدرس الثاني من مجموعة التدريب الذاتي MCTS (امتحان 70-536):Microsoft® .NET Framework 2.0 — مؤسسة تطوير التطبيقات, والسبب هو أن الاستثناء ربما حدث قبل إعلانات المتغيرات في كتلة المحاولة (كما لاحظ آخرون بالفعل).

اقتباس من الصفحة 25:

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

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

يجب ببساطة أن تعمل كنطاقات منفصلة.

try
    dim i as integer = 10 / 0 ''// Throw an exception
    dim s as string = "hi"
catch (e)
    console.writeln(s) ''// Would throw another exception, if this was allowed to compile
end try

المتغيرات على مستوى الكتلة وتقتصر على كتلة المحاولة أو الالتقاط.يشبه تحديد متغير في عبارة if.فكر في هذا الوضع.

try {    
    fileOpen("no real file Name");    
    String s = "GO TROJANS"; 
} catch (Exception) {   
    print(s); 
}

لن يتم الإعلان عن السلسلة مطلقًا، لذلك لا يمكن الاعتماد عليها.

لأن كتلة المحاولة وكتلة الالتقاط عبارة عن كتلتين مختلفتين.

في الكود التالي، هل تتوقع أن تكون العناصر المحددة في الكتلة A مرئية في الكتلة B؟

{ // block A
  string s = "dude";
}

{ // block B
  Console.Out.WriteLine(s); // or printf or whatever
}

في المثال المحدد الذي قدمته، لا يمكن للتهيئة s أن تؤدي إلى استثناء.لذلك قد تعتقد أنه ربما يمكن توسيع نطاقها.

ولكن بشكل عام، يمكن لتعبيرات التهيئة طرح استثناءات.لن يكون من المنطقي أن يكون المتغير الذي ألقى مُبدئه استثناءً (أو تم الإعلان عنه بعد متغير آخر حيث حدث ذلك) في نطاق الالتقاط/أخيرًا.

كما ستتأثر إمكانية قراءة التعليمات البرمجية.القاعدة في لغة C (واللغات التي تتبعها، بما في ذلك C++ وJava وC#) بسيطة:النطاقات المتغيرة تتبع الكتل.

إذا كنت تريد أن يكون المتغير في نطاق المحاولة/الالتقاط/أخيرًا ولكن ليس في أي مكان آخر، فقم بتغليف الأمر برمته في مجموعة أخرى من الأقواس (كتلة مجردة) وأعلن عن المتغير قبل المحاولة.

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

على الأقل عندما يتم الإعلان عنه خارج كتلة المحاولة، فأنت تعرف على وجه اليقين ما يمكن أن يكون عليه المتغير على الأقل عند طرح استثناء؛قيمة المتغير قبل كتلة المحاولة.

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

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

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

كما أشار المستخدمون الآخرون، تحدد الأقواس المتعرجة النطاق في كل لغة نمط C التي أعرفها تقريبًا.

إذا كان متغيرًا بسيطًا، فلماذا تهتم بالمدة التي سيستغرقها في النطاق؟انها ليست صفقة كبيرة.

في C#، إذا كان متغيرًا معقدًا، فستحتاج إلى تنفيذ IDisposable.يمكنك بعد ذلك إما استخدام المحاولة/التقاط/أخيرًا واستدعاء obj.Dispose() في الكتلة النهائية.أو يمكنك استخدام الكلمة الأساسية use، والتي ستستدعي تلقائيًا قسم التخلص الموجود في نهاية الكود.

في Python تكون مرئية في الالتقاط/الحظر الأخير إذا لم يتم رمي السطر الذي يعلنها.

ماذا لو تم طرح الاستثناء في بعض التعليمات البرمجية الموجودة أعلى إعلان المتغير.مما يعني أن الإعلان نفسه لم يحدث في هذه الحالة.

try {

       //doSomeWork // Exception is thrown in this line. 
       String s;
       //doRestOfTheWork

} catch (Exception) {
        //Use s;//Problem here
} finally {
        //Use s;//Problem here
}

في حين أنه من الغريب أنه لا يعمل في مثالك، خذ هذا المثال المماثل:

    try
    {
         //Code 1
         String s = "1|2";
         //Code 2
    }
    catch
    {
         Console.WriteLine(s.Split('|')[1]);
    }

قد يتسبب هذا في قيام المصيد بإلقاء استثناء مرجعي فارغ في حالة تعطل الرمز 1.الآن، على الرغم من أن دلالات المحاولة/التقاط مفهومة جيدًا، إلا أن هذه ستكون حالة زاوية مزعجة، حيث يتم تعريف s بقيمة أولية، لذلك لا ينبغي من الناحية النظرية أن تكون فارغة أبدًا، ولكن في ظل الدلالات المشتركة، ستكون كذلك.

مرة أخرى يمكن إصلاح هذا من الناحية النظرية من خلال السماح فقط بالتعريفات المنفصلة (String s; s = "1|2";)، أو مجموعة أخرى من الشروط، ولكن من الأسهل عمومًا أن تقول لا.

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

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

{
     String s;
     try
     {
          s = "test";
          //More code
     }
     catch
     {
          Console.WriteLine(s);
     }
}

قد يكون تفكيري هو أنه نظرًا لأن شيئًا ما في كتلة المحاولة أدى إلى تشغيل الاستثناء، فلا يمكن الوثوق بمحتويات مساحة الاسم الخاصة به - أي أن الرجوع إلى String 's' في كتلة الالتقاط قد يتسبب في ظهور استثناء آخر.

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

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

// won't compile!
try
{
    VeryLargeArray v = new VeryLargeArray(TOO_BIG_CONSTANT); // throws OutOfMemoryException
    string s = "Help";
}
catch
{
    Console.WriteLine(s); // whoops!
}

يفرض عليك CLR (وبالتالي المترجم) أيضًا تهيئة المتغيرات قبل استخدامها.في كتلة الالتقاط المقدمة، لا يمكن ضمان ذلك.

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

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

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

على سبيل المثالال استخدام الكلمة الرئيسية مع يمكن التخلص منه الكائنات في:

using(Writer writer = new Writer())
{
    writer.Write("Hello");
}

يعادل:

Writer writer = new Writer();
try
{        
    writer.Write("Hello");
}
finally
{
    if( writer != null)
    {
        ((IDisposable)writer).Dispose();
    }
}

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

بدلاً من المتغير المحلي، يمكن الإعلان عن خاصية عامة؛يجب أن يؤدي هذا أيضًا إلى تجنب خطأ محتمل آخر لمتغير غير معين.سلسلة عامة S {احصل على؛تعيين؛}

ال مواصفات C# (15.2) ينص على أن "نطاق المتغير المحلي أو الثابت المعلن في الكتلة هو الكتلة."

(في المثال الأول الخاص بك، كتلة المحاولة هي الكتلة حيث يتم الإعلان عن "s")

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

ج#3.0:

string html = new Func<string>(() =>
{
    string webpage;

    try
    {
        using(WebClient downloader = new WebClient())
        {
            webpage = downloader.DownloadString(url);
        }
    }
    catch(WebException)
    {
        Console.WriteLine("Download failed.");  
    }

    return webpage;
})();
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top