Com relação à consistência à parte, não faria sentido conseguir quebrar nosso código com o tratamento de erros sem a necessidade de refatorar?
Para responder a isso, é necessário examinar mais do que apenas o escopo de uma variável .
Mesmo que a variável permanecesse no escopo, ela não seria definitivamente atribuída .
Declarar a variável no bloco try expressa - para o compilador e para os leitores humanos - que isso só é significativo dentro desse bloco. É útil para o compilador impor isso.
Se você deseja que a variável esteja no escopo após o bloco try, você pode declará-la fora do bloco:
var zerothVariable = 1_000_000_000_000L;
int firstVariable;
try {
// Change checked to unchecked to allow the overflow without throwing.
firstVariable = checked((int)zerothVariable);
}
catch (OverflowException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
}
Isso expressa que a variável pode ser significativa fora do bloco try. O compilador permitirá isso.
Mas também mostra outro motivo pelo qual normalmente não seria útil manter as variáveis no escopo depois de introduzi-las em um bloco try. O compilador C # executa uma análise de atribuição definida e proíbe a leitura do valor de uma variável que ela não provou ter recebido um valor. Portanto, você ainda não pode ler da variável.
Suponha que eu tente ler a variável após o bloco try:
Console.WriteLine(firstVariable);
Isso dará um erro em tempo de compilação :
CS0165 Uso da variável local não atribuída 'firstVariable'
Chamei Environment.Exit no bloco catch, para que eu saiba que a variável foi atribuída antes da chamada para Console.WriteLine. Mas o compilador não infere isso.
Por que o compilador é tão rigoroso?
Eu não posso nem fazer isso:
int n;
try {
n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}
Console.WriteLine(n);
Uma maneira de analisar essa restrição é dizer que a análise de atribuição definida em C # não é muito sofisticada. Mas outra maneira de analisar é que, quando você escreve código em um bloco try com cláusulas catch, está dizendo ao compilador e a qualquer leitor humano que ele deva ser tratado como se nem todos pudessem ser executados.
Para ilustrar o que quero dizer, imagine se o compilador permitiu o código acima, mas você adicionou uma chamada no bloco try a uma função que você sabe que pessoalmente não lançará uma exceção . Não sendo possível garantir que a função chamada não gerou um IOException
, o compilador não sabia o que n
foi atribuído e, então, você teria que refatorar.
Isso significa que, ao excluir a análise altamente sofisticada para determinar se uma variável atribuída em um bloco try com cláusulas catch foi definitivamente atribuída posteriormente, o compilador ajuda a evitar a gravação de código que provavelmente quebrará mais tarde. (Afinal, capturar uma exceção geralmente significa que você acha que uma pode ser lançada.)
Você pode garantir que a variável seja atribuída por todos os caminhos de código.
Você pode compilar o código, atribuindo à variável um valor antes do bloco try ou no bloco catch. Dessa forma, ele ainda terá sido inicializado ou atribuído, mesmo que a atribuição no bloco try não ocorra. Por exemplo:
var n = 0; // But is this meaningful, or just covering a bug?
try {
n = 10;
}
catch (IOException) {
}
Console.WriteLine(n);
Ou:
int n;
try {
n = 10;
}
catch (IOException) {
n = 0; // But is this meaningful, or just covering a bug?
}
Console.WriteLine(n);
Aqueles compilam. Mas é melhor fazer algo assim apenas se o valor padrão que você der faz sentido * e produz um comportamento correto.
Observe que, neste segundo caso, em que você atribui a variável no bloco try e em todos os blocos catch, embora seja possível ler a variável após o try-catch, você ainda não seria capaz de ler a variável dentro de um finally
bloco anexado , porque a execução pode deixar um bloco de tentativa em mais situações do que geralmente pensamos .
* A propósito, algumas linguagens, como C e C ++, permitem variáveis não inicializadas e não possuem análise de atribuição definida para impedir a leitura delas. Como a leitura de memória não inicializada faz com que os programas se comportem de maneira não determinística e errática , geralmente é recomendável evitar a introdução de variáveis nesses idiomas sem fornecer um inicializador. Em linguagens com análise de atribuição definida, como C # e Java, o compilador evita a leitura de variáveis não inicializadas e também o menor mal de inicializá-las com valores sem sentido que posteriormente podem ser mal interpretados como significativos.
Você pode fazer isso para que os caminhos de código em que a variável não está atribuída gerem uma exceção (ou retornem).
Se você planeja executar alguma ação (como registro em log) e relançar a exceção ou lançar outra exceção, e isso acontece em qualquer cláusula catch em que a variável não esteja atribuída, o compilador saberá que a variável foi atribuída:
int n;
try {
n = 10;
}
catch (IOException e) {
Console.Error.WriteLine(e.Message);
throw;
}
Console.WriteLine(n);
Isso compila e pode muito bem ser uma escolha razoável. No entanto, em um aplicativo real, a menos que a exceção seja lançada apenas em situações em que nem faz sentido tentar recuperar * , você deve garantir que ainda está capturando e manipulando-o adequadamente em algum lugar .
(Você também não pode ler a variável em um bloco finalmente nessa situação, mas não parece que você deveria - afinal, os blocos finalmente sempre são executados essencialmente e, nesse caso, a variável nem sempre é atribuída .)
* Por exemplo, muitos aplicativos não possuem uma cláusula catch que lida com uma OutOfMemoryException porque qualquer coisa que eles possam fazer sobre isso pode ser pelo menos tão ruim quanto travar .
Talvez você realmente não quiser refatorar o código.
No seu exemplo, você apresenta firstVariable
e secondVariable
tenta blocos. Como eu disse, você pode defini-los antes dos blocos try nos quais eles são atribuídos, para que eles permaneçam no escopo posteriormente, e você pode satisfazer / enganar o compilador para permitir que você os leia, certificando-se de que eles sempre sejam atribuídos.
Mas o código que aparece após esses blocos depende, presumivelmente, deles terem sido atribuídos corretamente. Se for esse o caso, seu código deve refletir e garantir isso.
Primeiro, você pode (e deve) realmente lidar com o erro aí? Um dos motivos pelos quais o tratamento de exceções existe é facilitar o tratamento de erros onde eles podem ser tratados com eficiência , mesmo que isso não esteja próximo de onde eles ocorrem.
Se você realmente não consegue lidar com o erro na função que inicializou e usa essas variáveis, talvez o bloco try não deva estar nessa função, mas em algum lugar mais alto (ou seja, no código que chama essa função ou código que chama esse código). Apenas verifique se você não está capturando acidentalmente uma exceção lançada em outro lugar e assumindo erroneamente que ela foi lançada durante a inicialização firstVariable
e secondVariable
.
Outra abordagem é colocar o código que usa as variáveis no bloco try. Isso geralmente é razoável. Novamente, se as mesmas exceções que você está capturando de seus inicializadores também puderem ser lançadas a partir do código ao redor, certifique-se de não negligenciar essa possibilidade ao manipulá-las.
(Suponho que você esteja inicializando as variáveis com expressões mais complicadas do que as mostradas em seus exemplos, de modo que elas possam realmente gerar uma exceção e também que você não esteja planejando capturar todas as exceções possíveis , mas apenas para capturar quaisquer exceções específicas você pode antecipar e lidar de maneira significativa.É verdade que o mundo real nem sempre é tão bom e o código de produção às vezes faz isso , mas como seu objetivo aqui é lidar com erros que ocorrem durante a inicialização de duas variáveis específicas, qualquer cláusula catch que você escrever para esse específico propósito deve ser específico para quaisquer erros que sejam.)
Uma terceira maneira é extrair o código que pode falhar e o try-catch que lida com ele, em seu próprio método. Isso é útil se você quiser lidar com os erros completamente primeiro e depois não se preocupar com a captura acidental de uma exceção que deve ser tratada em outro lugar.
Suponha, por exemplo, que você queira sair imediatamente do aplicativo após falha na atribuição de qualquer variável. (Obviamente, nem todo tratamento de exceção é para erros fatais; este é apenas um exemplo e pode ou não ser como você deseja que seu aplicativo reaja ao problema.) Você pode fazer algo assim:
// In real life, this should be named more descriptively.
private static (int firstValue, int secondValue) GetFirstAndSecondValues()
{
try {
// This code is contrived. The idea here is that obtaining the values
// could actually fail, and throw a SomeSpecificException.
var firstVariable = 1;
var secondVariable = firstVariable;
return (firstVariable, secondVariable);
}
catch (SomeSpecificException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
throw new InvalidOperationException(); // unreachable
}
}
// ...and of course so should this.
internal static void MethodThatUsesTheValues()
{
var (firstVariable, secondVariable) = GetFirstAndSecondValues();
// Code that does something with them...
}
Esse código retorna e desconstrói um ValueTuple com a sintaxe do C # 7.0 para retornar vários valores, mas se você ainda estiver em uma versão anterior do C #, ainda poderá usar esta técnica; por exemplo, você pode usar parâmetros ou retornar um objeto personalizado que fornece os dois valores . Além disso, se as duas variáveis não estiverem realmente estreitamente relacionadas, provavelmente seria melhor ter dois métodos separados de qualquer maneira.
Especialmente se você tiver vários métodos como esse, considere centralizar seu código para notificar o usuário sobre erros fatais e sair. (Por exemplo, você pode escrever um Die
método com um message
parâmetro.) A throw new InvalidOperationException();
linha nunca é realmente executada; portanto, você não precisa (nem deve) escrever uma cláusula catch para ela.
Além de encerrar quando ocorre um erro específico, às vezes você pode escrever um código parecido com esse se lançar uma exceção de outro tipo que envolve a exceção original . (Nessa situação, você não precisaria de uma segunda expressão de lançamento inacessível.)
Conclusão: o escopo é apenas parte da imagem.
Você pode obter o efeito de agrupar seu código com tratamento de erros sem refatoração (ou, se preferir, com quase nenhuma refatoração), apenas separando as declarações das variáveis de suas atribuições. O compilador permite isso se você atender às regras de atribuição definidas do C # e declarar uma variável antes do bloco try deixa seu escopo maior claro. Mas refatorar ainda mais pode ser sua melhor opção.
try.. catch
é um tipo específico de bloco de código e, no que diz respeito a todos os blocos de código, você não pode declarar uma variável em um e usar a mesma variável em outro como uma questão de escopo.