Debug.Assert vs Exception Throwing

92

Eu li muitos artigos (e algumas outras perguntas semelhantes que foram postadas no StackOverflow) sobre como e quando usar asserções e as entendi bem. Mesmo assim, não entendo que tipo de motivação deveria me levar a usar em Debug.Assertvez de lançar uma exceção simples. O que quero dizer é que, no .NET, a resposta padrão a uma declaração com falha é "parar o mundo" e exibir uma caixa de mensagem para o usuário. Embora esse tipo de comportamento possa ser modificado, acho altamente irritante e redundante fazer isso, enquanto eu poderia, em vez disso, apenas lançar uma exceção adequada. Dessa forma, eu poderia facilmente gravar o erro no log do aplicativo antes de lançar a exceção e, além disso, meu aplicativo não necessariamente congela.

Então, por que eu deveria, se fosse, usar em Debug.Assertvez de uma exceção simples? Colocar uma asserção onde não deveria estar poderia causar todos os tipos de "comportamento indesejado", então, no meu ponto de vista, eu realmente não ganho nada usando uma asserção em vez de lançar uma exceção. Você concorda comigo ou estou faltando alguma coisa aqui?

Nota: Eu entendo perfeitamente qual é a diferença "em teoria" (Depurar versus Liberar, padrões de uso etc.), mas a meu ver, seria melhor lançar uma exceção em vez de executar uma declaração. Visto que se um bug for descoberto em uma versão de produção, eu ainda quero que a "asserção" falhe (afinal, a "sobrecarga" é ridiculamente pequena), então seria melhor lançar uma exceção.


Edit: A meu ver, se uma declaração falhou, isso significa que o aplicativo entrou em algum tipo de estado corrompido e inesperado. Então, por que eu iria querer continuar a execução? Não importa se o aplicativo é executado em uma versão de depuração ou lançamento. O mesmo vale para ambos

Comunidade
fonte
1
Para coisas que você está dizendo "se um bug for descoberto em uma versão de produção, eu ainda gostaria que a" afirmação "falhe" sobre, exceções são o que você deve usar
Tom Neyland
1
O desempenho é a única razão. Verificar tudo nulo o tempo todo pode diminuir a velocidade, embora possa ser completamente imperceptível. Isso é principalmente para casos que nunca deveriam acontecer, por exemplo, você sabe que já o verificou em uma função anterior, não há nenhum ponto em ciclos verificando-o novamente. O debug.assert atua efetivamente como um teste de unidade de última chance para informá-lo.
rola em

Respostas:

175

Embora eu concorde que seu raciocínio seja plausível - isto é, se uma afirmação é violada inesperadamente, faz sentido interromper a execução lançando - eu pessoalmente não usaria exceções no lugar de afirmações. Aqui está o porquê:

Como já foi dito, as assertivas devem documentar as situações impossíveis , de forma que caso a suposta situação impossível se concretize, o desenvolvedor seja informado. As exceções, por outro lado, fornecem um mecanismo de fluxo de controle para situações excepcionais, improváveis ​​ou errôneas, mas não para situações impossíveis. Para mim, a principal diferença é esta:

  • Deve ser SEMPRE possível produzir um caso de teste que exerça uma determinada instrução de lance. Se não for possível produzir tal caso de teste, então você tem um caminho de código em seu programa que nunca é executado e deve ser removido como código morto.

  • NUNCA deve ser possível produzir um caso de teste que provoque o disparo de uma asserção. Se uma asserção for disparada, o código está errado ou a asserção está errada; de qualquer forma, algo precisa mudar no código.

É por isso que eu não substituiria uma afirmação por uma exceção. Se a asserção não puder realmente disparar, substituí-la por uma exceção significa que você tem um caminho de código não testável em seu programa . Não gosto de caminhos de código não testáveis.

Eric Lippert
fonte
16
O problema com as afirmações é que elas não estão presentes na construção de produção. A falha em uma condição assumida significa que seu programa entrou em um terreno de comportamento indefinido, caso em que um programa responsável deve interromper a execução o mais rápido possível (desfazer a pilha também é um tanto perigoso, dependendo de quão rigoroso você deseja obter). Sim, as afirmações geralmente deveriam ser impossíveis de disparar, mas você não sabe o que é possível quando as coisas acontecem no mundo selvagem. O que você considerou impossível pode acontecer na produção, e um programa responsável deve detectar suposições violadas e agir imediatamente.
kizzx2
2
@ kizzx2: OK, então quantas exceções impossíveis você escreve por linha de código de produção?
Eric Lippert
4
@ kixxx2: Este é C #, portanto, você pode manter as afirmações no código de produção usando Trace.Assert. Você pode até usar o arquivo app.config para redirecionar as asserções de produção para um arquivo de texto, em vez de ser rude com o usuário final.
HTTP 410
3
@AnorZaken: Seu ponto ilustra uma falha de design nas exceções. Como observei em outro lugar, as exceções são (1) desastres fatais, (2) erros estúpidos que nunca deveriam acontecer, (3) falhas de projeto em que uma exceção é usada para sinalizar uma condição não excepcional ou (4) condições exógenas inesperadas . Por que essas quatro coisas completamente diferentes são todas representadas por exceções? Se eu tivesse meus druthers, boneheaded "nulo foi desreferenciado" exceções não seria catchable em tudo . Nunca está certo e deve encerrar o programa antes que cause mais danos . Eles deveriam ser mais como afirmações.
Eric Lippert
2
@Backwards_Dave: falsas afirmações são ruins, não verdadeiras. As asserções permitem que você execute verificações caras que você não gostaria de executar na produção. E se uma declaração for violada na produção, o que o usuário final deve fazer a respeito?
Eric Lippert
17

As asserções são usadas para verificar a compreensão do programador sobre o mundo. Uma asserção deve falhar apenas se o programador tiver feito algo errado. Por exemplo, nunca use uma asserção para verificar a entrada do usuário.

Realiza teste para condições que "não podem acontecer". As exceções são para condições que "não deveriam acontecer, mas acontecem".

Assertions são úteis porque em tempo de construção (ou até mesmo tempo de execução) você pode mudar seu comportamento. Por exemplo, frequentemente em compilações de lançamento, as declarações nem são verificadas, porque introduzem sobrecarga desnecessária. Isso também é algo para se ter cuidado: seus testes podem nem mesmo ser executados.

Se você usar exceções em vez de declarações, perderá algum valor:

  1. O código é mais detalhado, já que testar e lançar uma exceção tem pelo menos duas linhas, enquanto uma declaração é apenas uma.

  2. Seu código de teste e lançamento sempre será executado, enquanto afirmações podem ser compiladas.

  3. Você perde alguma comunicação com outros desenvolvedores, porque afirmações têm um significado diferente do código do produto que verifica e lança. Se você estiver realmente testando uma asserção de programação, use uma assert.

Mais aqui: http://nedbatchelder.com/text/assert.html

Ned Batchelder
fonte
Se isso "não pode acontecer", por que escrever uma afirmação. Isso não é redundante? Se isso pode realmente acontecer, mas não deveria, então não é o mesmo que "não deveria acontecer, mas acontece", que é para exceções?
David Klempfner
2
"Não pode acontecer" está entre aspas por um motivo: só pode acontecer se o programador tiver feito algo errado em outra parte do programa. A afirmação é uma verificação dos erros do programador.
Ned Batchelder
@NedBatchelder O termo programador é um pouco ambíguo quando você desenvolve uma biblioteca. É certo que essas "situações impossíveis" deve ser impossível pelo usuário biblioteca, mas poderia ser possível quando o autor biblioteca cometeu um erro?
Bruno Zell,
12

EDIT: Em resposta à edição / nota que você fez em sua postagem: Parece que usar exceções é a coisa certa a usar em vez de asserções para o tipo de coisas que você está tentando realizar. Acho que o obstáculo mental que você está atingindo é que está considerando exceções e afirmações para cumprir o mesmo propósito e, portanto, está tentando descobrir qual seria "certo" usar. Embora possa haver alguma sobreposição em como as asserções e exceções podem ser usadas, não confunda isso, por serem soluções diferentes para o mesmo problema - não são. Cada uma das afirmações e exceções tem seu próprio propósito, pontos fortes e fracos.

Eu ia digitar uma resposta com minhas próprias palavras, mas isso faz justiça ao conceito melhor do que eu faria:

Estação C #: Asserções

O uso de instruções assert pode ser uma maneira eficaz de detectar erros de lógica de programa em tempo de execução, e ainda assim eles são facilmente filtrados do código de produção. Depois que o desenvolvimento for concluído, o custo de tempo de execução desses testes redundantes para erros de codificação pode ser eliminado simplesmente definindo o símbolo do pré-processador NDEBUG [que desativa todas as asserções] durante a compilação. Lembre-se, entretanto, de que o código colocado no próprio assert será omitido na versão de produção.

Uma asserção é mais bem usada para testar uma condição somente quando todas as seguintes são válidas:

* the condition should never be false if the code is correct,
* the condition is not so trivial so as to obviously be always true, and
* the condition is in some sense internal to a body of software.

As asserções quase nunca devem ser usadas para detectar situações que surgem durante a operação normal do software. Por exemplo, geralmente as asserções não devem ser usadas para verificar se há erros na entrada de um usuário. Pode, entretanto, fazer sentido usar asserções para verificar se um chamador já verificou a entrada de um usuário.

Basicamente, use exceções para coisas que precisam ser capturadas / tratadas em um aplicativo de produção, use asserções para realizar verificações lógicas que serão úteis para o desenvolvimento, mas desligadas na produção.

Tom Neyland
fonte
Eu percebo tudo isso. Mas o fato é que a mesma declaração que você marcou como negrito também vai para as exceções. Então, a meu ver, em vez de uma afirmação, eu poderia apenas lançar uma exceção (já que se a "situação que nunca deveria ocorrer" acontecer em uma versão implantada, eu ainda gostaria de ser informado sobre isso [mais, o aplicativo pode entrar em um estado corrompido, portanto, se uma exceção for adequada, posso não querer continuar o fluxo de execução normal)
1
As asserções devem ser usadas em invariantes; exceções devem ser usadas quando, digamos, algo não deve ser nulo, mas será (como um parâmetro para um método).
Ed S.
Eu acho que tudo se resume a quão defensivamente você deseja codificar.
Ned Batchelder
Eu concordo, pelo que parece que você precisa, as exceções são o caminho a percorrer. Você disse que gostaria de: Falhas detectadas na produção, capacidade de registrar informações sobre erros e controle do fluxo de execução, etc. Essas três coisas me fazem pensar que o que você precisa fazer é lançar algumas exceções.
Tom Neyland
7

Acho que um exemplo prático (inventado) pode ajudar a iluminar a diferença:

(adaptado da extensão Batch MoreLinq )

// 'public facing' method
public int DoSomething(List<string> stuff, object doohickey, int limit) {

    // validate user input and report problems externally with exceptions

    if(stuff == null) throw new ArgumentNullException("stuff");
    if(doohickey == null) throw new ArgumentNullException("doohickey");
    if(limit <= 0) throw new ArgumentOutOfRangeException("limit", limit, "Should be > 0");

    return DoSomethingImpl(stuff, doohickey, limit);
}

// 'developer only' method
private static int DoSomethingImpl(List<string> stuff, object doohickey, int limit) {

    // validate input that should only come from other programming methods
    // which we have control over (e.g. we already validated user input in
    // the calling method above), so anything using this method shouldn't
    // need to report problems externally, and compilation mode can remove
    // this "unnecessary" check from production

    Debug.Assert(stuff != null);
    Debug.Assert(doohickey != null);
    Debug.Assert(limit > 0);

    /* now do the actual work... */
}

Então, como Eric Lippert et al disseram, você apenas afirma coisas que espera que estejam corretas, apenas no caso de você (o desenvolvedor) acidentalmente ter usado errado em outro lugar, para que possa corrigir seu código. Basicamente, você lança exceções quando não tem controle sobre ou não pode antecipar o que chega, por exemplo , para a entrada do usuário , de modo que o que quer que tenha fornecido dados incorretos possa responder adequadamente (por exemplo, o usuário).

drzaus
fonte
Seus 3 afirmações não são completamente redundantes? É impossível que seus parâmetros sejam avaliados como falsos.
David Klempfner
1
Esse é o ponto - as afirmações existem para documentar coisas que são impossíveis. Por que você faria isso? Porque você pode ter algo como ReSharper que avisa dentro do método DoSomethingImpl que "você pode estar desreferenciando null aqui" e quer dizer "Eu sei o que estou fazendo, isso nunca pode ser nulo". É também uma indicação para algum programador posterior, que pode não perceber imediatamente a conexão entre DoSomething e DoSomethingImpl, especialmente se eles estiverem separados por centenas de linhas.
Marcel Popescu
4

Outra pepita do Code Complete :

"Uma asserção é uma função ou macro que reclama em voz alta se uma suposição não for verdadeira. Use afirmações para documentar suposições feitas no código e para eliminar condições inesperadas. ...

"Durante o desenvolvimento, as afirmações eliminam suposições contraditórias, condições inesperadas, valores ruins passados ​​para as rotinas e assim por diante."

Ele acrescenta algumas diretrizes sobre o que deve e o que não deve ser afirmado.

Por outro lado, exceções:

"Use o tratamento de exceções para chamar a atenção para casos inesperados. Os casos excepcionais devem ser tratados de uma forma que os torne óbvios durante o desenvolvimento e recuperáveis ​​quando o código de produção estiver em execução."

Se você não tem este livro, deve comprá-lo.

Andrew Cowenhoven
fonte
2
Eu li o livro, é excelente. No entanto .. você não respondeu minha pergunta :)
Você está certo, eu não respondi. Minhas respostas são não, não concordo com você. Asserções e exceções são animais diferentes, conforme abordado acima e algumas das outras respostas postadas aqui.
Andrew Cowenhoven
0

Debug.Assert por padrão só funcionará em compilações de depuração, então se você quiser detectar qualquer tipo de comportamento inesperado em suas compilações de lançamento, você precisará usar exceções ou ativar a constante de depuração nas propriedades do projeto (que é considerado em geral não é uma boa ideia).

Mez
fonte
a primeira frase parcial é verdadeira, o resto é, em geral, uma má ideia: as afirmações são suposições e não há validação (como afirmado acima), habilitar a depuração no lançamento realmente não é uma opção.
Marc Wittke
0

Use asserções para coisas que SÃO possíveis, mas não deveriam acontecer (se fosse impossível, por que você colocaria uma asserção?).

Não parece um caso para usar um Exception? Por que você usaria uma afirmação em vez de um Exception?

Porque deve haver um código que é chamado antes de sua declaração para impedir que o parâmetro da declaração seja falso.

Normalmente não há nenhum código antes de você Exceptionque garanta que ele não será lançado.

Por que é bom que Debug.Assert()seja compilado no prod? Se você quiser saber sobre isso no debug, não gostaria de saber sobre isso no prod?

Você quer isso apenas durante o desenvolvimento, porque, depois de encontrar as Debug.Assert(false)situações, você escreve o código para garantir que Debug.Assert(false)isso não aconteça novamente. Assim que o desenvolvimento estiver concluído, supondo que você encontrou as Debug.Assert(false)situações e as corrigiu, eles Debug.Assert()podem ser compilados com segurança, pois agora são redundantes.

David Klempfner
fonte
0

Suponha que você seja membro de uma equipe bastante grande e que haja várias pessoas trabalhando na mesma base de código geral, incluindo a sobreposição de classes. Você pode criar um método que é chamado por vários outros métodos e, para evitar contenção de bloqueio, não adiciona um bloqueio separado a ele, mas "assume" que foi bloqueado anteriormente pelo método de chamada com um bloqueio específico. Como Debug.Assert (RepositoryLock.IsReadLockHeld || RepositoryLock.IsWriteLockHeld); Os outros desenvolvedores podem ignorar um comentário que diz que o método de chamada deve usar o bloqueio, mas eles não podem ignorar isso.

Daniel
fonte