Qual, e por que, você prefere exceções ou códigos de retorno?

93

Minha pergunta é o que a maioria dos desenvolvedores prefere para tratamento de erros, exceções ou códigos de retorno de erro. Por favor, seja específico do idioma (ou família de idiomas) e por que você prefere um em vez do outro.

Estou perguntando isso por curiosidade. Pessoalmente, prefiro códigos de retorno de erro, pois são menos explosivos e não forçam o código do usuário a pagar a penalidade de desempenho de exceção se não quiserem.

atualização: obrigado por todas as respostas! Devo dizer que, embora eu não goste da imprevisibilidade do fluxo de código com exceções. A resposta sobre o código de retorno (e os identificadores de seu irmão mais velho) adiciona muito Noise ao código.

Robert Gould
fonte
1
Um problema da galinha ou do ovo do mundo do software ... eternamente discutível. :)
Gishu
Desculpe por isso, mas espero que ter uma variedade de opiniões ajude as pessoas (eu inclusive) a escolher apropriadamente.
Robert Gould

Respostas:

107

Para algumas linguagens (por exemplo, C ++), o vazamento de recursos não deve ser uma razão

C ++ é baseado em RAII.

Se você tem um código que pode falhar, retornar ou lançar (ou seja, a maioria dos códigos normais), então você deve ter seu ponteiro dentro de um ponteiro inteligente (supondo que você tenha um bom motivo para não ter seu objeto criado na pilha).

Os códigos de retorno são mais detalhados

Eles são prolixos e tendem a se desenvolver em algo como:

if(doSomething())
{
   if(doSomethingElse())
   {
      if(doSomethingElseAgain())
      {
          // etc.
      }
      else
      {
         // react to failure of doSomethingElseAgain
      }
   }
   else
   {
      // react to failure of doSomethingElse
   }
}
else
{
   // react to failure of doSomething
}

No final, seu código é uma coleção de instruções identificadas (eu vi esse tipo de código no código de produção).

Este código pode muito bem ser traduzido em:

try
{
   doSomething() ;
   doSomethingElse() ;
   doSomethingElseAgain() ;
}
catch(const SomethingException & e)
{
   // react to failure of doSomething
}
catch(const SomethingElseException & e)
{
   // react to failure of doSomethingElse
}
catch(const SomethingElseAgainException & e)
{
   // react to failure of doSomethingElseAgain
}

Que separa claramente o código do processamento de erros, o que pode ser uma coisa boa .

Códigos de retorno são mais frágeis

Se não for algum aviso obscuro de um compilador (veja o comentário de "phjr"), eles podem ser facilmente ignorados.

Com os exemplos acima, suponha que alguém se esquece de lidar com seu possível erro (isso acontece ...). O erro é ignorado quando "retornado" e possivelmente explodirá mais tarde (ou seja, um ponteiro NULL). O mesmo problema não acontecerá com exceção.

O erro não será ignorado. Às vezes, você quer que ele não exploda ... Portanto, você deve escolher com cuidado.

Códigos de retorno às vezes devem ser traduzidos

Digamos que temos as seguintes funções:

  • doSomething, que pode retornar um int chamado NOT_FOUND_ERROR
  • doSomethingElse, que pode retornar um bool "falso" (para falha)
  • doSomethingElseAgain, que pode retornar um objeto Error (com as variáveis ​​__LINE__, __FILE__ e metade da pilha.
  • do TryToDoSomethingWithAllThisMess que, bem ... Use as funções acima e retorne um código de erro do tipo ...

Qual é o tipo de retorno de doExperimenteToDoSomethingWithAllThisMess se uma de suas funções chamadas falhar?

Códigos de retorno não são uma solução universal

Os operadores não podem retornar um código de erro. Os construtores C ++ também não.

Códigos de retorno significam que você não pode encadear expressões

O corolário do ponto acima. E se eu quiser escrever:

CMyType o = add(a, multiply(b, c)) ;

Não posso, porque o valor de retorno já está em uso (e às vezes não pode ser alterado). Assim, o valor de retorno passa a ser o primeiro parâmetro, enviado como referência ... Ou não.

Exceções são digitadas

Você pode enviar classes diferentes para cada tipo de exceção. As exceções de recursos (ou seja, sem memória) devem ser leves, mas qualquer outra coisa pode ser tão pesada quanto necessário (eu gosto da exceção Java que me dá toda a pilha).

Cada captura pode então ser especializada.

Nunca use pegar (...) sem jogar de novo

Normalmente, você não deve ocultar um erro. Se você não relançar, no mínimo, registre o erro em um arquivo, abra uma caixa de mensagem, o que for ...

As exceções são ... NUKE

O problema com a exceção é que o uso excessivo deles irá produzir um código cheio de try / catch. Mas o problema está em outro lugar: quem tenta / captura seu código usando o contêiner STL? Ainda assim, esses contêineres podem enviar uma exceção.

Claro, em C ++, nunca deixe uma exceção sair de um destruidor.

As exceções são ... síncronas

Certifique-se de capturá-los antes que eles coloquem seu tópico de joelhos ou se propaguem dentro do loop de mensagens do Windows.

A solução poderia ser misturá-los?

Então eu acho que a solução é jogar quando algo não deveria acontecer. E quando algo puder acontecer, use um código de retorno ou um parâmetro para permitir que o usuário reaja a isso.

Portanto, a única questão é "o que é algo que não deveria acontecer?"

Depende do contrato de sua função. Se a função aceita um ponteiro, mas especifica que o ponteiro não deve ser NULL, então está ok para lançar uma exceção quando o usuário envia um ponteiro NULL (a questão é, em C ++, quando o autor da função não usou referências em vez disso de ponteiros, mas ...)

Outra solução seria mostrar o erro

Às vezes, seu problema é que você não quer erros. Usar exceções ou códigos de retorno de erro é legal, mas ... Você quer saber sobre isso.

No meu trabalho, usamos uma espécie de "Assert". Vai, dependendo dos valores de um arquivo de configuração, não importa as opções de depuração / liberação de compilação:

  • registre o erro
  • abra uma caixa de mensagem com um "Ei, você tem um problema"
  • abra uma caixa de mensagem com um "Ei, você tem um problema, deseja depurar"

Tanto no desenvolvimento quanto no teste, isso permite que o usuário identifique o problema exatamente quando ele é detectado, e não depois (quando algum código se preocupa com o valor de retorno ou dentro de uma captura).

É fácil adicionar ao código legado. Por exemplo:

void doSomething(CMyObject * p, int iRandomData)
{
   // etc.
}

leva um tipo de código semelhante a:

void doSomething(CMyObject * p, int iRandomData)
{
   if(iRandomData < 32)
   {
      MY_RAISE_ERROR("Hey, iRandomData " << iRandomData << " is lesser than 32. Aborting processing") ;
      return ;
   }

   if(p == NULL)
   {
      MY_RAISE_ERROR("Hey, p is NULL !\niRandomData is equal to " << iRandomData << ". Will throw.") ;
      throw std::some_exception() ;
   }

   if(! p.is Ok())
   {
      MY_RAISE_ERROR("Hey, p is NOT Ok!\np is equal to " << p->toString() << ". Will try to continue anyway") ;
   }

   // etc.
}

(Eu tenho macros semelhantes que estão ativas apenas na depuração).

Note que na produção o arquivo de configuração não existe, então o cliente nunca vê o resultado desta macro ... Mas é fácil ativá-la quando necessário.

Conclusão

Ao codificar usando códigos de retorno, você está se preparando para o fracasso e espera que sua fortaleza de testes seja segura o suficiente.

Ao codificar usando exceção, você sabe que seu código pode falhar e geralmente coloca o contra-fogo na posição estratégica escolhida em seu código. Mas geralmente, seu código é mais sobre "o que deve fazer" do que "o que temo que aconteça".

Mas quando você codifica, você deve usar a melhor ferramenta à sua disposição e, às vezes, é "Nunca esconda um erro e mostre-o o mais rápido possível". A macro que falei acima segue essa filosofia.

paercebal
fonte
3
Sim, MAS em relação ao seu primeiro exemplo, isso poderia ser facilmente escrito como: if( !doSomething() ) { puts( "ERROR - doSomething failed" ) ; return ; // or react to failure of doSomething } if( !doSomethingElse() ) { // react to failure of doSomethingElse() }
bobobobo
Por que não ... Mas então, eu ainda acho doSomething(); doSomethingElse(); ...melhor porque se eu precisar adicionar if / while / etc. para fins de execução normal, não quero que sejam misturadas com if / while / etc declarações adicionadas para propósitos excepcionais ... E como a regra real sobre o uso de exceções é lançar , não pegar , as declarações try / catch geralmente não são invasivas.
paercebal
1
Seu primeiro ponto mostra qual é o problema das exceções. Seu fluxo de controle fica estranho e é separado do problema real. Está substituindo alguns níveis de identificação por uma cascata de capturas. Eu usaria ambos, códigos de retorno (ou objetos de retorno com informações pesadas) para possíveis erros e exceções para coisas que não são esperadas .
Peter
2
@Peter Weber: Não é estranho. Ele é separado do problema real porque não faz parte do fluxo de execução normal . É uma execução excepcional . E então, novamente, o ponto sobre a exceção é, em caso de erro excepcional , lançar com frequência e pegar raramente , ou nunca. Portanto, os blocos catch raramente aparecem no código.
paercebal
Os exemplos neste tipo de debate são muito simplistas. Normalmente, "doSomething ()" ou "doSomethingElse ()" realmente executa algo, como alterar o estado de algum objeto. O código de exceção não garante o retorno do objeto ao estado anterior e menos ainda quando o catch está muito longe do lançamento ... Por exemplo, imagine doSomething ser chamado duas vezes e incrementar um contador antes de lançar. Como você sabe, ao capturar a exceção, que deve diminuir uma ou duas vezes? Em geral, escrever código de exceção segura para qualquer coisa que não seja um exemplo de brinquedo é muito difícil (impossível?).
xryl669
36

Eu uso os dois na verdade.

Eu uso códigos de retorno se for um erro conhecido e possível. Se for um cenário que eu sei que pode e vai acontecer, então há um código que é enviado de volta.

As exceções são usadas apenas para coisas que NÃO estou esperando.

Stephen Wrighton
fonte
1 para uma maneira muito simples de ir. Pelo menos é curto o suficiente para que eu possa lê-lo muito rapidamente. :-)
Ashish Gupta
1
As exceções são usadas apenas para coisas que NÃO estou esperando. ” Se você não está esperando, por que fazer ou como pode usá-las?
anar khalilov
5
Só porque não estou esperando que isso aconteça, não significa que não posso ver como isso poderia acontecer. Espero que meu SQL Server esteja ligado e respondendo. Mas eu ainda codifico minhas expectativas para que possa falhar normalmente se ocorrer um tempo de inatividade inesperado.
Stephen Wrighton
Um SQL Server que não responde não seria facilmente classificado como "erro conhecido e possível"?
tkburbidge
21

De acordo com o Capítulo 7 intitulado "Exceções" em Diretrizes de Design de Estrutura: Convenções, Expressões Idiomáticas e Padrões para Bibliotecas .NET Reutilizáveis , várias justificativas são fornecidas para explicar por que usar exceções em vez de valores de retorno é necessário para estruturas OO como C #.

Talvez este seja o motivo mais convincente (página 179):

"As exceções se integram bem com as linguagens orientadas a objetos. As linguagens orientadas a objetos tendem a impor restrições às assinaturas de membros que não são impostas por funções em linguagens não OO. Por exemplo, no caso de construtores, sobrecargas de operadores e propriedades, o desenvolvedor não tem escolha no valor de retorno. Por esse motivo, não é possível padronizar o relatório de erro baseado em valor de retorno para estruturas orientadas a objetos. Um método de relatório de erro, como exceções, que está fora da faixa da assinatura do método é a única opção. "

hurst
fonte
10

Minha preferência (em C ++ e Python) é usar exceções. Os recursos fornecidos pela linguagem tornam um processo bem definido para levantar, capturar e (se necessário) relançar exceções, tornando o modelo fácil de ver e usar. Conceitualmente, é mais limpo do que os códigos de retorno, pois exceções específicas podem ser definidas por seus nomes e têm informações adicionais que os acompanham. Com um código de retorno, você está limitado apenas ao valor do erro (a menos que queira definir um objeto ReturnStatus ou algo assim).

A menos que o código que você está escrevendo seja crítico em termos de tempo, a sobrecarga associada ao desenrolar da pilha não é significativa o suficiente para se preocupar.

Jason Etheridge
fonte
2
Lembre-se de que usar exceções torna a análise do programa mais difícil.
Paweł Hajdan
7

As exceções devem ser retornadas apenas quando acontece algo que você não esperava.

O outro ponto de exceções, historicamente, é que os códigos de retorno são inerentemente proprietários, às vezes um 0 pode ser retornado de uma função C para indicar sucesso, às vezes -1, ou qualquer um deles para uma falha com 1 para um sucesso. Mesmo quando são enumeradas, as enumerações podem ser ambíguas.

As exceções também podem fornecer muito mais informações e, especificamente, definir bem 'Algo deu errado, aqui está, um rastreamento de pilha e algumas informações de suporte para o contexto'

Dito isto, um código de retorno bem enumerado pode ser útil para um conjunto conhecido de resultados, um simples 'aqui os resultados da função, e ele apenas funcionou dessa maneira'

johnc
fonte
6

Em Java, eu uso (na seguinte ordem):

  1. Projeto por contrato (garantindo que as pré-condições sejam atendidas antes de tentar qualquer coisa que possa falhar). Isso captura a maioria das coisas e eu retorno um código de erro para isso.

  2. Retornar códigos de erro durante o processamento do trabalho (e executar reversão, se necessário).

  3. Exceções, mas são usadas apenas para coisas inesperadas.

paxdiablo
fonte
1
Não seria um pouco mais correto usar asserções para contratos? Se o contrato for quebrado, não há nada para salvá-lo.
Paweł Hajdan
@ PawełHajdan, as asserções estão desabilitadas por padrão, eu acredito. Isso tem o mesmo problema que as assertcoisas de C, pois não detectará problemas no código de produção a menos que você execute asserções o tempo todo. Eu tendo a ver as asserções como uma forma de detectar problemas durante o desenvolvimento, mas apenas para coisas que serão ou não declaradas de forma consistente (como coisas com constantes, não coisas com variáveis ​​ou qualquer outra coisa que pode mudar em tempo de execução).
paxdiablo
E doze anos para responder à sua pergunta. Eu deveria executar um help desk :-)
paxdiablo
6

Os códigos de retorno falham no teste " Poço do Sucesso " para mim quase todas as vezes.

  • É muito fácil esquecer de verificar um código de retorno e, posteriormente, ter um erro de advertência.
  • Os códigos de retorno não possuem nenhuma das grandes informações de depuração, como pilha de chamadas, exceções internas.
  • Os códigos de retorno não se propagam, o que, junto com o ponto acima, tende a gerar um registro de diagnóstico excessivo e entrelaçado em vez de registrar em um local centralizado (manipuladores de exceção de nível de aplicativo e thread).
  • Códigos de retorno tendem a gerar códigos confusos na forma de blocos "se" aninhados
  • O tempo do desenvolvedor gasto na depuração de um problema desconhecido que de outra forma seria uma exceção óbvia (poço de sucesso) É caro.
  • Se a equipe por trás do C # não pretendesse que as exceções governassem o fluxo de controle, as execuções não seriam digitadas, não haveria filtro "quando" nas instruções catch e não haveria necessidade da instrução 'throw' sem parâmetro .

Com relação ao desempenho:

  • As exceções podem ser computacionalmente caras RELATIVAS a não serem lançadas, mas são chamadas de EXCEÇÕES por um motivo. As comparações de velocidade sempre conseguem assumir uma taxa de exceção de 100%, o que nunca deveria ser o caso. Mesmo se uma exceção for 100x mais lenta, quanto isso realmente importa se acontecer apenas 1% do tempo?
  • A menos que estejamos falando de aritmética de ponto flutuante para aplicativos gráficos ou algo semelhante, os ciclos da CPU são baratos comparados ao tempo do desenvolvedor.
  • Custo de uma perspectiva de tempo carrega o mesmo argumento. Em relação a consultas de banco de dados ou chamadas de serviço da web ou carregamentos de arquivo, o tempo normal do aplicativo diminuirá o tempo de exceção. As exceções foram quase abaixo do MICROsegundo em 2006
    • Ouso qualquer um que trabalhe em .net, definir seu depurador para quebrar em todas as exceções e desabilitar apenas o meu código e ver quantas exceções já estão acontecendo que você nem conhece.
b_levitt
fonte
4

Um ótimo conselho que recebi do The Pragmatic Programmer foi algo como "seu programa deve ser capaz de executar todas as suas funções principais sem usar exceções".

Smashery
fonte
4
Você está interpretando mal. O que eles queriam dizer era "se o seu programa lançar exceções em seus fluxos normais, está errado". Em outras palavras, "use exceções apenas para coisas excepcionais".
Paweł Hajdan
4

Eu escrevi uma postagem no blog sobre isso há um tempo.

A sobrecarga de desempenho de lançar uma exceção não deve desempenhar nenhum papel em sua decisão. Afinal, se você estiver fazendo certo, uma exceção é excepcional .

Thomas
fonte
O post do blog vinculado é mais sobre olhar antes de pular (verificações) do que mais fácil pedir perdão do que permissão (exceções ou códigos de retorno). Eu respondi lá com minhas idéias sobre esse assunto (dica: TOCTTOU). Mas esta questão é sobre uma questão diferente, ou seja, sob quais condições usar o mecanismo de exceção de uma linguagem em vez de retornar um valor com propriedades especiais.
Damian Yerrick
Eu concordo completamente. Parece que aprendi uma ou duas coisas nos últimos nove anos;)
Thomas
4

Não gosto de códigos de retorno porque eles fazem com que o seguinte padrão se espalhe em todo o seu código

Logo, uma chamada de método que consiste em 4 chamadas de função incha com 12 linhas de tratamento de erros. Algumas das quais nunca acontecerão. Casos de if e switch abundam.

As exceções são mais claras se você as usar bem ... para sinalizar eventos excepcionais .. após os quais o caminho de execução não pode continuar. Freqüentemente, são mais descritivos e informativos do que os códigos de erro.

Se você tiver vários estados após uma chamada de método que deve ser tratada de forma diferente (e não são casos excepcionais), use códigos de erro ou parâmetros de saída. Embora pessoalmente eu ache isso raro ..

Eu pesquisei um pouco sobre o contra-argumento da 'penalidade de desempenho' .. mais no mundo C ++ / COM, mas nas linguagens mais novas, acho que a diferença não é muito. Em qualquer caso, quando algo explode, as preocupações com o desempenho são relegadas para segundo plano :)

Gishu
fonte
3

Com qualquer compilador decente ou exceções de ambiente de tempo de execução não incorrem em uma penalidade significativa. É mais ou menos como uma instrução GOTO que salta para o manipulador de exceções. Além disso, ter exceções capturadas por um ambiente de tempo de execução (como a JVM) ajuda a isolar e corrigir um bug com muito mais facilidade. Vou usar um NullPointerException em Java em vez de um segfault em C a qualquer dia.

Kyle Cronin
fonte
2
As exceções são extremamente caras. Eles precisam percorrer a pilha para localizar possíveis manipuladores de exceção. Essa caminhada pela pilha não é barata. Se um rastreamento de pilha for criado, é ainda mais caro, porque toda a pilha deve ser analisada.
Derek Park
Estou surpreso que os compiladores não podem determinar onde a exceção será detectada pelo menos algumas vezes. Além disso, o fato de que uma exceção altera o fluxo do código torna mais fácil identificar exatamente onde ocorre um erro, na minha opinião, compensa uma penalidade de desempenho.
Kyle Cronin
As pilhas de chamadas podem se tornar extremamente complexas em tempo de execução e os compiladores geralmente não fazem esse tipo de análise. Mesmo se o fizessem, você ainda teria que percorrer a pilha para obter um traço. Você também teria que desenrolar a pilha para lidar com finallyblocos e destruidores para objetos alocados na pilha.
Derek Park
Eu concordo que os benefícios de depuração das exceções geralmente compensam os custos de desempenho.
Derek Park
1
Derak Park, exceção são caros quando acontecem. Esta é a razão pela qual não devem ser usados ​​em demasia. Mas quando não acontecem, não custam virtualmente nada.
paercebal
3

Tenho um conjunto simples de regras:

1) Use códigos de retorno para coisas às quais você espera que seu chamador imediato reaja.

2) Use exceções para erros que são mais amplos em escopo e podem ser razoavelmente esperados para serem tratados por algo muitos níveis acima do chamador, de modo que a percepção do erro não precise percorrer muitas camadas, tornando o código mais complexo.

Em Java, eu só usei exceções não verificadas, exceções verificadas acabam sendo apenas outra forma de código de retorno e, em minha experiência, a dualidade do que pode ser "retornado" por uma chamada de método geralmente é mais um obstáculo do que uma ajuda.

Kendall Helmstetter Gelner
fonte
3

Eu uso exceções em python em circunstâncias excepcionais e não excepcionais.

Muitas vezes é bom poder usar uma Exceção para indicar que a "solicitação não pôde ser realizada", em vez de retornar um valor de Erro. Significa que você / sempre / sabe que o valor de retorno é do tipo correto, ao invés de arbitrariamente None ou NotFoundSingleton ou algo assim. Aqui está um bom exemplo de onde eu prefiro usar um manipulador de exceção em vez de um condicional no valor de retorno.

try:
    dataobj = datastore.fetch(obj_id)
except LookupError:
    # could not find object, create it.
    dataobj = datastore.create(....)

O efeito colateral é que quando um datastore.fetch (obj_id) é executado, você nunca precisa verificar se seu valor de retorno é None, você obtém esse erro imediatamente de graça. Isso vai contra o argumento, "seu programa deve ser capaz de executar todas as suas funções principais sem usar exceções".

Aqui está outro exemplo de onde as exceções são 'excepcionalmente' úteis, a fim de escrever código para lidar com o sistema de arquivos que não está sujeito a condições de corrida.

# wrong way:
if os.path.exists(directory_to_remove):
    # race condition is here.
    os.path.rmdir(directory_to_remove)

# right way:
try: 
    os.path.rmdir(directory_to_remove)
except OSError:
    # directory didn't exist, good.
    pass

Uma chamada de sistema em vez de duas, sem condição de corrida. Este é um exemplo pobre porque obviamente isso irá falhar com um OSError em mais circunstâncias do que o diretório não existe, mas é uma solução 'boa o suficiente' para muitas situações rigidamente controladas.

Jerub
fonte
O segundo exemplo é enganoso. A maneira supostamente errada é errada porque o código os.path.rmdir foi projetado para lançar exceções. A implementação correta no fluxo do código de retorno seria 'if rmdir (...) == FAILED: pass'
MaR
3

Eu acredito que os códigos de retorno aumentam o ruído do código. Por exemplo, sempre odiei a aparência do código COM / ATL devido aos códigos de retorno. Deve haver uma verificação HRESULT para cada linha de código. Considero que o código de retorno de erro é uma das más decisões tomadas pelos arquitetos do COM. Isso torna difícil fazer o agrupamento lógico do código, portanto, a revisão do código se torna difícil.

Não tenho certeza sobre a comparação de desempenho quando há uma verificação explícita do código de retorno a cada linha.

rpattabi
fonte
1
COM wad projetado para ser usado por idiomas que não oferecem suporte a exceções.
Kevin
Essa é uma boa opinião. Faz sentido lidar com códigos de erro para linguagens de script. Pelo menos o VB6 oculta bem os detalhes do código de erro, encapsulando-os no objeto Err, o que de certa forma ajuda no código mais limpo.
rpattabi de
Discordo: VB6 só registra o último erro. Combinado com o infame "em caso de erro, retomar o próximo", você perderá completamente a origem do seu problema, no momento em que o vir. Observe que esta é a base do tratamento de erros no Win32 APII (consulte a função GetLastError)
paercebal
2

Prefiro usar exceções para tratamento de erros e valores de retorno (ou parâmetros) como o resultado normal de uma função. Isso fornece um esquema de tratamento de erros fácil e consistente e, se feito corretamente, torna o código com uma aparência muito mais limpa.

Trent
fonte
2

Uma das grandes diferenças é que as exceções forçam você a lidar com um erro, enquanto os códigos de retorno de erro podem ficar desmarcados.

Códigos de retorno de erro, se usados ​​intensamente, também podem causar códigos muito feios com muitos testes if semelhantes a este formulário:

if(function(call) != ERROR_CODE) {
    do_right_thing();
}
else {
    handle_error();
}

Pessoalmente, prefiro usar exceções para erros que DEVEM ou DEVEM ser corrigidos pelo código de chamada, e só uso códigos de erro para "falhas esperadas" onde retornar algo é realmente válido e possível.

Daniel Bruce
fonte
Pelo menos em C / C ++ e gcc você pode dar a uma função um atributo que irá gerar um aviso quando seu valor de retorno for ignorado.
Paweł Hajdan
phjr: Embora eu discorde do padrão de "código de erro de retorno", seu comentário talvez deva se tornar uma resposta completa. Eu acho isso interessante o suficiente. No mínimo, me deu uma informação útil.
paercebal de
2

Há muitos motivos para preferir exceções ao código de retorno:

  • Normalmente, para facilitar a leitura, as pessoas tentam minimizar o número de declarações de retorno em um método. Fazendo isso, as exceções impedem a realização de algum trabalho extra enquanto em um estado incorreto e, portanto, evitam potencialmente danificar mais dados.
  • As exceções são geralmente mais detalhadas e mais facilmente extensíveis do que o valor de retorno. Suponha que um método retorne um número natural e que você use números negativos como código de retorno quando ocorrer um erro, se o escopo de seu método mudar e agora retornar inteiros, você terá que modificar todas as chamadas de método em vez de apenas ajustar um pouco a exceção.
  • As exceções permitem mais facilmente separar o tratamento de erros do comportamento normal. Eles permitem garantir que algumas operações funcionem de alguma forma como uma operação atômica.
gizmo
fonte
2

As exceções não são para tratamento de erros, IMO. As exceções são apenas isso; eventos excepcionais que você não esperava. Use com cautela, eu digo.

Códigos de erro podem estar OK, mas retornar 404 ou 200 de um método é ruim, IMO. Use enums (.Net) em vez disso, o que torna o código mais legível e fácil de usar para outros desenvolvedores. Além disso, você não precisa manter uma tabela de números e descrições.

Além disso; o padrão try-catch-finally é um antipadrão em meu livro. Try-finally pode ser bom, try-catch também pode ser bom, mas try-catch-finally nunca é bom. try-finally muitas vezes pode ser substituído por uma instrução "usando" (padrão IDispose), que é melhor IMO. E Try-catch onde você realmente pega uma exceção com a qual é capaz de lidar é bom, ou se você fizer isso:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Então, desde que você deixe a exceção continuar a borbulhar, está tudo bem. Outro exemplo é este:

try{
    dbHasBeenUpdated = db.UpdateAll(somevalue); // true/false
}
catch (ConnectionException ex) {
    logger.Exception(ex, "Connection failed");
    dbHasBeenUpdated = false;
}

Aqui, eu realmente lido com a exceção; o que eu faço fora do try-catch quando o método de atualização falha é outra história, mas acho que meu ponto foi feito. :)

Por que então tentar pegar finalmente é um anti-padrão? Aqui está o porquê:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}
finally {
    db.Close();
}

O que acontece se o objeto db já foi fechado? Uma nova exceção é lançada e deve ser tratada! Isto é melhor:

try{
    using(IDatabase db = DatabaseFactory.CreateDatabase()) {
        db.UpdateAll(somevalue);
    }
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Ou, se o objeto db não implementar IDisposable, faça o seguinte:

try{
    try {
        IDatabase db = DatabaseFactory.CreateDatabase();
        db.UpdateAll(somevalue);
    }
    finally{
        db.Close();
    }
}
catch (DatabaseAlreadyClosedException dbClosedEx) {
    logger.Exception(dbClosedEx, "Database connection was closed already.");
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

De qualquer maneira, são meus 2 centavos! :)

noócito
fonte
Será estranho se um objeto tiver .Close (), mas não tiver .Dispose ()
abatishchev
Só estou usando Close () como exemplo. Sinta-se à vontade para pensar nisso como outra coisa. Como eu afirmo; o padrão de uso deve ser usado (doh!) se disponível. É claro que isso implica que a classe implementa IDisposable e, como tal, você pode chamar Dispose.
noocyte de
1

Eu só uso exceções, sem códigos de retorno. Estou falando sobre Java aqui.

A regra geral que sigo é se eu tenho um método chamado, doFoo()então segue que se ele não "faz foo", por assim dizer, então algo excepcional aconteceu e uma exceção deve ser lançada.

SCdF
fonte
1

Uma coisa que temo sobre as exceções é que lançar uma exceção bagunce o fluxo do código. Por exemplo, se você fizer

void foo()
{
  MyPointer* p = NULL;
  try{
    p = new PointedStuff();
    //I'm a module user and  I'm doing stuff that might throw or not

  }
  catch(...)
  {
    //should I delete the pointer?
  }
}

Ou pior ainda, se eu deletasse algo que não deveria, mas fosse jogado para pegar antes de fazer o resto da limpeza. Jogando colocar muito peso sobre o pobre usuário IMHO.

Robert Gould
fonte
É para isso que serve a instrução <code> finally </code>. Mas, infelizmente, não está no padrão C ++ ...
Thomas,
Em C ++, você deve seguir a regra prática "Adquira recursos no construtor e libere-os no destruidor. Para este caso em particular, auto_ptr servirá perfeitamente.
Serge,
Thomas, você está errado. C ++ finalmente não, porque não precisa disso. Em vez disso, tem RAII. A solução de Serge é uma solução usando RAII.
paercebal de
Robert, use a solução de Serge e você verá que seu problema vai embora. Agora, se você escrever mais try / catch do que throws, então (a julgar seu comentário) talvez você tenha um problema em seu código. Claro, usar catch (...) sem um relance geralmente é ruim, pois esconde o erro para melhor ignorá-lo.
paercebal
1

Minha regra geral no argumento de exceção vs. código de retorno:

  • Use códigos de erro quando precisar de localização / internacionalização - no .NET, você pode usar esses códigos de erro para fazer referência a um arquivo de recurso que exibirá o erro no idioma apropriado. Caso contrário, use exceções
  • Use exceções apenas para erros realmente excepcionais. Se for algo que acontece com bastante frequência, use um código de erro booleano ou enum.
Jon Limjap
fonte
Não há razão para que você não possa usar uma exceção ao fazer l10n / i18n. As exceções também podem conter informações localizadas.
gizmo
1

Não acho os códigos de retorno menos feios do que as exceções. Com exceção, você tem o try{} catch() {} finally {}where, assim como os códigos de retorno que você temif(){} . Eu costumava temer exceções pelas razões apresentadas no post; você não sabe se o ponteiro precisa ser apagado, o que você quer. Mas acho que você tem os mesmos problemas quando se trata de códigos de retorno. Você não sabe o estado dos parâmetros, a menos que conheça alguns detalhes sobre a função / método em questão.

Independentemente disso, você deve lidar com o erro, se possível. Você pode tão facilmente permitir que uma exceção se propague para o nível superior quanto ignorar um código de retorno e deixar o programa falhar em seg.

Eu gosto da ideia de retornar um valor (enumeração?) Para resultados e uma exceção para um caso excepcional.

Jonathan Adelson
fonte
0

Para uma linguagem como Java, eu escolheria Exception porque o compilador dá erro de tempo de compilação se as exceções não forem tratadas. Isso força a função de chamada a tratar / lançar as exceções.

Para Python, estou mais em conflito. Não há compilador, portanto, é possível que o chamador não trate a exceção lançada pela função que leva às exceções de tempo de execução. Se você usar códigos de retorno, poderá ter um comportamento inesperado se não for tratado adequadamente e, se usar exceções, poderá obter exceções de tempo de execução.

2ank3
fonte
0

Geralmente prefiro códigos de retorno porque permitem que o chamador decida se a falha é excepcional .

Essa abordagem é típica na linguagem Elixir.

# I care whether this succeeds. If it doesn't return :ok, raise an exception.
:ok = File.write(path, content)

# I don't care whether this succeeds. Don't check the return value.
File.write(path, content)

# This had better not succeed - the path should be read-only to me.
# If I get anything other than this error, raise an exception.
{:error, :erofs} = File.write(path, content)

# I want this to succeed but I can handle its failure
case File.write(path, content) do
  :ok => handle_success()
  error => handle_error(error)
end

As pessoas mencionaram que os códigos de retorno podem fazer com que você tenha muitas ifinstruções aninhadas , mas isso pode ser tratado com uma sintaxe melhor. No Elixir, a withdeclaração nos permite separar facilmente uma série de valores de retorno do caminho feliz de quaisquer falhas.

with {:ok, content} <- get_content(),
  :ok <- File.write(path, content) do
    IO.puts "everything worked, happy path code goes here"
else
  # Here we can use a single catch-all failure clause
  # or match every kind of failure individually
  # or match subsets of them however we like
  _some_error => IO.puts "one of those steps failed"
  _other_error => IO.puts "one of those steps failed"
end

Elixir ainda tem funções que geram exceções. Voltando ao meu primeiro exemplo, eu poderia fazer qualquer um desses para gerar uma exceção se o arquivo não puder ser gravado.

# Raises a generic MatchError because the return value isn't :ok
:ok = File.write(path, content)

# Raises a File.Error with a descriptive error message - eg, saying
# that the file is read-only
File.write!(path, content)

Se eu, como chamador, souber que desejo gerar um erro se a gravação falhar, posso escolher chamar em File.write!vez deFile.write . Ou posso escolher ligar File.writee lidar com cada um dos motivos possíveis para o fracasso de forma diferente.

Claro que sempre é possível fazer rescueuma exceção, se quisermos. Mas, comparado a lidar com um valor de retorno informativo, parece estranho para mim. Se eu sei que uma chamada de função pode falhar ou mesmo deveria falhar, sua falha não é um caso excepcional.

Nathan Long
fonte