Por que não usar exceções como fluxo regular de controle?


192

Para evitar todas as respostas padrão que eu poderia ter pesquisado no Google, darei um exemplo que todos vocês podem atacar à vontade.

C # e Java (e muitos outros) têm, com muitos tipos, alguns dos comportamentos de 'estouro' que eu não gosto (por type.MaxValue + type.SmallestValue == type.MinValueexemplo int.MaxValue + 1 == int.MinValue:).

Mas, vendo minha natureza cruel, adicionarei algum insulto a essa lesão expandindo esse comportamento para, digamos, um DateTimetipo Substituído . (Sei que DateTimeestá selado no .NET, mas pelo exemplo deste exemplo, estou usando uma pseudo linguagem que é exatamente como C #, exceto pelo fato de que o DateTime não está selado).

O Addmétodo substituído :

/// <summary>
/// Increments this date with a timespan, but loops when
/// the maximum value for datetime is exceeded.
/// </summary>
/// <param name="ts">The timespan to (try to) add</param>
/// <returns>The Date, incremented with the given timespan. 
/// If DateTime.MaxValue is exceeded, the sum wil 'overflow' and 
/// continue from DateTime.MinValue. 
/// </returns>
public DateTime override Add(TimeSpan ts) 
{
    try
    {                
        return base.Add(ts);
    }
    catch (ArgumentOutOfRangeException nb)
    {
        // calculate how much the MaxValue is exceeded
        // regular program flow
        TimeSpan saldo = ts - (base.MaxValue - this);
        return DateTime.MinValue.Add(saldo)                         
    }
    catch(Exception anyOther) 
    {
        // 'real' exception handling.
    }
}

É claro que um if poderia resolver isso da maneira mais fácil, mas o fato é que eu simplesmente não consigo entender por que você não pode usar exceções (logicamente, ou seja, eu posso ver que quando o desempenho é um problema, em certos casos as exceções devem ser evitadas). )

Eu acho que em muitos casos eles são mais claros que as estruturas if e não quebram nenhum contrato que o método está fazendo.

IMHO, a reação "Nunca os use para o fluxo regular do programa" que todo mundo parece ter não está tão bem construída quanto a força dessa reação pode justificar.

Ou estou enganado?

Eu li outras postagens, lidando com todos os tipos de casos especiais, mas o que quero dizer é que não há nada errado com isso, se vocês dois são:

  1. Claro
  2. Honre o contrato do seu método

Atire em mim.


2
+1 eu me sinto da mesma maneira. Além do desempenho, a única boa razão para evitar exceções para o fluxo de controle é quando o código do chamador será muito mais legível com os valores de retorno.

4
é o: return -1 se algo aconteceu, return -2 se algo mais, etc ... realmente mais legível que as exceções?
Kender

1
É triste que alguém tenha reputação negativa por dizer a verdade: que seu exemplo não poderia ter sido escrito com declarações if. (Isso não quer dizer que é correta / completa.)
Ingo

8
Eu diria que lançar uma exceção às vezes pode ser sua única opção. Por exemplo, eu tenho um componente de negócios que inicializa seu estado interno dentro de seu construtor, consultando o banco de dados. Há momentos em que nenhum dado apropriado no banco de dados está disponível. Lançar uma exceção no construtor é a única maneira de efetivamente cancelar a construção do objeto. Isso está claramente indicado no contrato (Javadoc no meu caso) da classe, portanto, não tenho nenhum problema que o código do cliente possa (e deva) capturar essa exceção ao criar o componente e continuar a partir daí.
Stefan Haberl

1
Desde que você formulou uma hipótese, cabe a você também citar evidências / razões corroboradoras. Para iniciantes, cite uma razão pela qual seu código é superior a uma ifdeclaração muito mais curta e auto-documentada . Você achará isso muito difícil. Em outras palavras: sua própria premissa é falha e as conclusões que você tira dela são, portanto, erradas.
Konrad Rudolph

Respostas:


169

Você já tentou depurar um programa que gera cinco exceções por segundo no curso normal da operação?

Eu tenho.

O programa era bastante complexo (era um servidor de cálculo distribuído) e uma pequena modificação em um lado do programa poderia facilmente quebrar algo em um lugar totalmente diferente.

Eu gostaria de ter iniciado o programa e aguardar a ocorrência de exceções, mas houve cerca de 200 exceções durante a inicialização no curso normal das operações

O que quero dizer : se você usa exceções para situações normais, como localiza situações incomuns (ou seja, todas as exceções )?

Obviamente, existem outros motivos fortes para não usar exceções demais, principalmente em termos de desempenho.


13
Exemplo: quando depuro um programa .net, inicio-o no visual studio e peço ao VS para interromper todas as exceções. Se você confiar nas exceções como um comportamento esperado, não posso mais fazer isso (já que isso quebraria 5 vezes / s), e é muito mais complicado localizar a parte problemática do código.
12119 Brann

15
+1 para apontar que você não deseja criar um palheiro de exceção para encontrar uma agulha excepcional real.
Grant Wagner

16
não recebo essa resposta, acho que as pessoas não entendem aqui. Não tem nada a ver com depuração, mas com design. Este é um raciocínio circular, porque tenho medo. E o seu ponto está realmente além da questão como afirmado anteriormente #
Peter

11
@ Peter: Depurar sem interromper exceções é difícil, e capturar todas as exceções é doloroso se houver muitas delas por design. Eu acho que um design que torna a depuração mais difícil é quase parcialmente quebrado (em outras palavras, design tem algo a ver com a depuração, IMO)
Brann

7
Mesmo ignorando o fato de que a maioria das situações que eu quero depurar não corresponde às exceções lançadas, a resposta para sua pergunta é: "por tipo", por exemplo, direi ao meu depurador para pegar apenas AssertionError ou StandardError ou algo que aconteça correspondem a coisas ruins acontecendo. Se você tiver problemas com isso, como faz o registro - não registra por nível e classe, precisamente para poder filtrá-los? Você também acha que é uma má ideia?
Ken

157

Exceções são basicamente gotodeclarações não locais , com todas as consequências deste último. O uso de exceções no controle de fluxo viola um princípio de menor espanto , dificulta a leitura dos programas (lembre-se de que os programas foram escritos primeiro para os programadores).

Além disso, não é isso que os fornecedores de compiladores esperam. Eles esperam que exceções sejam lançadas raramente, e geralmente deixam o throwcódigo ser ineficiente. Lançar exceções é uma das operações mais caras do .NET.

No entanto, algumas linguagens (principalmente Python) usam exceções como construções de controle de fluxo. Por exemplo, os iteradores geram uma StopIterationexceção se não houver mais itens. Até construções de linguagem padrão (como for) dependem disso.


11
ei, exceções não são surpreendentes! E você meio que se contradiz quando diz "é uma má ideia" e depois diz "mas é uma boa ideia em python".
hasen

6
Ainda não estou convencido: 1) Eficiência estava fora de questão, muitos programas que não são de barco não se importavam (por exemplo, interface do usuário) 2) Surpreendente: como eu disse, é simplesmente surpreendente, porque não é usado, mas a pergunta permanece: por que não usar id em primeiro lugar? Mas, uma vez que esta é a resposta #
Peter

4
+1 Na verdade, estou feliz que você apontou a distinção entre Python e C #. Eu não acho que é uma contradição. O Python é muito mais dinâmico e a expectativa de usar exceções dessa maneira é inserida na linguagem. Também faz parte da cultura EAFP do Python. Não sei qual abordagem é conceitualmente mais pura ou mais consistente, mas gosto da ideia de escrever código que faça o que os outros esperam que seja, o que significa estilos diferentes em idiomas diferentes.
Mark E. Haase

13
Ao contrário de goto, é claro, as exceções interagem corretamente com sua pilha de chamadas e com o escopo lexical e não deixam a pilha ou os escopos em uma bagunça.
Lukas Eder

4
Na verdade, a maioria dos fornecedores de VM espera exceções e as trata com eficiência. Como o @LukasEder observa, as exceções são totalmente diferentes das que são estruturadas.
Marcin

29

Minha regra de ouro é:

  • Se você puder fazer algo para se recuperar de um erro, capture exceções
  • Se o erro for muito comum (por exemplo, o usuário tentou fazer login com a senha incorreta), use returnvalues
  • Se você não puder fazer nada para se recuperar de um erro, deixe-o em branco (ou pegue-o no seu apanhador principal para fazer um desligamento sem graça do aplicativo)

O problema que vejo com exceções é do ponto de vista puramente de sintaxe (tenho certeza de que a sobrecarga de desempenho é mínima). Não gosto de blocos de tentativa por todo o lado.

Veja este exemplo:

try
{
   DoSomeMethod();  //Can throw Exception1
   DoSomeOtherMethod();  //Can throw Exception1 and Exception2
}
catch(Exception1)
{
   //Okay something messed up, but is it SomeMethod or SomeOtherMethod?
}

.. Outro exemplo pode ser quando você precisa atribuir algo a um identificador usando uma fábrica, e essa fábrica pode lançar uma exceção:

Class1 myInstance;
try
{
   myInstance = Class1Factory.Build();
}
catch(SomeException)
{
   // Couldn't instantiate class, do something else..
}
myInstance.BestMethodEver();   // Will throw a compile-time error, saying that myInstance is uninitalized, which it potentially is.. :(

Então, pessoalmente, acho que você deve manter exceções para condições de erro raras (falta de memória etc.) e usar valores de retorno (classes de valor, estruturas ou enumerações) para fazer sua verificação de erro.

Espero ter entendido sua pergunta correta :)


4
re: Seu segundo exemplo - por que não fazer a chamada para BestMethodEver dentro do bloco try, depois do Build? Se Build () lançar uma exceção, ela não será executada e o compilador ficará satisfeito.
Blorgbeard saiu em 08/04/09

2
Sim, provavelmente seria com isso que você terminará, mas considere um exemplo mais complexo em que o próprio tipo myInstance pode gerar exceções. E outras instâncias no escopo do método também podem. Você vai acabar com um monte de blocos try / catch aninhados :(
cwap

Você deve fazer a conversão de exceção (para um tipo de exceção apropriado ao nível de abstração) no seu bloco de captura. FYI: "Multi-catch" é suposto estar indo para Java 7.
jasonnerothin

FYI: No C ++, você pode colocar várias capturas após uma tentativa de capturar exceções diferentes.
RobH

2
Para o software shrinkwrap, você precisa capturar todas as exceções. Pelo menos, abra uma caixa de diálogo que explica que o programa precisa desligar e aqui está algo incompreensível que você pode enviar em um relatório de erro.
22615 David Thornley

25

Uma primeira reação a muitas respostas:

você está escrevendo para os programadores e o princípio de menos espanto

Claro! Mas um se simplesmente não é mais claro o tempo todo.

Não deveria ser surpreendente, por exemplo: dividir (1 / x) catch (divisionByZero) é mais claro do que qualquer outro para mim (no Conrad e outros). O fato de esse tipo de programação não ser esperado é puramente convencional e, de fato, ainda é relevante. Talvez no meu exemplo um if seria mais claro.

Mas DivisionByZero e FileNotFound são mais claros do que ifs.

É claro que se tiver menos desempenho e precisar de um zilhão de vezes por segundo, é claro que você deve evitá-lo, mas ainda não li nenhuma boa razão para evitar o design geral.

No que diz respeito ao princípio de menor espanto: existe o perigo de raciocínio circular aqui: suponha que uma comunidade inteira use um design ruim, esse design será esperado! Portanto, o princípio não pode ser um graal e deve ser considerado com cuidado.

exceções para situações normais, como você localiza situações incomuns (isto é, excepcionais)?

Em muitas reações sth. assim brilha através. Apenas pegue eles, não? Seu método deve ser claro, bem documentado e com clareza de contrato. Não entendi essa pergunta, devo admitir.

Depurando em todas as exceções: o mesmo, isso é feito algumas vezes porque o design para não usar exceções é comum. Minha pergunta era: por que isso é comum em primeiro lugar?


1
1) Você sempre verifica xantes de ligar 1/x? 2) Você envolve todas as operações de divisão em um bloco try-catch para pegar DivideByZeroException? 3) Qual lógica você coloca no bloco catch para recuperar DivideByZeroException?
Lightman

1
Exceto DivisionByZero e FileNotFound são exemplos ruins, pois são casos excepcionais que devem ser tratados como exceções.
0x6C38 28/05

Não há nada de "excessivamente excepcional" em um arquivo não encontrado da maneira que as pessoas "anti-exceção" aqui estão divulgando. openConfigFile();pode ser seguido por uma { createDefaultConfigFile(); setFirstAppRun(); }exceção FileNotFound capturada com FileNotFound manipulada normalmente; sem falhas, vamos melhorar a experiência do usuário final, não pior. Você pode dizer "Mas e se essa não fosse realmente a primeira corrida e eles conseguissem isso toda vez?" Pelo menos o aplicativo é executado todas as vezes e não está travando em todas as startups! Em um número de 1 a 10, "isso é terrível": "primeira execução" a cada inicialização = 3 ou 4, trava a cada inicialização = 10. #
Loduwijk #

Seus exemplos são exceções. Não, você nem sempre verifica xantes de ligar 1/x, porque geralmente é bom. O caso excepcional é o caso em que não está bem. Não estamos falando aqui de abalar a terra, mas, por exemplo, para um número inteiro básico dado aleatoriamente x, apenas 1 em 4294967296 falharia em fazer a divisão. Isso é excepcional e as exceções são uma boa maneira de lidar com isso. No entanto, você pode usar exceções para implementar o equivalente a uma switchinstrução, mas seria bem bobo.
precisa saber é o seguinte

16

Antes das exceções, em C, havia setjmpe longjmpque poderia ser usado para realizar um desenrolamento semelhante do quadro da pilha.

Em seguida, a mesma construção recebeu um nome: "Exceção". E a maioria das respostas se baseia no significado desse nome para argumentar sobre seu uso, alegando que as exceções devem ser usadas em condições excepcionais. Essa nunca foi a intenção no original longjmp. Havia apenas situações em que você precisava interromper o fluxo de controle em muitos quadros de pilha.

As exceções são um pouco mais gerais, pois você também pode usá-las no mesmo quadro de pilha. Isso levanta analogias com as gotoquais acredito estarem erradas. Gotos são um par fortemente acoplado (e o são setjmpe longjmp). As exceções seguem uma publicação / assinatura pouco acoplada que é muito mais limpa! Portanto, usá-los no mesmo quadro de pilha dificilmente é a mesma coisa que usar gotos.

A terceira fonte de confusão refere-se ao fato de serem exceções marcadas ou desmarcadas. Obviamente, exceções não verificadas parecem particularmente terríveis para usar no fluxo de controle e talvez em muitas outras coisas.

No entanto, as exceções verificadas são ótimas para controlar o fluxo, uma vez que você supera todas as dificuldades vitorianas e vive um pouco.

Meu uso favorito é uma sequência de throw new Success()um longo fragmento de código que tenta uma coisa após a outra até encontrar o que está procurando. Cada coisa - cada parte da lógica - pode ter um aninhamento arbitrário, assim breakcomo também qualquer tipo de teste de condição. O if-elsepadrão é quebradiço. Se eu editar elseou alterar a sintaxe de alguma outra maneira, haverá um bug peludo.

Usar throw new Success() lineariza o fluxo de código. Uso Successclasses definidas localmente - verificadas, é claro - para que, se eu esquecer de pegá-lo, o código não seja compilado. E eu não entendo Successes de outro método .

Às vezes, meu código verifica uma coisa após a outra e só é bem-sucedida se tudo estiver correto. Neste caso, eu tenho uma linearização semelhante usando throw new Failure().

O uso de uma função separada interfere com o nível natural de compartimentação. Portanto, a returnsolução não é ótima. Prefiro ter uma ou duas páginas de código em um único local por razões cognitivas. Não acredito em códigos divididos de maneira ultra fina.

O que as JVMs ou os compiladores fazem é menos relevante para mim, a menos que haja um ponto de acesso. Não acredito que exista qualquer razão fundamental para os compiladores não detectarem exceções lançadas e capturadas localmente e simplesmente tratá-las como gotos muito eficientes no nível do código da máquina.

Quanto a usá-los em funções para controle de fluxo - ou seja, para casos comuns e não para casos excepcionais - não vejo como eles seriam menos eficientes do que múltiplas interrupções, testes de condição e retornos para percorrer três quadros de pilha, em vez de apenas restaurar o ponteiro da pilha.

Pessoalmente, não uso o padrão entre os quadros de pilha e posso ver como isso exigiria sofisticação do design para fazê-lo com elegância. Mas, com moderação, deve ficar bem.

Por fim, em relação a programadores virgens surpreendentes, não é uma razão convincente. Se você gentilmente apresentá-los à prática, eles aprenderão a amá-la. Lembro que o C ++ costumava surpreender e assustar os programadores em C.


3
Usando esse padrão, a maioria das minhas funções grosseiras tem duas pequenas capturas no final - uma para o Sucesso e outra para a Falha, e é aí que a função envolve coisas como preparar a resposta correta do servlet ou preparar valores de retorno. Ter um único local para fazer o encerramento é bom. A returnalternativa -pattern exigiria duas funções para cada uma dessas funções. Um externo para preparar a resposta do servlet ou outras ações desse tipo e um interno para fazer o cálculo. PS: Professor Um Inglês provavelmente sugere que eu uso "surpreendente" ao invés de "surpreendente" no último parágrafo :-)
necromante

11

A resposta padrão é que as exceções não são regulares e devem ser usadas em casos excepcionais.

Uma razão, que é importante para mim, é que, quando leio uma try-catchestrutura de controle em um software que mantenho ou depuro, tento descobrir por que o codificador original usou um tratamento de exceção em vez de uma if-elseestrutura. E espero encontrar uma boa resposta.

Lembre-se de que você escreve código não apenas para o computador, mas também para outros codificadores. Existe uma semântica associada a um manipulador de exceções que você não pode jogar fora apenas porque a máquina não se importa.


Uma resposta subestimada, eu acho. O computador pode não desacelerar muito quando encontra uma exceção sendo engolida, mas quando estou trabalhando no código de outra pessoa e me deparo com ele, ele me impede de seguir em frente quando resolvo se perdi algo importante que não fiz. não sei, ou se realmente não há justificativa para o uso desse anti-padrão.
Tim Abell

9

E quanto ao desempenho? Durante o teste de carga de um aplicativo da Web .NET, alcançamos 100 usuários simulados por servidor da Web até corrigirmos uma exceção comum e esse número aumentou para 500 usuários.


8

Josh Bloch lida com esse tópico extensivamente em Java eficaz. Suas sugestões são esclarecedoras e devem se aplicar ao .Net também (exceto pelos detalhes).

Em particular, as exceções devem ser usadas em circunstâncias excepcionais. As razões para isso estão relacionadas à usabilidade, principalmente. Para que um método seja usável ao máximo, suas condições de entrada e saída devem ser restringidas ao máximo.

Por exemplo, o segundo método é mais fácil de usar que o primeiro:

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 * @throws AdditionException if addend1 or addend2 is less than or equal to zero
 */
int addPositiveNumbers(int addend1, int addend2) throws AdditionException{
  if( addend1 <= 0 ){
     throw new AdditionException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new AdditionException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 */
public int addPositiveNumbers(int addend1, int addend2) {
  if( addend1 <= 0 ){
     throw new IllegalArgumentException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new IllegalArgumentException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

Em qualquer um dos casos, você precisa verificar se o chamador está usando sua API adequadamente. Mas no segundo caso, você precisa (implicitamente). As exceções suaves ainda serão lançadas se o usuário não leu o javadoc, mas:

  1. Você não precisa documentá-lo.
  2. Você não precisa testá-lo (dependendo de quão agressiva é sua estratégia de teste de unidade).
  3. Você não precisa que o chamador lide com três casos de uso.

O ponto principal é que as exceções não devem ser usadas como códigos de retorno, principalmente porque você complicou não apenas a SUA API, mas também a API do chamador.

Fazer a coisa certa tem um custo, é claro. O custo é que todos precisam entender que precisam ler e seguir a documentação. Espero que seja o caso de qualquer maneira.


7

Eu acho que você pode usar exceções para controle de fluxo. Existe, no entanto, um outro lado dessa técnica. Criar exceções é caro, porque eles precisam criar um rastreamento de pilha. Portanto, se você deseja usar Exceções com mais frequência do que apenas para sinalizar uma situação excepcional, é necessário garantir que a construção dos rastreamentos de pilha não influencie negativamente seu desempenho.

A melhor maneira de reduzir o custo de criação de exceções é substituir o método fillInStackTrace () como este:

public Throwable fillInStackTrace() { return this; }

Essa exceção não terá nenhum rastreamento de pilha preenchido.


O stacktrace também exige que o chamador "conheça" (ou seja, tenha uma dependência) de todos os Throwables na pilha. Isso é uma coisa ruim. Lance exceções apropriadas ao nível de abstração (ServiceExceptions in Services, DaoExceptions from Dao method, etc). Apenas traduza se necessário.
jasonnerothin

5

Realmente não vejo como você está controlando o fluxo do programa no código que você citou. Você nunca verá outra exceção além da exceção ArgumentOutOfRange. (Portanto, sua segunda cláusula catch nunca será atingida). Tudo o que você está fazendo é usar um arremesso extremamente caro para imitar uma declaração if.

Além disso, você não está executando as operações mais sinistras em que apenas lança uma exceção para que ela seja capturada em outro lugar para executar o controle de fluxo. Você está realmente lidando com um caso excepcional.


5

Aqui estão as práticas recomendadas que descrevi na minha postagem no blog :

  • Lance uma exceção para indicar uma situação inesperada no seu software.
  • Use valores de retorno para validação de entrada .
  • Se você souber lidar com exceções que uma biblioteca lança, identifique-as no nível mais baixo possível .
  • Se você tiver uma exceção inesperada, descarte completamente a operação atual. Não finja que você sabe lidar com eles .

4

Como o código é difícil de ler, você pode ter problemas para depurá-lo, introduzirá novos bugs ao corrigi-los depois de um longo período de tempo, é mais caro em termos de recursos e tempo e o incomoda se você estiver depurando seu código e o depurador pára na ocorrência de todas as exceções;)


4

Além dos motivos expostos, um dos motivos para não usar exceções no controle de fluxo é que ele pode complicar bastante o processo de depuração.

Por exemplo, quando estou tentando rastrear um bug no VS, normalmente ativarei "interromper todas as exceções". Se você estiver usando exceções para controle de fluxo, estarei quebrando o depurador regularmente e terei que continuar ignorando essas exceções não excepcionais até que eu chegue ao problema real. É provável que isso enlouqueça alguém!


1
Eu já entreguei esse item: Depurando em todas as exceções: o mesmo foi feito porque o design para não usar exceções é comum. Minha pergunta era: por que isso é comum em primeiro lugar?
Peter

Então, sua resposta é basicamente "É ruim porque o Visual Studio tem esse recurso ..."? Estou programando há cerca de 20 anos e nem percebi que havia uma opção de "interrupção em todas as exceções". Ainda assim, "por causa dessa característica!" parece uma razão fraca. Basta rastrear a exceção à sua origem; espero que você esteja usando uma linguagem que facilite isso - caso contrário, seu problema é com os recursos da linguagem, não com o uso geral das próprias exceções.
Loduwijk

3

Vamos supor que você tenha um método que faça alguns cálculos. Existem muitos parâmetros de entrada que ele precisa validar e, em seguida, retornar um número maior que 0.

Usando valores de retorno para sinalizar erro de validação, é simples: se o método retornou um número menor que 0, ocorreu um erro. Como saber então qual parâmetro não foi validado?

Lembro-me dos meus dias C muitas funções retornaram códigos de erro como este:

-1 - x lesser then MinX
-2 - x greater then MaxX
-3 - y lesser then MinY

etc.

É realmente menos legível do que lançar e capturar uma exceção?


É por isso que eles inventaram enums :) Mas números mágicos é um assunto completamente diferente .. en.wikipedia.org/wiki/...
Isak Savo

Ótimo exemplo. Eu estava prestes a escrever a mesma coisa. @IsakSavo: Enums não são úteis nessa situação se se espera que o método retorne algum valor ou objeto de significado. Por exemplo, getAccountBalance () deve retornar um objeto Money, não um objeto AccountBalanceResultEnum. Muitos programas C têm um padrão semelhante, em que um valor sentinela (0 ou nulo) representa um erro e, em seguida, é necessário chamar outra função para obter um código de erro separado para determinar por que o erro ocorreu. (A API MySQL C é assim.)
Mark E. Haase

3

Você pode usar uma garra de martelo para girar um parafuso, assim como você pode usar exceções para controlar o fluxo. Isso não significa que é o uso pretendido do recurso. A ifdeclaração expressa condições, cujo uso pretendido é controlar o fluxo.

Se você estiver usando um recurso de forma não intencional e optar por não usar o recurso projetado para esse fim, haverá um custo associado. Nesse caso, a clareza e o desempenho não sofrem nenhum valor agregado real. O que o uso de exceções lhe dá sobre a ifdeclaração amplamente aceita ?

Disse de outra maneira: só porque você pode , não significa que deveria .


1
Você está dizendo que não há necessidade de exceção, depois que obtivemos ifo uso normal ou o uso de execuções não se destina porque não se destina (argumento circular)?
Val

1
@ Val: Exceções são para situações excepcionais - se detectarmos o suficiente para lançar uma exceção e lidar com isso, temos informações suficientes para não lançá-lo e ainda lidar com isso. Podemos ir direto à lógica de manipulação e pular a tentativa / captura cara e supérflua.
Bryan Watts

Por essa lógica, é possível que você também não tenha exceções e sempre faça uma saída do sistema em vez de lançar uma exceção. Se você quiser fazer alguma coisa antes de sair, faça um invólucro e chame-o. Exemplo de Java: public class ExitHelper{ public static void cleanExit() { cleanup(); System.exit(1); } }Em seguida, basta chamar isso em vez de lançar: ExitHelper.cleanExit();Se o seu argumento fosse sólido, essa seria a abordagem preferida e não haveria exceções. Você está basicamente dizendo "A única razão para exceções é travar de uma maneira diferente".
Loduwijk

@ Aaron: Se eu lançar e capturar a exceção, tenho informações suficientes para evitar isso. Isso não significa que todas as exceções são subitamente fatais. Outro código que eu não controlo pode pegá-lo, e isso é bom. Meu argumento, que permanece sólido, é que lançar e capturar uma exceção no mesmo contexto é supérfluo. Não declarei nem declarei que todas as exceções devem sair do processo.
Bryan Watts

@BryanWatts Reconhecido. Muitos outros têm disse que você só deve estar usando exceções para qualquer coisa que você não pode recuperar, e por extensão deve ser sempre batendo nas exceções. É por isso que é difícil discutir essas coisas; Não existem apenas 2 opiniões, mas muitas. Ainda discordo de você, mas não fortemente. Há momentos em que jogar / capturar juntos é o código mais legível, sustentável e com melhor aparência; normalmente isso acontece se você já está capturando outras exceções e, portanto, já tenta / captura e adicionar 1 ou 2 capturas a mais é mais limpo do que as verificações de erro separadas if.
Loduwijk 28/07

3

Como outros já mencionaram numerosamente, o princípio de menor espanto proibirá o uso excessivo de exceções apenas para fins de controle de fluxo. Por outro lado, nenhuma regra é 100% correta e sempre há casos em que uma exceção é "a ferramenta certa" - a exemplo de gotosi mesma, a propósito, que é fornecida na forma breake continueem linguagens como Java, que geralmente são a maneira perfeita de saltar de loops muito aninhados, que nem sempre são evitáveis.

A seguinte postagem no blog explica um caso de uso bastante complexo, mas também bastante interessante, para um não local ControlFlowException :

Ele explica como, dentro do jOOQ (uma biblioteca de abstração SQL para Java) , essas exceções são ocasionalmente usadas para interromper o processo de renderização SQL mais cedo, quando alguma condição "rara" é atendida.

Exemplos de tais condições são:

  • Muitos valores de ligação são encontrados. Alguns bancos de dados não oferecem suporte a números arbitrários de valores de ligação em suas instruções SQL (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). Nesses casos, o jOOQ interrompe a fase de renderização do SQL e renderiza novamente a instrução SQL com valores de ligação embutidos. Exemplo:

    // Pseudo-code attaching a "handler" that will
    // abort query rendering once the maximum number
    // of bind values was exceeded:
    context.attachBindValueCounter();
    String sql;
    try {
    
      // In most cases, this will succeed:
      sql = query.render();
    }
    catch (ReRenderWithInlinedVariables e) {
      sql = query.renderWithInlinedBindValues();
    }

    Se extraíssemos explicitamente os valores de ligação da consulta AST para contá-los todas as vezes, desperdiçamos valiosos ciclos de CPU para aquelas 99,9% das consultas que não sofrem com esse problema.

  • Alguma lógica está disponível apenas indiretamente por meio de uma API que queremos executar apenas "parcialmente". O UpdatableRecord.store()método gera uma instrução INSERTou UPDATE, dependendo Recorddos sinalizadores internos do. Do lado de fora, não sabemos em que tipo de lógica está contida store()(por exemplo, bloqueio otimista, manipulação de ouvinte de eventos etc.), portanto, não queremos repetir essa lógica quando armazenamos vários registros em uma instrução em lote, onde gostaríamos de store()gerar apenas a instrução SQL e não executá-la. Exemplo:

    // Pseudo-code attaching a "handler" that will
    // prevent query execution and throw exceptions
    // instead:
    context.attachQueryCollector();
    
    // Collect the SQL for every store operation
    for (int i = 0; i < records.length; i++) {
      try {
        records[i].store();
      }
    
      // The attached handler will result in this
      // exception being thrown rather than actually
      // storing records to the database
      catch (QueryCollectorException e) {
    
        // The exception is thrown after the rendered
        // SQL statement is available
        queries.add(e.query());                
      }
    }

    Se tivéssemos externalizado a store()lógica na API "reutilizável" que pode ser personalizada para opcionalmente não executar o SQL, estaríamos tentando criar uma API bastante difícil de manter e dificilmente reutilizável.

Conclusão

Em essência, nosso uso desses recursos não locais goto s é exatamente como o que [Mason Wheeler] [5] disse em sua resposta:

"Acabei de encontrar uma situação com a qual não posso lidar adequadamente neste momento, porque não tenho contexto suficiente para lidar com isso, mas a rotina que me chamou (ou algo mais acima na pilha de chamadas) deve saber como lidar com isso. . "

Ambos os usos ControlFlowExceptionsforam bastante fáceis de implementar em comparação com suas alternativas, permitindo reutilizar uma ampla gama de lógicas sem refatorá-la dos internos relevantes.

Mas a sensação de que isso é uma surpresa para os futuros mantenedores permanece. O código parece bastante delicado e, embora tenha sido a escolha certa nesse caso, sempre preferimos não usar exceções no fluxo de controle local , onde é fácil evitar o uso de ramificações comuns if - else.


2

Normalmente, não há nada errado, por si só, em lidar com uma exceção em um nível baixo. Uma exceção É uma mensagem válida que fornece muitos detalhes sobre por que uma operação não pode ser executada. E se você pode lidar com isso, você deve.

Em geral, se você sabe que existe uma alta probabilidade de falha que você pode verificar ... você deve fazer a verificação ... ou seja, se (obj! = Null) obj.method ()

No seu caso, não estou familiarizado o suficiente com a biblioteca C # para saber se a data e hora tem uma maneira fácil de verificar se um carimbo de data / hora está fora dos limites. Se isso acontecer, basta chamar if (.isvalid (ts)), caso contrário, seu código está basicamente bom.

Então, basicamente, tudo se resume à maneira como cria um código mais limpo ... se a operação para proteger contra uma exceção esperada for mais complexa do que apenas manipular a exceção; do que você tem minha permissão para lidar com a exceção, em vez de criar guardas complexos em todos os lugares.


Ponto adicional: se sua Exceção fornecer informações de captura de falha (um getter como "Param getWhatParamMessedMeUp ()"), isso pode ajudar o usuário da sua API a tomar uma boa decisão sobre o que fazer em seguida. Caso contrário, você está apenas dando um nome a um estado de erro.
jasonnerothin

2

Se você estiver usando manipuladores de exceção para o fluxo de controle, estará sendo muito geral e preguiçoso. Como outra pessoa mencionou, você sabe que algo aconteceu se estiver manipulando o processamento no manipulador, mas o que exatamente? Essencialmente, você está usando a exceção para uma instrução else, se estiver usando-a para o fluxo de controle.

Se você não souber qual estado possível pode ocorrer, poderá usar um manipulador de exceções para estados inesperados, por exemplo, quando precisar usar uma biblioteca de terceiros ou capturar tudo na interface do usuário para mostrar um bom erro mensagem e registre a exceção.

No entanto, se você sabe o que pode dar errado e não coloca uma declaração if ou algo para verificar, está apenas sendo preguiçoso. É preguiçoso permitir que o manipulador de exceções seja o item-chave de tudo que você sabe que pode acontecer e ele voltará a assombrá-lo mais tarde, porque você estará tentando corrigir uma situação no manipulador de exceções com base em uma suposição possivelmente falsa.

Se você colocar a lógica no seu manipulador de exceções para determinar o que exatamente aconteceu, seria bastante estúpido por não colocar essa lógica dentro do bloco try.

Manipuladores de exceção são o último recurso, pois quando você fica sem ideias / maneiras de impedir que algo dê errado, ou as coisas estão além da sua capacidade de controlar. Como, o servidor está inoperante e atinge o tempo limite e você não pode impedir que essa exceção seja lançada.

Finalmente, fazer todas as verificações antecipadamente mostra o que você sabe ou espera que ocorra e a torna explícita. O código deve ser claro na intenção. O que você prefere ler?


1
Não é verdade: "Essencialmente, você está usando a exceção para uma instrução else, se estiver usando-a para o fluxo de controle." Se você a usa para o fluxo de controle, sabe exatamente o que capturar e nunca usa uma captura geral, mas um específico, é claro!
Peter

2

Você pode estar interessado em dar uma olhada no sistema de condições do Common Lisp, que é uma espécie de generalização de exceções feitas corretamente. Como você pode desenrolar a pilha ou não de maneira controlada, também obtém "reinicializações", o que é extremamente útil.

Isso não tem muito a ver com as práticas recomendadas em outros idiomas, mas mostra o que pode ser feito com algum pensamento de design (aproximadamente) na direção em que você está pensando.

É claro que ainda existem considerações de desempenho se você está pulando para cima e para baixo na pilha como um ioiô, mas é uma idéia muito mais geral do que o tipo de abordagem "oh merda, vamos salvá-la" que a maioria dos sistemas de exceção captura / joga incorporar.


2

Acho que não há nada de errado em usar exceções para controle de fluxo. As exceções são um pouco semelhantes às continuações e nas linguagens estaticamente tipadas; as exceções são mais poderosas que as continuações; portanto, se você precisar de continuações, mas seu idioma não as tiver, poderá usar as exceções para implementá-las.

Bem, na verdade, se você precisar de continuações e seu idioma não as possuir, você escolheu o idioma errado e deve usar um idioma diferente. Mas às vezes você não tem escolha: a programação da Web do lado do cliente é o principal exemplo - não há como evitar o JavaScript.

Um exemplo: Microsoft Volta é um projeto que permite gravar aplicativos da Web no .NET direto e permitir que a estrutura decida quais bits precisam ser executados para onde. Uma consequência disso é que Volta precisa poder compilar CIL para JavaScript, para que você possa executar o código no cliente. No entanto, há um problema: o .NET possui multithreading, o JavaScript não. Portanto, Volta implementa continuações em JavaScript usando Exceções de JavaScript e implementa Threads .NET usando essas continuações. Dessa forma, os aplicativos Volta que usam threads podem ser compilados para serem executados em um navegador não modificado - não é necessário o Silverlight.


2

Uma razão estética:

Uma tentativa sempre vem com uma pegadinha, enquanto uma if não precisa vir com outra.

if (PerformCheckSucceeded())
   DoSomething();

Com try / catch, torna-se muito mais detalhado.

try
{
   PerformCheckSucceeded();
   DoSomething();
}
catch
{
}

São 6 linhas de código a mais.


1

Mas você nem sempre saberá o que acontece nos métodos que você chama. Você não saberá exatamente onde a exceção foi lançada. Sem examinar o objeto de exceção em mais detalhes ....


1

Eu sinto que não há nada errado com o seu exemplo. Pelo contrário, seria um pecado ignorar a exceção lançada pela função chamada.

Na JVM, lançar uma exceção não é tão caro, apenas criando a exceção com o novo xyzException (...), porque o último envolve uma caminhada de pilha. Portanto, se você tiver algumas exceções criadas com antecedência, poderá lançá-las várias vezes sem custos. Obviamente, dessa forma, você não pode passar dados junto com a exceção, mas acho que é uma coisa ruim a se fazer.


Desculpe, isso está errado, Brann. Depende da condição. Nem sempre é o caso de a condição ser trivial. Assim, uma declaração if pode levar horas, dias ou até mais.
Ingo

Na JVM, é isso. Não é mais caro do que um retorno. Vai saber. Mas a questão é: o que você escreveria na instrução if, se não o próprio código que já está presente na função chamada para diferenciar um caso excepcional de um caso normal - portanto, duplicação de código.
Ingo

1
Ingo: uma situação excepcional é aquela que você não espera. isto é, um que você não considerou um programador. Assim, a minha regra é "escrever código que não lançar exceções" :)
Brann

1
Eu nunca escrevo manipuladores de exceção, sempre soluciono o problema (exceto quando não posso fazer isso porque não tenho controle sobre o código com falha). E nunca lanço exceções, exceto quando o código que escrevi se destina a outra pessoa (por exemplo, uma biblioteca). Não me mostra a contradição?
11119 Brann

1
Concordo com você em não lançar exceções descontroladamente. Mas certamente, o que é "excepcional" é uma questão de definição. Esse String.parseDouble lança uma exceção se não puder fornecer um resultado útil, por exemplo, está em ordem. O que mais deveria fazer? Retornar NaN? E quanto ao hardware não IEEE?
Ingo

1

Existem alguns mecanismos gerais através dos quais um idioma pode permitir que um método saia sem retornar um valor e desenrolar no próximo bloco "catch":

  • Faça com que o método examine o quadro da pilha para determinar o site de chamada e use os metadados do site de chamada para encontrar informações sobre um trybloco no método de chamada ou o local em que o método de chamada armazenou o endereço do chamador; na última situação, examine os metadados para determinar o chamador do chamador da mesma maneira que o chamador imediato, repetindo até encontrar um trybloco ou a pilha estar vazia. Essa abordagem adiciona muito pouca sobrecarga ao caso de nenhuma exceção (impede algumas otimizações), mas é cara quando ocorre uma exceção.

  • Faça com que o método retorne um sinalizador "oculto", que distingue um retorno normal de uma exceção, e faça com que o chamador verifique esse sinalizador e ramifique para uma rotina de "exceção", se estiver definido. Essa rotina adiciona 1-2 instruções ao caso de exceção, mas relativamente pouca sobrecarga quando ocorre uma exceção.

  • Faça com que o chamador coloque as informações ou o código de tratamento de exceções em um endereço fixo relativo ao endereço de retorno empilhado. Por exemplo, com o ARM, em vez de usar a instrução "sub-rotina BL", pode-se usar a sequência:

        adr lr,next_instr
        b subroutine
        b handle_exception
    next_instr:
    

Para sair normalmente, a sub-rotina faria simplesmente bx lrou pop {pc}; no caso de uma saída anormal, a sub-rotina subtrairia 4 da LR antes de executar o retorno ou o uso sub lr,#4,pc(dependendo da variação do BRAÇO, modo de execução etc.) Essa abordagem funcionará muito mal se o chamador não for projetado para acomodá-la.

Uma linguagem ou estrutura que utiliza exceções verificadas pode se beneficiar com a manipulação de mecanismos como os nºs 2 ou 3 acima, enquanto exceções não verificadas são tratadas com o nº 1. Embora a implementação de exceções verificadas em Java seja bastante incômoda, elas não seriam um conceito ruim se houvesse um meio pelo qual um site de chamada pudesse dizer, essencialmente: "Este método é declarado como jogando XX, mas não espero que fazer isso, se houver, repetir novamente como uma exceção "não verificada" .Em uma estrutura em que as exceções verificadas foram tratadas dessa maneira, elas poderiam ser um meio eficaz de controle de fluxo para coisas como métodos de análise que, em alguns contextos, podem ter um alta probabilidade de falha, mas onde a falha deve retornar informações fundamentalmente diferentes de sucesso. no entanto, não conheço nenhuma estrutura que use esse padrão. Em vez disso, o padrão mais comum é usar a primeira abordagem acima (custo mínimo para o caso sem exceção, mas alto custo quando são lançadas exceções) para todas as exceções.

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.