Por que as variáveis ​​declaradas no escopo "try" não estão em "catch" ou "finalmente"?


139

Em C # e em Java (e possivelmente em outras linguagens também), as variáveis ​​declaradas em um bloco "try" não estão no escopo nos blocos "catch" ou "finalmente" correspondentes. Por exemplo, o código a seguir não é compilado:

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

Nesse código, ocorre um erro em tempo de compilação na referência a s no bloco catch, porque s está apenas no escopo no bloco try. (Em Java, o erro de compilação é "s não pode ser resolvido"; em C #, é "O nome 's' não existe no contexto atual".)

A solução geral para esse problema parece ser declarar variáveis ​​pouco antes do bloco try, em vez de dentro do bloco try:

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

No entanto, pelo menos para mim, (1) isso parece uma solução complicada e (2) resulta em variáveis ​​com um escopo maior do que o programador pretendia (todo o restante do método, em vez de apenas no contexto do tente pegar finalmente).

Minha pergunta é: quais foram / são as razões por trás dessa decisão de design de linguagem (em Java, em C # e / ou em qualquer outra linguagem aplicável)?

Respostas:


171

Duas coisas:

  1. Geralmente, o Java tem apenas dois níveis de escopo: global e função. Mas, try / catch é uma exceção (sem trocadilhos). Quando uma exceção é lançada e o objeto de exceção recebe uma variável atribuída a ela, essa variável de objeto fica disponível apenas na seção "catch" e é destruída assim que a captura é concluída.

  2. (e mais importante). Você não pode saber onde no bloco try a exceção foi lançada. Pode ter sido antes de sua variável ser declarada. Portanto, é impossível dizer quais variáveis ​​estarão disponíveis para a cláusula catch / finally. Considere o seguinte caso, em que o escopo é o sugerido:

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

Isso claramente é um problema - quando você alcança o manipulador de exceções, s não serão declarados. Dado que as capturas são destinadas a lidar com circunstâncias excepcionais e, por fim, devem ser executadas, estar seguro e declarar isso um problema no tempo de compilação é muito melhor do que no tempo de execução.


55

Como você pode ter certeza de que alcançou a parte da declaração no seu bloco de captura? E se a instanciação lançar a exceção?


6
Hã? Declarações de variáveis ​​não lançam exceções.
Joshua

6
Concordado, é a instanciação que pode lançar a exceção.
22909 Burkhard

19

Tradicionalmente, nas linguagens de estilo C, o que acontece dentro do aparelho permanece dentro dele. Eu acho que ter uma vida útil variável variando entre escopos assim não seria intuitivo para a maioria dos programadores. Você pode conseguir o que deseja colocando os blocos try / catch / finalmente dentro de outro nível de chaves. por exemplo

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

EDIT: Eu acho que todas as regras não têm uma exceção. O seguinte é C ++ válido:

int f() { return 0; }

void main() 
{
    int y = 0;

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

O escopo de x é a condicional, a cláusula then e a cláusula else.


10

Todos os outros abordaram o básico - o que acontece em um bloco permanece em um bloco. Mas no caso do .NET, pode ser útil examinar o que o compilador pensa que está acontecendo. Pegue, por exemplo, o seguinte código try / catch (observe que o StreamReader está declarado, corretamente, fora dos blocos):

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

Isso será compilado para algo semelhante ao seguinte no 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

O que vemos? O MSIL respeita os blocos - eles fazem parte intrinsecamente do código subjacente gerado quando você compila seu C #. O escopo não é apenas rígido na especificação C #, mas também na especificação CLR e CLS.

O escopo protege você, mas você ocasionalmente precisa contorná-lo. Com o tempo, você se acostuma e começa a parecer natural. Como todo mundo disse, o que acontece em um bloco permanece nesse bloco. Você quer compartilhar algo? Você tem que sair dos quarteirões ...


8

De qualquer forma, em C ++, o escopo de uma variável automática é limitado pelas chaves que a cercam. Por que alguém esperaria que isso fosse diferente inserindo uma palavra-chave try fora do aparelho?


1
Acordado; "}" significa fim do escopo. No entanto, try-catch-finalmente é incomum, pois após um bloco de tentativa, você deve ter uma captura e / ou, finalmente, um bloco; portanto, uma exceção à regra normal em que o escopo de um bloco try transportado para a captura associada / finalmente pode parecer aceitável?
Jon Schneider

7

Como ravenspoint apontou, todo mundo espera que as variáveis ​​sejam locais para o bloco em que são definidas. tryIntroduz um bloco e o mesmo acontece catch.

Se você deseja que as variáveis ​​sejam locais para ambos trye catch, tente incluir as duas em um bloco:

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

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

5

A resposta simples é que C e a maioria dos idiomas que herdaram sua sintaxe têm escopo de bloco. Isso significa que se uma variável é definida em um bloco, ou seja, dentro de {}, esse é seu escopo.

A exceção, a propósito, é o JavaScript, que tem uma sintaxe semelhante, mas tem escopo de função. Em JavaScript, uma variável declarada em um bloco try está no escopo no bloco catch e em qualquer outro lugar em sua função que contém.


4

O @burkhard tem a pergunta sobre o porquê de ter respondido corretamente, mas como uma observação que eu gostaria de acrescentar, enquanto o seu exemplo de solução recomendado é bom 99,9999 +% do tempo, não é uma boa prática, é muito mais seguro verificar se é nulo antes de usar algo é instanciado dentro do bloco try ou inicializa a variável para algo em vez de apenas declará-la antes do bloco try. Por exemplo:

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

Ou:

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

Isso deve fornecer escalabilidade na solução alternativa, para que, mesmo quando o que você está fazendo no bloco try for mais complexo do que atribuir uma sequência, você possa acessar com segurança os dados do seu bloco catch.


4

De acordo com a seção intitulada "Como lançar e capturar exceções" na Lição 2 do Kit de treinamento individual do MCTS (Exame 70-536): Microsoft® .NET Framework 2.0 - Application Development Foundation , o motivo é que a exceção pode ter ocorrido antes das declarações de variáveis ​​no bloco try (como outros já observaram).

Citação da página 25:

"Observe que a declaração StreamReader foi movida para fora do bloco Try no exemplo anterior. Isso é necessário porque o bloco Finalmente não pode acessar variáveis ​​declaradas no bloco Try. Isso faz sentido porque, dependendo de onde ocorreu uma exceção, as declarações de variáveis ​​dentro do bloco O bloco Try pode ainda não ter sido executado . "


4

A resposta, como todos apontaram, é basicamente "é assim que os blocos são definidos".

Existem algumas propostas para tornar o código mais bonito. Veja ARM

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

Os fechamentos devem tratar disso também.

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

ATUALIZAÇÃO: O ARM é implementado no Java 7. http://download.java.net/jdk7/docs/technotes/guides/language/try-with-resources.html


2

Sua solução é exatamente o que você deve fazer. Você não pode ter certeza de que sua declaração foi alcançada no bloco try, o que resultaria em outra exceção no bloco catch.

Simplesmente deve funcionar como escopos separados.

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

As variáveis ​​estão no nível do bloco e restritas a esse bloco Try ou Catch. Semelhante à definição de uma variável em uma instrução if. Pense nessa situação.

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

A String nunca seria declarada, portanto não pode ser dependente.


2

Porque o bloco try e o bloco catch são 2 blocos diferentes.

No código a seguir, você esperaria que s definidos no bloco A sejam visíveis no bloco B?

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

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

2

Enquanto no seu exemplo é estranho que ele não funcione, faça o seguinte:

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

Isso faria com que a captura lançasse uma exceção de referência nula se o Código 1 fosse quebrado. Agora, embora a semântica de try / catch seja bem compreendida, esse seria um caso irritante, uma vez que s é definido com um valor inicial, portanto, em teoria, nunca deve ser nulo, mas sob a semântica compartilhada, seria.

Novamente, isso poderia ser corrigido em teoria, permitindo apenas definições separadas ( String s; s = "1|2";) ou algum outro conjunto de condições, mas geralmente é mais fácil dizer não.

Além disso, ele permite que a semântica do escopo seja definida globalmente, sem exceção, especificamente, os locais duram enquanto {} que sejam definidos em todos os casos. Ponto menor, mas um ponto.

Por fim, para fazer o que você deseja, você pode adicionar um conjunto de colchetes ao redor da captura de tentativa. Dá a você o escopo que você deseja, apesar de custar um pouco de legibilidade, mas não muito.

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

1

No exemplo específico que você deu, a inicialização de s não pode gerar uma exceção. Então você pensaria que talvez seu escopo pudesse ser estendido.

Mas, em geral, as expressões do inicializador podem gerar exceções. Não faria sentido que uma variável cujo inicializador lançou uma exceção (ou que foi declarada após outra variável onde isso aconteceu) estivesse no escopo de catch / finalmente.

Além disso, a legibilidade do código seria prejudicada. A regra em C (e linguagens que a seguem, incluindo C ++, Java e C #) é simples: escopos de variáveis ​​seguem blocos.

Se você deseja que uma variável esteja no escopo de try / catch / finalmente, mas em nenhum outro lugar, envolva tudo em outro conjunto de chaves (um bloco simples) e declare a variável antes da tentativa.


1

Parte do motivo pelo qual eles não estão no mesmo escopo é que, em qualquer ponto do bloco try, você pode ter lançado a exceção. Se eles estivessem no mesmo escopo, seria um desastre à espera, porque, dependendo de onde a exceção foi lançada, poderia ser ainda mais ambíguo.

Pelo menos quando declarado fora do bloco try, você sabe ao certo qual poderia ser a variável quando uma exceção é lançada; O valor da variável antes do bloco try.


1

Quando você declara uma variável local, ela é colocada na pilha (para alguns tipos, o valor inteiro do objeto estará na pilha, para outros tipos, apenas uma referência estará na pilha). Quando há uma exceção dentro de um bloco try, as variáveis ​​locais dentro do bloco são liberadas, o que significa que a pilha é "desenrolada" de volta ao estado em que estava no início do bloco try. Isso ocorre por design. É assim que o try / catch é capaz de recuperar todas as chamadas de função dentro do bloco e coloca seu sistema novamente em um estado funcional. Sem esse mecanismo, você nunca poderá ter certeza do estado de qualquer coisa quando ocorrer uma exceção.

Ter seu código de manipulação de erros baseado em variáveis ​​declaradas externamente, cujos valores foram alterados dentro do bloco try, parece um projeto ruim para mim. O que você está fazendo é essencialmente vazar recursos intencionalmente para obter informações (nesse caso em particular, não é tão ruim porque você está vazando apenas informações, mas imagine se fosse algum outro recurso? futuro). Sugiro dividir seus blocos de tentativa em blocos menores se você precisar de mais granularidade no tratamento de erros.


1

Quando você tenta capturar, na maioria das vezes você deve saber quais erros isso pode gerar. Essas classes de exceção normalmente informam tudo o que você precisa sobre a exceção. Caso contrário, você deve criar suas próprias classes de exceção e transmitir essas informações. Dessa forma, você nunca precisará obter as variáveis ​​de dentro do bloco try, porque a exceção é auto-explicativa. Portanto, se você precisar fazer isso muito, pense no seu design e tente pensar se existe alguma outra maneira, que você pode prever exceções chegando ou usar as informações provenientes das exceções e, talvez, refazer o seu próprio exceção com mais informações.


1

Como foi apontado por outros usuários, os chavetas definem o escopo em praticamente todas as linguagens de estilo C que eu conheço.

Se é uma variável simples, por que você se importa com quanto tempo isso vai ter no escopo? Não é grande coisa.

em C #, se for uma variável complexa, você desejará implementar o IDisposable. Você pode usar try / catch / finalmente e chamar obj.Dispose () no bloco final. Ou você pode usar a palavra-chave using, que automaticamente chamará Dispose no final da seção de código.


1

No Python, eles são visíveis nos blocos catch / finalmente, se a linha que os declara não foi lançada.


1

E se a exceção for lançada em algum código que esteja acima da declaração da variável. O que significa que a declaração em si não aconteceu neste caso.

try {

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

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

1

A C # Spec (15.2) declara "O escopo de uma variável ou constante local declarada em um bloco é o bloco".

(no seu primeiro exemplo, o bloco try é o bloco em que "s" é declarado)


0

Meu pensamento seria que, porque algo no bloco try desencadeou a exceção, seu conteúdo do espaço para nome não pode ser confiável - ou seja, referenciar as String 's' no bloco catch pode causar o lançamento de mais uma exceção.


0

Bem, se ele não gerar um erro de compilação e você puder declará-lo pelo resto do método, não haverá como declará-lo apenas no escopo de tentativa. Ele está forçando você a ser explícito sobre onde a variável deveria existir e não faz suposições.


0

Se ignorarmos a questão do escopo por um momento, o cumpridor terá que trabalhar muito mais em uma situação que não está bem definida. Embora isso não seja impossível, o erro de escopo também obriga você, o autor do código, a perceber a implicação do código que você escreve (que a string s pode ser nula no bloco catch). Se o seu código for legal, no caso de uma exceção OutOfMemory, s não garante nem que seja alocado um slot de memória:

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

O CLR (e, portanto, o compilador) também força você a inicializar variáveis ​​antes que elas sejam usadas. No bloco catch apresentado, não é possível garantir isso.

Então, acabamos com o compilador fazendo muito trabalho, o que, na prática, não oferece muitos benefícios e provavelmente confunde as pessoas e as leva a perguntar por que o try / catch funciona de maneira diferente.

Além da consistência, ao não permitir nada sofisticado e aderir à semântica de escopo já estabelecida usada em todo o idioma, o compilador e o CLR são capazes de fornecer uma maior garantia do estado de uma variável dentro de um bloco de captura. Que existe e foi inicializado.

Observe que os designers de linguagem fizeram um bom trabalho com outras construções, como usar e bloquear, onde o problema e o escopo estão bem definidos, o que permite que você escreva um código mais claro.

por exemplo, a palavra-chave using com objetos IDisposable em:

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

é equivalente a:

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

Se for difícil entender sua tentativa / captura / finalmente, tente refatorar ou introduzir outra camada de indireção com uma classe intermediária que encapsule a semântica do que você está tentando realizar. Sem ver o código real, é difícil ser mais específico.


0

Em vez de uma variável local, uma propriedade pública pode ser declarada; isso também deve evitar outro erro em potencial de uma variável não atribuída. sequência pública S {get; conjunto; }


-1

Se a operação de atribuição falhar, sua instrução catch terá uma referência nula para a variável não atribuída.


2
Não está atribuído. Não é nem nulo (ao contrário de instâncias e variáveis ​​estáticas).
Tom Hawtin - defina

-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? Por que o voto negativo? O encapsulamento é parte integrante do POO. Parece bonito também.
núcleo

2
Eu não era o voto negativo, mas o que há de errado é retornar uma string não inicializada.
quer
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.