Почему переменные не объявлены в «try» в области видимости в «catch» или «finally»?


143

В C # и Java (а также, возможно, в других языках) переменные, объявленные в блоке «try», не попадают в область видимости соответствующих блоков «catch» или «finally». Например, следующий код не компилируется:

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

В этом коде ошибка времени компиляции возникает при ссылке на s в блоке catch, потому что s находится только в области видимости в блоке try. (В Java ошибка компиляции - «s не может быть разрешена»; в C # это «имя 's' не существует в текущем контексте».)

Общее решение этой проблемы состоит в том, чтобы вместо этого объявлять переменные непосредственно перед блоком try, а не внутри блока try:

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

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

У меня вопрос: каковы причины этого решения о языковом дизайне (на Java, на C # и / или на любых других применимых языках)?

Ответы:


176

Две вещи:

  1. Как правило, Java имеет всего 2 уровня области действия: глобальный и функциональный. Но try / catch - исключение (это не каламбур). Когда генерируется исключение и объект исключения получает присвоенную ему переменную, эта объектная переменная доступна только в разделе «catch» и уничтожается, как только захват завершается.

  2. (и что более важно). Вы не можете знать, где в блоке try возникло исключение. Возможно, это было до объявления вашей переменной. Поэтому невозможно сказать, какие переменные будут доступны для предложения catch / finally. Рассмотрим следующий случай, когда область видимости такая, как вы предложили:

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

Это явно проблема - когда вы дойдете до обработчика исключения, s не будет объявлен. Учитывая, что уловы предназначены для обработки исключительных обстоятельств и, наконец, должны выполняться, быть безопасным и объявлять это проблемой во время компиляции намного лучше, чем во время выполнения.


55

Как вы могли быть уверены, что достигли части объявления в своем блоке catch? Что, если при создании экземпляра возникнет исключение?


6
А? Объявления переменных не вызывают исключений.
Джошуа

7
Согласен, это экземпляр, который может вызвать исключение.
Burkhard

19

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

... 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 - это условие, предложение then и предложение else.


10

Все остальные подняли основы - то, что происходит в блоке, остается в блоке. Но в случае .NET может быть полезно проверить, что, по мнению компилятора, происходит. Возьмем, например, следующий код try / catch (обратите внимание, что 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 ++ область действия автоматической переменной ограничена фигурными скобками, которые ее окружают. Почему кто-то мог ожидать, что это изменится, если ввести ключевое слово try вне фигурных скобок?


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

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 переменная, объявленная в блоке try, находится в области видимости в блоке catch и везде в содержащейся в ней функции.


5

Согласно разделу «Как генерировать и перехватывать исключения» в Уроке 2 набора для самостоятельного обучения MCTS (экзамен 70-536): Microsoft® .NET Framework 2.0 - Application Development Foundation , причина в том, что могло произойти исключение. перед объявлением переменных в блоке try (как уже отмечали другие).

Цитата со страницы 25:

«Обратите внимание, что объявление StreamReader было перемещено за пределы блока Try в предыдущем примере. Это необходимо, поскольку блок finally не может получить доступ к переменным, объявленным в блоке Try. Это имеет смысл, потому что в зависимости от того, где произошло исключение, объявления переменных внутри Возможно, блок попытки еще не был выполнен . "


4

@burkhard имеет вопрос о том, почему ответил правильно, но в качестве примечания я хотел добавить, хотя ваш рекомендуемый пример решения хорош в 99,9999 +% времени, это не очень хорошая практика, гораздо безопаснее проверять значение null перед использованием что-то создает в блоке try или инициализирует переменную чем-то вместо того, чтобы просто объявлять ее перед блоком try. Например:

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, сложнее, чем присвоение строки, вы должны иметь возможность безопасно получить доступ к данным из вашего блока catch.


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, что приведет к другому исключению в блоке catch.

Он просто должен работать как отдельные области.

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

Переменные являются уровнями блока и ограничены этим блоком Try или Catch. Аналогично определению переменной в операторе if. Подумайте об этой ситуации.

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

String никогда не будет объявлен, поэтому на него нельзя полагаться.


2

Потому что блок try и блок catch - это 2 разных блока.

Ожидаете ли вы, что в следующем коде s, определенные в блоке A, будут видны в блоке B?

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

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

2

В Python они видны в блоках catch / finally, если строка, объявляющая их, не выбрана.


2

Хотя в вашем примере это странно, что он не работает, возьмите этот похожий:

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

Это приведет к тому, что уловка вызовет исключение нулевой ссылки, если код 1 сломается. Теперь, хотя семантика try / catch довольно хорошо изучена, это было бы неприятным угловым случаем, поскольку s определяется с начальным значением, поэтому теоретически оно никогда не должно быть нулевым, но с общей семантикой это было бы так.

Опять же, теоретически это можно исправить, разрешив только отдельные определения ( String s; s = "1|2";) или какой-либо другой набор условий, но, как правило, проще просто сказать «нет».

Кроме того, он позволяет определять семантику области без исключения глобально, в частности, локальные переменные существуют до тех пор, пока {} они определены в, во всех случаях. Мелочь, но точка.

Наконец, чтобы делать то, что вы хотите, вы можете добавить скобки вокруг try catch. Дает вам желаемый объем, хотя это происходит за счет небольшой читабельности, но не слишком большой.

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

1

В конкретном примере, который вы привели, инициализация s не может вызвать исключение. Вы бы подумали, что, возможно, его объем можно расширить.

Но в целом выражения инициализатора могут вызывать исключения. Было бы бессмысленно, если бы переменная, инициализатор которой выдал исключение (или которая была объявлена ​​после другой переменной, где это произошло), попала в область действия catch / finally.

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

Если вы хотите, чтобы переменная находилась в области видимости для try / catch / finally, но нигде больше, тогда оберните все это в другой набор фигурных скобок (пустой блок) и объявите переменную перед попыткой.


1

Частично причина, по которой они не находятся в одной области, заключается в том, что в любой точке блока try вы можете вызвать исключение. Если бы они были в одной области, ожидание было бы катастрофой, потому что в зависимости от того, где было создано исключение, оно могло быть еще более неоднозначным.

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


1

Когда вы объявляете локальную переменную, она помещается в стек (для некоторых типов все значение объекта будет в стеке, для других типов в стеке будет только ссылка). Когда возникает исключение внутри блока try, локальные переменные в блоке освобождаются, что означает, что стек «раскручивается» до состояния, в котором он находился в начале блока try. Это сделано намеренно. Это то, как try / catch может отказаться от всех вызовов функций внутри блока и вернуть вашу систему в функциональное состояние. Без этого механизма вы никогда не сможете быть уверены в состоянии чего-либо в случае возникновения исключения.

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


1

Когда у вас есть попытка уловки, вы должны по большей части знать, какие ошибки она может вызвать. Эти классы исключений обычно сообщают все, что вам нужно, об исключении. Если нет, вам следует создать собственные классы исключений и передавать эту информацию. Таким образом, вам никогда не нужно будет получать переменные из блока try, потому что исключение не требует пояснений. Поэтому, если вам нужно делать это много, подумайте о своем дизайне и попытайтесь подумать, есть ли какой-то другой способ, чтобы вы могли либо предсказать появление исключений, либо использовать информацию, полученную из исключений, а затем, возможно, повторно выбросить свой собственный исключение с дополнительной информацией.


1

Как отмечали другие пользователи, фигурные скобки определяют область видимости практически во всех языках стиля C, о которых я знаю.

Если это простая переменная, то почему вас волнует, как долго она будет в области видимости? Это не так уж и важно.

в C #, если это сложная переменная, вы захотите реализовать IDisposable. Затем вы можете использовать try / catch / finally и вызвать obj.Dispose () в блоке finally. Или вы можете использовать ключевое слово using, которое автоматически вызовет Dispose в конце раздела кода.


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) гласит: «Область локальной переменной или константы, объявленной в блоке, является блоком».

(в вашем первом примере блок try - это блок, в котором объявлен "s")


0

Я думал, что, поскольку что-то в блоке try вызвало исключение, его содержимому пространства имен нельзя доверять - то есть ссылка на String в блоке catch может вызвать выброс еще одного исключения.


0

Что ж, если он не вызывает ошибку компиляции, и вы могли бы объявить ее для остальной части метода, тогда не было бы возможности объявить ее только в пределах области try. Это заставляет вас четко указывать, где должна существовать переменная, и не делает никаких предположений.


0

Если мы на мгновение проигнорируем проблему блокировки области видимости, компилятору придется работать намного усерднее в ситуации, которая не совсем четко определена. Хотя это не невозможно, ошибка области видимости также вынуждает вас, автора кода, осознать значение написанного вами кода (что строка s может быть нулевой в блоке catch). Если ваш код был законным, в случае исключения OutOfMemory s даже не гарантируется выделение слота памяти:

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

CLR (и, следовательно, компилятор) также заставляет вас инициализировать переменные перед их использованием. В представленном блоке catch это не может гарантировать.

Таким образом, компилятор должен выполнять много работы, что на практике не приносит особой пользы и, вероятно, сбивает людей с толку и заставляет их спросить, почему try / catch работает по-другому.

Помимо согласованности, не допуская ничего необычного и придерживаясь уже установленной семантики области видимости, используемой во всем языке, компилятор и среда CLR могут обеспечить большую гарантию состояния переменной внутри блока catch. Что он существует и был инициализирован.

Обратите внимание, что разработчики языка хорошо поработали с другими конструкциями, такими как using и lock где проблема и область действия четко определены, что позволяет писать более понятный код.

например, ключевое слово using с объектами IDisposable в:

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

эквивалентно:

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

Если ваш try / catch / finally трудно понять, попробуйте провести рефакторинг или ввести другой уровень косвенного обращения с промежуточным классом, который инкапсулирует семантику того, что вы пытаетесь выполнить. Не видя реального кода, трудно быть более конкретным.


0

Вместо локальной переменной можно было объявить публичное свойство; это также должно избежать еще одной потенциальной ошибки неназначенной переменной. публичная строка S {получить; задавать; }


-1

Если операция присваивания не удалась, ваш оператор catch будет иметь нулевую ссылку на неназначенную переменную.


2
Это не назначено. Это даже не null (в отличие от переменных экземпляра и статических переменных).
Tom Hawtin - tackline

-1

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

Какого черта? Почему голосование против? Инкапсуляция является неотъемлемой частью ООП. Смотрится тоже красиво.
core

2
Я не голосовал против, но что не так, так это возвращение неинициализированной строки.
Бен Фойгт
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.