Чому змінні, задекларовані в "спробувати", в області "улову" або "нарешті"?


139

У C # і в Java (і, можливо, в інших мовах) змінні, оголошені в блоці "спробувати", не знаходяться в межах відповідних блоків "catch" або "нарешті". Наприклад, наступний код не компілюється:

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

У цьому коді помилка часу компіляції виникає при посиланні на s у блоці вилову, оскільки s є лише в області дії у блоці спробу. (У Java помилка компіляції "s не може бути усунена"; у C # це "ім'я 's" не існує в поточному контексті ".)

Загальним рішенням цієї проблеми, здається, є замість оголошення блоку змін перед блоком спробу, а не в блоці спробу:

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

Однак, принаймні, мені (1) це здається незграбним рішенням, і (2) це призводить до того, що змінні мають більший обсяг, ніж планував програміст (весь залишок методу, а не лише в контексті пробувати-нарешті).

Моє запитання полягає в тому, які обґрунтування (-ла), що лежать в основі цього мовного рішення (на Java, в C # та / або будь-яких інших застосовних мовах)?

Відповіді:


171

Дві речі:

  1. Взагалі, Java має лише два рівні сфери: глобальний та функціональний. Але спробувати / catch - виняток (каламбур не призначений). Коли викид виключається, а об'єкт винятку отримує присвоєну йому змінну, ця змінна об'єкт доступна лише в розділі "catch" і руйнується, як тільки видобуток завершується.

  2. (і що ще важливіше). Ви не можете знати, куди в блок спробу було викинуто виняток. Можливо, це було раніше, ніж ваша змінна була оголошена. Тому неможливо сказати, які змінні будуть доступні для пункту catch / нарешті. Розглянемо наступний випадок, коли проведення масштабування, як ви запропонували:

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

Очевидно, це проблема - коли ви дістанетеся до обробника винятків, s не буде оголошено. Зважаючи на те, що улов призначений для вирішення виняткових обставин, і, нарешті, потрібно виконати, будучи безпечним і оголосивши про це проблемою під час компіляції, набагато краще, ніж під час виконання.


55

Як ви могли бути впевнені, що ви дійшли до деклараційної частини у вашому блоці вилову? Що робити, якщо екземпляр кидає виняток?


6
Так? Змінні декларації не кидають винятків.
Джошуа

6
Домовились, виняток може кинути випадок.
Буркгард

19

Традиційно в мовах стилю С те, що відбувається всередині фігурних брекетів, залишається всередині фігурних брекетів. Я думаю, що тривалість роботи змінної розтяжки на таких областях була б неінтуїтивною для більшості програмістів. Ви можете досягти того, що хочете, додавши блоки "try / catch / нарешті" всередині іншого рівня дужок. напр

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

EDIT: Я думаю , кожне правило має є виняток. Наступне дійсне на C ++:

int f() { return 0; }

void main() 
{
    int y = 0;

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

Обсяг x - умовний, тодішній і інший.


10

Всі інші підняли основи - те, що відбувається в блоці, залишається в блоці. Але у випадку .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.

Розмах захищає вас, але вам іноді доводиться обходити це. З часом ти звикаєш до цього, і він починає відчувати себе природним. Як і всі інші казали, те, що відбувається в блоці, залишається в цьому блоці. Ви хочете чимось поділитися? Ви повинні вийти за межі блоків ...


8

У будь-якому випадку C ++ сфера застосування автоматичної змінної обмежена фігурними дужками, які її оточують. Чому хтось очікує, що це буде по-іншому, скорочуючи спробуйте ключове слово поза фігурними дужками?


1
Домовились; "}" означає кінець області. Однак try-catch - нарешті, незвичний тим, що після пробного блоку ви повинні мати улов та / або остаточно заблокувати; Таким чином, виняток із звичайного правила, коли область блоку спробу, що переноситься на асоційований улов / нарешті, може здатися прийнятною?
Джон Шнайдер

7

Як і вказується на ravenspoint, кожен очікує, що змінні будуть локальними для блоку, в якому він визначений. Вводить tryблок і так це робиться catch.

Якщо ви хочете, щоб змінні були локальними для обох tryі catch, спробуйте вкласти обидва в блок:

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

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

5

Проста відповідь полягає в тому, що C і більшість мов, які успадкували його синтаксис, мають блок-діапазон. Це означає, що якщо змінна визначена в одному блоці, тобто всередині {}, то це її область.

Виняток, до речі, - JavaScript, який має подібний синтаксис, але функціонує. В JavaScript змінна, оголошена в блоці спробу, знаходиться в області блоку вилову, і всюди в її функції, що містить.


4

@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);
   }
}

Це має забезпечити масштабованість у вирішенні, так що навіть коли те, що ви робите в блоці спробу, складніше, ніж призначати рядок, ви зможете безпечно отримувати доступ до даних вашого блоку вилову.


4

Відповідно до розділу під назвою "Як кинути та виловлювати винятки" на уроці 2 із навчального набору для самостійного темпу MCTS (іспит 70-536): Microsoft® .NET Framework 2.0 - Foundation Development Application , причина в тому, що виняток може статися перед оголошеннями змінних у блоці спробу (як уже зазначали інші).

Цитата зі сторінки 25:

"Зауважте, що декларація StreamReader була переміщена за межі блоку" Спроба "у попередньому прикладі. Це необхідно, оскільки блок" Нарешті "не може отримати доступ до змінних, оголошених у блоці" Спроба ". Це має сенс, оскільки залежно від того, де стався виняток, оголошення змінної в межах Спробу блокувати ще не було виконано . "


4

Відповідь, як усі вказували, є майже "саме так визначаються блоки".

Є кілька пропозицій зробити код гарнішим. Див. ARM

 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


2

Ви вирішуєте саме те, що вам слід зробити. Ви не можете бути впевнені, що ваша декларація навіть була досягнута в блоці спробу, що призведе до іншого винятку в блоці вилову.

Він просто повинен працювати як окремі сфери.

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

2

Змінні рівні блоку та обмежені цим блоком "Спробуйте" або "Зловити". Аналогічно визначенню змінної у операторі if. Подумайте над цією ситуацією.

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

Рядок ніколи не буде оголошений, тому від нього не можна залежати.


2

Оскільки блок спробу та блок лову - це 2 різні блоки.

У наступному коді, чи очікуєте ви, що s, визначений у блоці A, буде видно у блоці B?

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

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

2

Хоча у вашому прикладі дивно, що він не працює, візьміть подібний:

    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);
     }
}

1

У конкретному прикладі, який ви подали, ініціалізація s не може кинути виняток. Тож ви можете подумати, що, можливо, його сфера може бути розширена.

Але загалом вирази ініціалізатора можуть кидати винятки. Не було б сенсу, щоб змінна, ініціалізатор якої викинув виняток (або яка була оголошена після іншої змінної, де це сталося), матиме змогу вловлювати / нарешті.

Також постраждає читабельність коду. Правило в C (і мовах, які його дотримуються, включаючи C ++, Java і C #), є простим: змінні області дії слідують за блоками.

Якщо ви хочете, щоб змінна мала область для спробувати / catch / нарешті, але ніде більше, то загортайте все в інший набір дужок (голий блок) і оголошуйте змінну перед спробою.


1

Частина причини, що вони не в тому ж обсязі, полягає в тому, що в будь-якій точці блоку спробу ви можете викинути виняток. Якби вони були в тому ж масштабі, це буде катастрофою в очікуванні, бо залежно від того, куди було викинуто виняток, це могло бути ще більш неоднозначним.

Принаймні, коли його оголошено поза блоком спробу, ви точно знаєте, якою може бути змінна як мінімум, коли викидається виняток; Значення змінної перед блоком спробу.


1

Коли ви оголошуєте локальну змінну, вона розміщується на стеку (для деяких типів все значення об'єкта буде знаходитися на стеку, для інших типів на стеку буде лише посилання). Коли всередині блоку спробу є виняток, локальні змінні всередині блоку звільняються, що означає, що стек повертається до стану, який він був на початку блоку спробу. Це за дизайном. Це те, як спроба / улов здатна відмовитись від усіх функціональних викликів всередині блоку і поверне вашу систему у функціональний стан. Без цього механізму ви ніколи не можете бути впевнені в стані будь-якого, коли відбувається виняток.

Якщо ваш код обробки помилок покладається на зовнішні декларовані змінні, які змінили їх значення всередині блоку спробу, мені здається поганим дизайном. Те, що ви робите, це, по суті, навмисне витікання ресурсів для отримання інформації (у цьому конкретному випадку це не так вже й погано, оскільки ви лише витікаєте інформацію, але уявіть, якби це був якийсь інший ресурс? Ви просто ускладнюєте життя собі в майбутнє). Я б запропонував розділити ваші пробні блоки на менші шматки, якщо вам потрібна більш детальна обробка помилок.


1

Коли у вас є спроба лову, ви повинні здебільшого знати, які помилки можуть бути сприятливі. Класи Theese Exception зазвичай повідомляють все, що потрібно про виняток. Якщо ні, то вам слід зробити власні класи виключень і передавати цю інформацію. Таким чином, вам ніколи не доведеться отримувати змінні зсередини блоку спробу, оскільки Виняток сам пояснює. Отже, якщо вам потрібно зробити це багато, подумайте про те, що ви розробляєте, і спробуйте подумати, чи є інший спосіб, що ви можете або передбачити, що можливі винятки, або використовувати інформацію, що надходить за винятками, а потім, можливо, повторно скинути свій власний виняток з додатковою інформацією.


1

Як вже вказували інші користувачі, фігурні дужки визначають сферу застосування майже в усіх мовах стилю C, які я знаю.

Якщо це проста змінна, то навіщо вам байдуже, як довго вона буде в масштабі? Це не така вже й велика справа.

у C #, якщо це складна змінна, вам потрібно буде реалізувати IDisposable. Потім можна або скористатися спробувати / catch / нарешті та зателефонувати obj.Dispose () у остаточному блоці. Або ви можете скористатись ключовим словом, яке автоматично викличе розпоряджатися в кінці розділу коду.


1

У Python вони помітні в блоках catch / нарешті, якщо лінія, що оголошує їх, не кинула.


1

Що робити, якщо виняток закидається в якийсь код, який знаходиться над оголошенням змінної. Що означає, сама декларація в цьому випадку не була випадковою.

try {

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

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

1

Спектр C # (15.2) зазначає "Область локальної змінної або константи, оголошеної в блоці ist blok."

(у першому прикладі блок спробу - це блок, де оголошено "s")


0

Думаю, що через те, що щось у блоці спробу викликало виняток, вмісту його простору імен не можна довіряти - тобто посилання на String 's у блоці catch може спричинити викид ще одного винятку.


0

Добре, якщо він не кине помилку компіляції, і ви можете оголосити її для решти методу, тоді не було б можливості оголосити її лише в межах спробувати. Це змушує вас бути явним щодо місця існування змінної, і не робить припущень.


0

Якщо ми на хвилину ігноруємо проблему блоку визначення, учаснику проекту доведеться працювати набагато важче в ситуації, яка не є чітко визначеною. Незважаючи на те, що це не є неможливим, помилка масштабування також змушує вас, автора коду, усвідомити наслідки написаного вами коду (що рядок s може бути нульовим у блоці вилову). Якщо ваш код був законним, у випадку винятку OutOfMemory, s навіть не гарантується, що йому буде виділено гніздо пам'яті:

// 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();
    }
}

Якщо ваш спроба / зловити / нарешті важко зрозуміти, спробуйте перефабрикувати або ввести інший шар непрямості з проміжним класом, який інкапсулює семантику того, що ви намагаєтеся досягти. Не бачачи реального коду, важко бути більш конкретним.


0

Замість місцевої змінної можна було б оголосити суспільну власність; це також повинно уникати чергової потенційної помилки непризначеної змінної. загальнодоступна рядок S {get; набір; }


-1

Якщо операція з присвоєння не вдасться, у вашій заяві про вилов буде встановлено нульове посилання на непризначену змінну.


2
Це не призначено. Це навіть недійсне значення (на відміну від екземпляра та статичних змінних).
Том Хотін - тайклін

-1

C # 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;
})();

WTF? Чому голосування вниз? Інкапсуляція є невід'ємною частиною OOP. Виглядає також досить.
ядро

2
Я не був головою, але те, що не так, це повернення неініціалізованого рядка.
Бен Войгт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.