Considerações sobre manipulação de erros

31

O problema:

Desde muito tempo, estou preocupado com o exceptionsmecanismo, porque sinto que ele realmente não resolve o que deveria.

REIVINDICAÇÃO: Há longos debates externos sobre esse tópico, e a maioria deles luta para comparar ou exceptionsretornar um código de erro. Definitivamente, este não é o tópico aqui.

Tentando definir um erro, eu concordo com o CppCoreGuidelines, da Bjarne Stroustrup & Herb Sutter

Um erro significa que a função não pode atingir seu objetivo anunciado

REIVINDICAÇÃO: O exceptionmecanismo é uma linguagem semântica para o tratamento de erros.

REIVINDICAÇÃO: Para mim, "não há desculpa" para uma função por não realizar uma tarefa: definimos condições pré / pós incorretamente para que a função não possa garantir resultados ou algum caso excepcional específico não é considerado importante o suficiente para gastar tempo no desenvolvimento uma solução. Considerando que, na IMO, a diferença entre o código normal e o tratamento do código de erro é (antes da implementação) uma linha muito subjetiva.

REIVINDICAÇÃO: Usar exceções para indicar quando uma condição anterior ou posterior não é mantida é outro objetivo do exceptionmecanismo, principalmente para fins de depuração. Não viso esse uso exceptionsaqui.

Em muitos livros, tutoriais e outras fontes, eles tendem a mostrar o tratamento de erros como uma ciência bastante objetiva, resolvida exceptionse você só precisa catchdeles para ter um software robusto, capaz de se recuperar de qualquer situação. Mas meus vários anos como desenvolvedor me fizeram ver o problema de uma abordagem diferente:

  • Os programadores tendem a simplificar sua tarefa lançando exceções quando o caso específico parece muito raro para ser implementado com cuidado. Os casos típicos são: problemas de falta de memória, problemas de disco cheio, problemas de arquivos corrompidos etc. Isso pode ser suficiente, mas nem sempre é decidido do nível da arquitetura.
  • Os programadores tendem a não ler atentamente a documentação sobre exceções nas bibliotecas e geralmente não estão cientes de quais e quando uma função é executada. Além disso, mesmo quando eles sabem, eles realmente não os gerenciam.
  • Os programadores tendem a não capturar exceções com antecedência suficiente e, quando o fazem, é principalmente para registrar e lançar ainda mais. (consulte o primeiro ponto).

Isso tem duas consequências:

  1. Erros que ocorrem com frequência são detectados no início do desenvolvimento e depurados (o que é bom).
  2. Exceções raras não são gerenciadas e fazem o sistema travar (com uma boa mensagem de log) na casa do usuário. Algumas vezes o erro é relatado, ou nem mesmo.

Considerando que, na IMO, o principal objetivo de um mecanismo de erro deve ser:

  1. Tornar visível no código onde algum caso específico não é gerenciado.
  2. Comunique o tempo de execução do problema ao código relacionado (pelo menos ao chamador) quando essa situação ocorrer.
  3. Fornece mecanismos de recuperação

A principal falha da exceptionsemântica como mecanismo de tratamento de erros é a IMO: é fácil ver onde a throwestá no código-fonte, mas não é absolutamente evidente se uma função específica pode ser lançada observando a declaração. Isso traz todo o problema que apresentei acima.

O idioma não aplica e verifica o código de erro tão estritamente quanto em outros aspectos do idioma (por exemplo, tipos fortes de variáveis)

Uma tentativa de solução

Com a intenção de melhorar isso, desenvolvi um sistema muito simples de tratamento de erros, que tenta colocar o tratamento no mesmo nível de importância que o código normal.

A ideia é:

  • Cada função (relevante) recebe uma referência a um successobjeto muito leve e pode configurá-lo para um status de erro no caso. O objeto fica muito claro até que um erro no texto seja salvo.
  • Uma função é incentivada a ignorar sua tarefa se o objeto fornecido já contiver um erro.
  • Um erro nunca deve ser substituído.

O design completo obviamente leva em consideração cada aspecto (cerca de 10 páginas) e também como aplicá-lo ao OOP.

Exemplo da Successclasse:

class Success
{
public:
    enum SuccessStatus
    {
        ok = 0,             // All is fine
        error = 1,          // Any error has been reached
        uninitialized = 2,  // Initialization is required
        finished = 3,       // This object already performed its task and is not useful anymore
        unimplemented = 4,  // This feature is not implemented already
    };

    Success(){}
    Success( const Success& v);
    virtual ~Success() = default;
    virtual Success& operator= (const Success& v);

    // Comparators
    virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
    virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}

    // Retrieve if the status is not "ok"
    virtual bool operator!() const { return status!=ok;}

    // Retrieve if the status is "ok"
    operator bool() const { return status==ok;}

    // Set a new status
    virtual Success& set( SuccessStatus status, std::string msg="");
    virtual void reset();

    virtual std::string toString() const{ return stateStr;}
    virtual SuccessStatus getStatus() const { return status; }
    virtual operator SuccessStatus() const { return status; }

private:
    std::string stateStr;
    SuccessStatus status = Success::ok;
};

Uso:

double mySqrt( Success& s, double v)
{
    double result = 0.0;
    if (!s) ; // do nothing
    else if (v<0.0) s.set(Error, "Square root require non-negative input.");
    else result = std::sqrt(v);
    return result;
}

Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;

Eu usei isso em muitos códigos (próprios) e forçou o programador (eu) a pensar mais sobre possíveis casos excepcionais e como resolvê-los (bom). No entanto, ele possui uma curva de aprendizado e não se integra bem ao código que agora o utiliza.

A questão

Eu gostaria de entender melhor as implicações do uso desse paradigma em um projeto:

  • A premissa do problema está correta? ou Perdi algo relevante?
  • A solução é uma boa idéia arquitetônica? ou o preço é muito alto?

EDITAR:

Comparação entre métodos:

//Exceptions:

    // Incorrect
    File f = open("text.txt"); // Could throw but nothing tell it! Will crash
    save(f);

    // Correct
    File f;
    try
    {
        f = open("text.txt");
        save(f);
    }
    catch( ... )
    {
        // do something 
    }

//Error code (mixed):

    // Incorrect
    File f = open("text.txt"); //Nothing tell you it may fail! Will crash
    save(f);

    // Correct
    File f = open("text.txt");
    if (f) save(f);

//Error code (pure);

    // Incorrect
    File f;
    open(f, "text.txt"); //Easy to forget the return value! will crash
    save(f);

    //Correct
    File f;
    Error er = open(f, "text.txt");
    if (!er) save(f);

//Success mechanism:

    Success s;
    File f;
    open(s, "text.txt");
    save(s, f); //s cannot be avoided, will never crash.
    if (s) ... //optional. If you created s, you probably don't forget it.
Adrian Maire
fonte
25
Promovido para "Esta pergunta mostra o esforço de pesquisa; é útil e clara", não porque eu concorde: acho que alguns dos pensamentos estão equivocados. (Detalhes podem seguir em uma resposta.)
Martin Ba
2
Absolutamente, eu entendo e concordo com isso! O objetivo desta pergunta é ser criticado. E a pontuação da pergunta para indicar perguntas boas / ruins, não que o OP esteja certo.
Adrian Maire
2
Se bem entendi, sua principal reclamação sobre exceções é que as pessoas podem ignorá-la (em c ++) em vez de lidar com elas. No entanto, sua construção de Sucesso tem a mesma falha de design. Como exceções, eles simplesmente ignoram. Pior ainda: é mais detalhado, leva a retornos em cascata e você não pode "pegá-lo" rio acima.
dagnelies
3
Por que não usar algo como mônadas? Eles tornam seus erros implícitos, mas não ficam em silêncio durante a execução. Na verdade, a primeira coisa que pensei ao olhar para o seu código foi "mônadas, legal". Dê uma olhada neles.
bash0r
2
O principal motivo pelo qual gosto de exceções é que elas permitem que você capture todos os erros inesperados de um determinado bloco de código e os lide com consistência. Sim, não há uma boa razão para o código não executar sua tarefa - "houve um erro" é uma má razão, mas ainda acontece e, quando isso acontece, você deseja registrar a causa e exibir uma mensagem ou tentar novamente. (Eu tenho algum código que faz uma interação complexa, restartable com um sistema remoto; se o sistema remoto ir para baixo, eu quero registrá-lo e tente novamente desde o início)
user253751

Respostas:

32

O tratamento de erros é talvez a parte mais difícil de um programa.

Em geral, é fácil perceber que há uma condição de erro; no entanto, sinalizá-lo de uma maneira que não pode ser contornada e manipulá-lo adequadamente (consulte os níveis de segurança de exceção da Abrahams ) é realmente difícil.

Em C, os erros de sinalização são feitos por um código de retorno, que é isomórfico para sua solução.

O C ++ introduziu exceções devido à falta de uma abordagem desse tipo; ou seja, só funciona se os chamadores se lembrarem de verificar se ocorreu um erro ou não e se falharem horrivelmente caso contrário. Sempre que você se encontra dizendo "Tudo bem desde que todas as vezes ...", você tem um problema; os humanos não são tão meticulosos, mesmo quando se importam.

O problema, no entanto, é que as exceções têm seus próprios problemas. Ou seja, fluxo de controle invisível / oculto. O objetivo era: ocultar o caso de erro para que a lógica do código não seja ofuscada pelo padrão de manipulação de erros. Isso torna o "caminho feliz" muito mais claro (e rápido!), Ao custo de tornar os caminhos de erro quase inescrutáveis.


Acho interessante ver como outros idiomas abordam o problema:

  • Java verificou exceções (e desmarcadas),
  • Go usa códigos de erro / pânico,
  • Rust usa tipos de soma / pânico).
  • Linguagens FP em geral.

O C ++ costumava ter alguma forma de exceções verificadas; talvez você tenha notado que foi preterido e simplificado em relação a um básico noexcept(<bool>): em vez disso, uma função é declarada como possivelmente lançada ou nunca. As exceções verificadas são um pouco problemáticas, pois não têm extensibilidade, o que pode causar mapeamentos / aninhamentos difíceis. E hierarquias de exceção complicadas (um dos principais casos de uso da herança virtual são as exceções ...).

Por outro lado, Go e Rust adotam a abordagem que:

  • erros devem ser sinalizados em banda,
  • A exceção deve ser usada para situações realmente excepcionais.

O último é bastante evidente no fato de que (1) eles denominam suas exceções de pânico e (2) não há hierarquia de tipos / cláusula complicada aqui. O idioma não oferece facilidades para inspecionar o conteúdo de um "pânico": nenhuma hierarquia de tipos, nenhum conteúdo definido pelo usuário, apenas um "opa, as coisas deram tão errado que não há recuperação possível".

Isso efetivamente incentiva os usuários a usar o tratamento adequado de erros, enquanto ainda deixa uma maneira fácil de sair em situações excepcionais (como: "espere, eu ainda não implementei isso!").

Obviamente, a abordagem Go infelizmente é muito parecida com a sua, pois você pode facilmente esquecer de verificar o erro ...

... a abordagem Rust, no entanto, é principalmente centrada em dois tipos:

  • Option, que é semelhante a std::optional,
  • Result, que é uma variante de duas possibilidades: Ok e Err.

isso é muito mais claro porque não há oportunidade de usar um resultado acidentalmente sem ter verificado o sucesso: se você o fizer, o programa entrará em pânico.


As linguagens FP formam seu tratamento de erros em construções que podem ser divididas em três camadas: - Functor - Aplicável / Alternativa - Mônadas / Alternativa

Vamos dar uma olhada na Functortipeclass de Haskell :

class Functor m where
  fmap :: (a -> b) -> m a -> m b

Primeiro de tudo, as classes de tipo são um pouco semelhantes, mas não iguais às interfaces. As assinaturas de funções de Haskell parecem um pouco assustadoras à primeira vista. Mas vamos decifrá-los. A função fmapassume uma função como primeiro parâmetro, que é um pouco semelhante a std::function<a,b>. A próxima coisa é uma m a. Você pode imaginar mcomo algo parecido std::vectore m aalgo parecido std::vector<a>. Mas a diferença é que m aisso não significa que tenha que ser explicitamente std:vector. Então poderia ser std::optiontambém. Dizendo ao idioma que temos uma instância para a classe Functorde tipo de um tipo específico como std::vectorou std::option, podemos usar a função fmappara esse tipo. O mesmo deve ser feito para os typeclasses Applicative, AlternativeeMonadque permite que você faça cálculos com estado e possíveis falhas. A Alternativeclasse tipográfica implementa abstrações de recuperação de erros. Com isso, você pode dizer algo como a <|> bsignificando que é termo aou termo b. Se nenhum dos dois cálculos tiver êxito, ainda será um erro.

Vamos dar uma olhada no Maybetipo de Haskell .

data Maybe a
  = Nothing
  | Just a

Isso significa que, onde você espera um Maybe a, recebe um Nothingou outro Just a. Ao olhar fmapde cima, uma implementação pode parecer

fmap f m = case m of
  Nothing -> Nothing
  Just a -> Just (f a)

A case ... ofexpressão é chamada de correspondência de padrões e se assemelha ao que é conhecido no mundo OOP como visitor pattern. Imagine a linha case m ofcomo m.apply(...)e os pontos é a instanciação de uma classe implementando as funções de despacho. As linhas abaixo da case ... ofexpressão são as respectivas funções de despacho, trazendo os campos da classe diretamente no escopo por nome. No Nothingramo que criamos Nothinge no Just aramo, denominamos nosso único valor ae criamos outro Just ...com a função de transformaçãof aplicada a. Leia-o como: new Just(f(a)).

Agora, isso pode lidar com cálculos errôneos enquanto abstrai as verificações de erro reais. Existem implementações para as outras interfaces, o que torna esse tipo de computação muito poderoso. Na verdade, Maybeé a inspiração para o Rust'sOption tipo- .


Gostaria de encorajá-lo a refazer sua Successclasse em direção a um Resultlugar. Alexandrescu propôs algo realmente próximo, chamado expected<T>, para o qual propostas padrão foram feitas .

Vou me ater ao nome e à API do Rust simplesmente porque ... está documentado e funciona. Obviamente, Rust tem um ?operador de sufixo bacana que tornaria o código muito mais doce; em C ++, usaremos a expressão deTRY macro e instruções do GCC para emulá-la.

template <typename E>
struct Error {
    Error(E e): error(std::move(e)) {}

    E error;
};

template <typename E>
Error<E> error(E e) { return Error<E>(std::move(e)); }

template <typename T, typename E>
struct [[nodiscard]] Result {
    template <typename U>
    Result(U u): ok(true), data(std::move(u)), error() {}

    template <typename F>
    Result(Error<F> f): ok(false), data(), error(std::move(f.error)) {}

    template <typename U, typename F>
    Result(Result<U, F> other):
        ok(other.ok), data(std::move(other.data)),  error(std::move(other.error)) {}

    bool ok = false;
    T data;
    E error;
};

#define TRY(Expr_) \
    ({ auto result = (Expr_); \
       if (!result.ok) { return result; } \
       std::move(result.data); })

Nota: este Resulté um espaço reservado. Uma implementação adequada usaria encapsulamento e umunion . É o suficiente para esclarecer o ponto.

O que me permite escrever ( veja em ação ):

Result<double, std::string> sqrt(double x) {
    if (x < 0) {
        return error("sqrt does not accept negative numbers");
    }
    return x;
}

Result<double, std::string> double_sqrt(double x) {
    auto y = TRY(sqrt(x));
    return sqrt(y);
}

que eu acho muito legal:

  • diferente do uso de códigos de erro (ou da sua Successclasse), esquecer de verificar erros resultará em um erro de tempo de execução 1, em vez de um comportamento aleatório,
  • diferentemente do uso de exceções, é aparente no site de chamada quais funções podem falhar, portanto não há surpresa.
  • com o padrão C ++ - 2X, podemos entrar conceptsno padrão. Isso tornaria esse tipo de programação muito mais agradável, pois poderíamos deixar a escolha sobre o tipo de erro. Por exemplo, com uma implementação de std::vectorcomo resultado, poderíamos calcular todas as soluções possíveis de uma só vez. Ou podemos optar por melhorar o tratamento de erros, como você propôs.

1 Com uma Resultimplementação devidamente encapsulada ;)


Nota: diferentemente da exceção, esse peso leve Resultnão possui backtraces, o que torna o registro menos eficiente; você pode achar útil registrar pelo menos o número do arquivo / linha no qual a mensagem de erro é gerada e geralmente escrever uma mensagem de erro avançada. Isso pode ser composto capturando o arquivo / linha toda vez que a TRYmacro é usada, criando essencialmente o backtrace manualmente ou usando códigos e bibliotecas específicas da plataforma, como libbacktracepara listar os símbolos na pilha de chamadas.


Há uma grande ressalva: as bibliotecas C ++ existentes e até mesmo stdsão baseadas em exceções. Será uma batalha difícil usar esse estilo, já que a API de qualquer biblioteca de terceiros deve ser envolvida em um adaptador ...

Matthieu M.
fonte
3
Essa macro parece ... muito errada. Vou assumir que ({...})existe alguma extensão do gcc, mas mesmo assim não deveria if (!result.ok) return result;? Sua condição aparece ao contrário e você faz uma cópia desnecessária do erro.
Mooing Duck
@MooingDuck A resposta explica que ({...})é a expressão de declarações do gcc .
Jamesdlin
11
Eu recomendo usar std::variantpara implementar o Resultse você estiver usando C ++ 17. Além disso, para receber um aviso se você ignorar um erro, use[[nodiscard]]
Justin
2
@ Justin: Usar std::variantou não é uma questão de gosto, dadas as vantagens e desvantagens em relação ao tratamento de exceções. [[nodiscard]]é de fato uma vitória pura.
Matthieu M.
46

RECLAMAÇÃO: O mecanismo de exceção é uma linguagem semântica para manipulação de erros

exceções são um mecanismo de controle de fluxo. A motivação para esse mecanismo de controle-fluxo foi separar especificamente o tratamento de erros do código de tratamento sem erros, no caso comum de que o tratamento de erros é muito repetitivo e tem pouca relevância para a parte principal da lógica.

REIVINDICAÇÃO: Para mim, "não há desculpa" para uma função por não realizar uma tarefa: definimos condições pré / pós incorretamente para que a função não possa garantir resultados ou algum caso excepcional específico não é considerado importante o suficiente para gastar tempo no desenvolvimento uma solução

Considere: eu tento criar um arquivo. O dispositivo de armazenamento está cheio.

Agora, isso não é uma falha na definição de minhas pré-condições: você não pode usar "deve haver armazenamento suficiente" como uma pré-condição em geral, porque o armazenamento compartilhado está sujeito a condições de corrida que impossibilitam a satisfação.

Então, meu programa deve, de alguma forma, liberar espaço e prosseguir com êxito, caso contrário, tenho preguiça de "desenvolver uma solução"? Isso parece francamente sem sentido. A "solução" para a gestão de armazenamento compartilhado está fora do âmbito do meu programa , e permitindo que o meu programa para falhar normalmente, e ser re-run uma vez que o utilizador seja lançado algum espaço, ou adicionado um pouco mais de armazenamento, é fino .


O que sua classe de sucesso faz é intercalar o tratamento de erros de maneira muito explícita com a lógica do seu programa. Toda função precisa verificar, antes da execução, se já ocorreu algum erro, o que significa que ela não deve fazer nada. Toda função de biblioteca precisa ser agrupada em outra função, com mais um argumento (e, esperançosamente, um encaminhamento perfeito), que faz exatamente a mesma coisa.

Observe também que sua mySqrtfunção precisa retornar um valor mesmo que tenha falhado (ou uma função anterior tenha falhado). Então, você está retornando um valor mágico (como NaN) ou injetando um valor indeterminado em seu programa e esperando nada o utilize sem verificar o estado de sucesso que você encadeara durante a execução.

Para correção - e desempenho - é muito melhor recuperar o controle do escopo quando você não puder progredir. Exceções e verificação explícita de erros no estilo C com retorno antecipado realizam isso.


Para comparação, um exemplo de sua ideia que realmente funciona é a mônada de erro em Haskell. A vantagem sobre o seu sistema é que você escreve a maior parte da sua lógica normalmente e, em seguida, envolve-a na mônada, que cuida de interromper a avaliação quando uma etapa falha. Dessa forma, o único código que toca diretamente no sistema de tratamento de erros é o código que pode falhar (gerar um erro) e o código que precisa lidar com a falha (capturar uma exceção).

Não tenho certeza se o estilo de mônada e a avaliação lenta traduzem bem para C ++.

Sem utilidade
fonte
11
Graças à sua resposta, ele adiciona luz ao tópico. Acho que o usuário discordaria and allowing my program to fail gracefully, and be re-runquando perdesse o trabalho de 2h:
Adrian Maire
14
Sua solução significa que em todos os lugares em que você pode criar um arquivo, é necessário solicitar ao usuário que corrija a situação e tente novamente. Então, qualquer outra coisa que possa dar errado, você também precisará corrigir de alguma forma localmente. Com exceções, você apenas captura std::exceptionno nível mais alto da operação lógica, diz ao usuário "X falhou por causa de ex.what ()" e oferece a tentativa de tentar novamente toda a operação quando e se estiver pronta.
Inútil
13
@AdrianMaire: A "permissão para falhar normalmente e ser executada novamente" também pode ser implementada como showing the Save dialog again along with an error message and allowing the user to specify an alternative location to try. Esse é um tratamento gracioso de um problema que normalmente não pode ser solucionado a partir do código que detecta que o primeiro local de armazenamento está cheio.
Bart van Ingen Schenau
3
A avaliação preguiçosa do @Useless não tem nada a ver com o uso da mônada Error, como evidenciado por linguagens estritas de avaliação como Rust, OCaml e F #, que fazem uso pesado dela.
8bittree
11
@ IMO inútil para software de qualidade, faz sentido que “em todo lugar em que você possa criar um arquivo, é necessário solicitar ao usuário que corrija a situação e tente novamente”. Os primeiros programadores costumavam fazer esforços notáveis ​​na recuperação de erros, pelo menos o programa TeX de Knuth está cheio deles. E com sua estrutura de "programação alfabetizada", ele encontrou uma maneira de manter o tratamento de erros em outra seção, para que o código permaneça legível e a recuperação de erros seja gravada com mais cuidado (porque quando você está escrevendo a seção de recuperação de erros, esse é o ponto e o programador tende a fazer um trabalho melhor).
ShreevatsaR
15

Eu gostaria de entender melhor as implicações do uso desse paradigma em um projeto:

  • A premissa do problema está correta? ou Perdi algo relevante?
  • A solução é uma boa idéia arquitetônica? ou o preço é muito alto?

Sua abordagem traz alguns grandes problemas ao seu código-fonte:

  • depende do código do cliente sempre lembrando de verificar o valor de s. Isso é comum com os códigos de retorno de uso para a abordagem de tratamento de erros e um dos motivos pelos quais as exceções foram introduzidas no idioma: com exceções, se você falhar, não falhará silenciosamente.

  • quanto mais código você escrever com essa abordagem, mais código de erro você precisará adicionar também para o tratamento de erros (seu código não é mais minimalista) e seu esforço de manutenção aumentará.

Mas meus vários anos como desenvolvedor me fazem ver o problema de uma abordagem diferente:

As soluções para esses problemas devem ser abordadas no nível do líder técnico ou no nível da equipe:

Os programadores tendem a simplificar sua tarefa lançando exceções quando o caso específico parece muito raro para ser implementado com cuidado. Os casos típicos são: problemas de falta de memória, problemas de disco cheio, problemas de arquivos corrompidos etc. Isso pode ser suficiente, mas nem sempre é decidido do nível da arquitetura.

Se você se encontra lidando com todo tipo de exceção que pode ser lançada o tempo todo, o design não é bom; Quais erros são tratados, devem ser decididos de acordo com as especificações do projeto, não de acordo com o que os desenvolvedores desejam implementar.

Aborde a configuração de testes automatizados, separando a especificação dos testes de unidade e a implementação (faça com que duas pessoas diferentes façam isso).

Os programadores tendem a não ler atentamente a documentação [...] Além disso, mesmo quando sabem, eles realmente não os gerenciam.

Você não abordará isso escrevendo mais código. Acho que sua melhor aposta são as análises de código meticulosamente aplicadas.

Os programadores tendem a não capturar exceções com antecedência suficiente e, quando o fazem, é principalmente para registrar e lançar ainda mais. (consulte o primeiro ponto).

O tratamento adequado de erros é difícil, mas menos tedioso com exceções do que com valores de retorno (se eles são realmente retornados ou transmitidos como argumentos de E / S).

A parte mais complicada do tratamento de erros não é como você recebe o erro, mas como garantir que seu aplicativo mantenha um estado consistente na presença de erros.

Para resolver isso, é necessário dar mais atenção à identificação e execução em condições de erro (mais testes, mais testes de unidade / integração, etc.).

utnapistim
fonte
12
Todo o código após um erro é ignorado, se você se lembrar de verificar todas as vezes que receber uma instância como argumento . Isto é o que eu quis dizer com "quanto mais código você escrever com essa abordagem, mais código de erro será necessário adicionar". Você precisará adivinhar seu código com ifs na instância de sucesso, e toda vez que você esquecer, será um bug. O segundo problema causado pelo esquecimento de verificar: o código que é executado até você verificar novamente, não deveria ter sido executado (continuando se você esquecer de verificar, corrompe seus dados).
Utnapishtim
11
Não, lidar com uma exceção (ou retornar um código de erro) não é uma falha - a menos que o erro / exceção seja logicamente fatal ou você opte por não tratá-la. Você ainda tem a oportunidade de lidar com o caso de erro, sem ter que verificar explicitamente a cada passo se ocorreu um erro anteriormente
Useless
11
@AdrianMaire Em quase todos os aplicativos em que trabalho, prefiro muito mais uma falha do que continuar silenciosamente. Trabalho em softwares críticos para os negócios, nos quais pegar uma saída ruim e continuar operando pode resultar em muito dinheiro perdido. Se a correção for crucial e a falha for aceitável, as exceções terão uma vantagem muito grande aqui.
21717 Chris Hayes
11
@AdrianMaire - Eu acho que é muito mais difícil esquecer de lidar com uma exceção do que o seu método de esquecer uma instrução if ... Além disso - o principal benefício das exceções é qual camada lida com elas. Convém deixar uma exceção do sistema aparecer ainda mais para mostrar uma mensagem de erro no nível do aplicativo, mas lidar com situações conhecidas em um nível inferior. Se você estiver usando bibliotecas de terceiros ou outros desenvolvedores de código esta é realmente a única escolha ...
Milney
5
@ Adrian Não há engano, você parece ter interpretado mal o que eu escrevi ou perdi na segunda metade. Meu argumento não é que a exceção será lançada durante o teste / desenvolvimento e que os desenvolvedores perceberão que precisam lidar com eles. O ponto é que a consequência de uma exceção completamente não tratada na produção é preferível à consequência de um código de erro não verificado. Se você perder o código de erro, obtém e continua usando resultados incorretos. Se você perder a exceção, o aplicativo trava e não continua a ser executado, você não obtém resultados nem resultados errados . (cont.)
Mr.Mindor