Quero adicionar tratamento de erros a:
var firstVariable = 1;
var secondVariable = firstVariable;
O abaixo não será compilado:
try
{
var firstVariable = 1;
}
catch {}
try
{
var secondVariable = firstVariable;
}
catch {}
Por que é necessário que um bloco try catch afete o escopo das variáveis como outros blocos de código? 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?
c#
.net
error-handling
language-features
JᴀʏMᴇᴇ
fonte
fonte
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.{}
sem a tentativa.Respostas:
E se o seu código fosse:
Agora você estaria tentando usar uma variável não declarada (
firstVariable
) se sua chamada de método for lançada.Nota : O exemplo acima responde especificamente à pergunta original, que indica "sake de consistência à parte". Isso demonstra que há outras razões além da consistência. Mas, como mostra a resposta de Pedro, também há um argumento poderoso da consistência, que com certeza teria sido um fator muito importante na decisão.
fonte
switch
e acessá-las em outras.) Essa regra pode ser aplicada facilmente aqui e impedir que esse código seja compilado de qualquer maneira. Eu acho que a resposta de Peter abaixo é mais plausível.catch
bloco e, em seguida, ela seria definitivamente atribuída no segundotry
bloco.Eu sei que isso foi bem respondido por Ben, mas eu queria abordar o ponto de vista da consistência que foi convenientemente deixado de lado. Supondo que os
try/catch
blocos não afetassem o escopo, você terminaria com:E, para mim, isso colide com o princípio do menor espanto (POLA), porque agora você tem o dever
{
e}
cumpre o duplo dever, dependendo do contexto do que os precedeu.A única maneira de sair dessa bagunça é designar outro marcador para delinear
try/catch
blocos. O que começa a adicionar um cheiro de código. Então, quando você não tem escopotry/catch
no idioma, teria sido uma bagunça que você estaria melhor com a versão com escopo.fonte
try
/catch
bloquear". - você quer dizertry { { // scope } }
:? :){}
um dever duplo como escopo e não criação de escopo, dependendo do contexto ainda.try^ //no-scope ^
seria um exemplo de um marcador diferente.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:
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:
Isso dará um erro em tempo de compilação :
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:
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 quen
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:
Ou:
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:
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
esecondVariable
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
esecondVariable
.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:
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 ummessage
parâmetro.) Athrow 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.
fonte