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

В 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 # и / или на любых других применимых языках)?


person Jon Schneider    schedule 18.09.2008    source источник


Ответы (28)


Две вещи:

  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 не будет объявлен. Учитывая, что уловы предназначены для обработки исключительных обстоятельств и, наконец, должны выполняться, быть безопасным и объявлять это проблемой во время компиляции намного лучше, чем во время выполнения.

person John Christensen    schedule 18.09.2008

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

person Burkhard    schedule 18.09.2008

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

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

person Ferruccio    schedule 18.09.2008

Все остальные подняли основы - то, что происходит в блоке, остается в блоке. Но в случае .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.

Прицел защищает вас, но иногда приходится его обходить. Со временем к этому привыкаешь, и это начинает казаться естественным. Как все говорили, то, что происходит в блоке, остается в этом блоке. Хотите чем-то поделиться? Вы должны выйти за пределы блоков ...

person John Rudy    schedule 18.09.2008

Во всяком случае, в C ++ область действия автоматической переменной ограничена фигурными скобками, которые ее окружают. Почему кто-то мог ожидать, что это изменится, если ввести ключевое слово try за пределами фигурных скобок?

person ravenspoint    schedule 18.09.2008
comment
Согласованный; } означает конец области видимости. Однако команда try-catch-finally необычна тем, что после блока try вы должны иметь перехват и / или блок finally; таким образом, исключение из обычного правила, когда объем блока try, переносимый в связанный catch / finally, может показаться приемлемым? - person Jon Schneider; 19.09.2008

Как указывал ravenspoint, все ожидают, что переменные будут локальными по отношению к блоку, в котором они определены. try вводит блок, и catch тоже.

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

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

        throw new Exception(":(")
    }
    catch (Exception e)
    {
        Debug.WriteLine(s);
    }
}
person Daren Thomas    schedule 18.09.2008

Простой ответ заключается в том, что C и большинство языков, унаследовавших его синтаксис, имеют блочную область видимости. Это означает, что если переменная определена в одном блоке, то есть внутри {}, это ее область действия.

Исключением, кстати, является JavaScript, имеющий похожий синтаксис, но ограниченный функцией. В JavaScript переменная, объявленная в блоке try, находится в области видимости блока catch и повсюду в его содержащей функции.

person dgvid    schedule 18.09.2008

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

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

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

person hurst    schedule 19.09.2008

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

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

Or:

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

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

person Timothy Carter    schedule 18.09.2008

Ответ, как все уже отмечали, сводится к тому, что «блоки определены так».

Есть предложения сделать код красивее. См. 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

person ykaganovich    schedule 18.09.2008

Ваше решение - это именно то, что вам следует делать. Вы не можете быть уверены, что ваше объявление было достигнуто даже в блоке 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
person EndangeredMassa    schedule 18.09.2008

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

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

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

person jW.    schedule 18.09.2008

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

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

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

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

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

person Community    schedule 19.09.2008

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

    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);
     }
}
person Guvante    schedule 18.09.2008

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

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

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

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

person Steve Jessop    schedule 18.09.2008

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

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

person zxcv    schedule 18.09.2008

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

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

person Wedge    schedule 18.09.2008

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

person Jesper Blad Jensen    schedule 18.09.2008

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

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

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

person Charles Graham    schedule 18.09.2008

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

try {

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

} catch (Exception) {
        //Use s;//Problem here
} finally {
        //Use s;//Problem here
}
person Ravi    schedule 29.12.2010

В спецификации C # (15.2) говорится: «Объем локальная переменная или константа, объявленная в блоке, является блоком ".

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

person tamberg    schedule 18.09.2008

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

person jpbarto    schedule 18.09.2008

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

person kemiller2002    schedule 18.09.2008

Если мы на мгновение проигнорируем проблему блокировки области видимости, компилятору придется работать намного усерднее в ситуации, которая не совсем четко определена. Хотя это не невозможно, ошибка области видимости также вынуждает вас, автора кода, осознать значение написанного вами кода (что строка 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 трудно понять, попробуйте провести рефакторинг или ввести другой уровень косвенного обращения с промежуточным классом, который инкапсулирует семантику того, что вы пытаетесь выполнить. Не видя реального кода, трудно быть более конкретным.

person Robert Paulson    schedule 18.09.2008

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

person usefulBee    schedule 23.07.2013

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

person SaaS Developer    schedule 18.09.2008
comment
Это не назначено. Это даже не null (в отличие от переменных экземпляра и статических переменных). - person Tom Hawtin - tackline; 19.09.2008

person    schedule
comment
Какого черта? Почему голосование против? Инкапсуляция является неотъемлемой частью ООП. Смотрится тоже красиво. - person core; 20.01.2009
comment
Я не голосовал против, но что не так, это неинициализированная строка. - person Ben Voigt; 27.04.2010