Debug.Assert vs lanzamiento de excepciones

93

He leído muchos artículos (y un par de otras preguntas similares que se publicaron en StackOverflow) sobre cómo y cuándo usar afirmaciones, y las entendí bien. Pero aún así, no entiendo qué tipo de motivación debería llevarme a usar en Debug.Assertlugar de lanzar una simple excepción. Lo que quiero decir es que en .NET la respuesta predeterminada a una afirmación fallida es "detener el mundo" y mostrar un cuadro de mensaje al usuario. Aunque este tipo de comportamiento podría modificarse, me resulta muy molesto y redundante hacer eso, mientras que podría, en cambio, lanzar una excepción adecuada. De esta manera, podría escribir fácilmente el error en el registro de la aplicación justo antes de lanzar la excepción y, además, mi aplicación no necesariamente se congela.

Entonces, ¿por qué debería usar, en todo caso, en Debug.Assertlugar de una simple excepción? Colocar una aserción donde no debería estar podría causar todo tipo de "comportamiento no deseado", así que desde mi punto de vista, realmente no gano nada usando una aserción en lugar de lanzar una excepción. ¿Estás de acuerdo conmigo o me falta algo aquí?

Nota: Entiendo completamente cuál es la diferencia "en teoría" (Depurar vs Liberar, patrones de uso, etc.), pero como yo lo veo, sería mejor lanzar una excepción en lugar de realizar una afirmación. Dado que si se descubre un error en una versión de producción, todavía querría que la "aserción" fallara (después de todo, la "sobrecarga" es ridículamente pequeña), así que será mejor lanzar una excepción en su lugar.


Editar: A mi modo de ver, si una afirmación falló, eso significa que la aplicación entró en algún tipo de estado dañado e inesperado. Entonces, ¿por qué querría continuar con la ejecución? No importa si la aplicación se ejecuta en una versión de depuración o de lanzamiento. Lo mismo vale para ambos

Comunidad
fuente
1
Para las cosas que dice "si se descubre un error en una versión de producción, aún me gustaría que la" afirmación "fallara", las excepciones son lo que debe usar
Tom Neyland
1
El rendimiento es la única razón. La verificación nula todo el tiempo puede disminuir la velocidad, aunque puede pasar completamente desapercibida. Esto es principalmente para casos que nunca deberían suceder, por ejemplo, si sabe que ya lo ha verificado en una función anterior, no tiene sentido desperdiciar ciclos para verificarlo nuevamente. El debug.assert actúa efectivamente como una prueba unitaria de última oportunidad para informarle.
llega el

Respuestas:

176

Aunque estoy de acuerdo en que su razonamiento es plausible , es decir, si una afirmación se viola inesperadamente, tiene sentido detener la ejecución arrojándola, personalmente no usaría excepciones en lugar de afirmaciones. Este es el por qué:

Como han dicho otros, las aseveraciones deben documentar situaciones que son imposibles , de tal manera que si se da la situación supuestamente imposible, se informe al desarrollador. Las excepciones, por el contrario, proporcionan un mecanismo de flujo de control para situaciones excepcionales, poco probables o erróneas, pero no para situaciones imposibles. Para mí, la diferencia clave es esta:

  • SIEMPRE debería ser posible producir un caso de prueba que ejercite una declaración de lanzamiento determinada. Si no es posible producir un caso de prueba de este tipo, entonces tiene una ruta de código en su programa que nunca se ejecuta, y debe eliminarse como código muerto.

  • NUNCA debería ser posible producir un caso de prueba que provoque una afirmación. Si se activa una afirmación, el código es incorrecto o la afirmación es incorrecta; de cualquier manera, algo debe cambiar en el código.

Es por eso que no reemplazaría una afirmación con una excepción. Si la aserción no se puede disparar, reemplazarla con una excepción significa que tiene una ruta de código no comprobable en su programa . No me gustan las rutas de código no comprobables.

Eric Lippert
fuente
16
El problema con las afirmaciones es que no están en la construcción de producción. Fallar en una condición asumida significa que su programa ha entrado en un terreno de comportamiento indefinido, en cuyo caso un programa responsable debe detener la ejecución tan pronto como sea posible (desenrollar la pila también es algo peligroso, dependiendo de cuán riguroso quiera ser). Sí, las afirmaciones generalmente deberían ser imposibles de disparar, pero no sabes qué es posible cuando las cosas salen a la luz. Lo que pensó que era imposible podría suceder en la producción, y un programa responsable debería detectar las suposiciones violadas y actuar con prontitud.
kizzx2
2
@ kizzx2: OK, entonces, ¿cuántas excepciones imposibles por línea de código de producción escribe?
Eric Lippert
4
@ kixxx2: Esto es C #, por lo que puede mantener las aserciones en el código de producción utilizando Trace.Assert. Incluso puede usar el archivo app.config para redirigir las afirmaciones de producción a un archivo de texto en lugar de ser grosero con el usuario final.
HTTP 410
3
@AnorZaken: Su punto ilustra un defecto de diseño en las excepciones. Como he señalado en otra parte, las excepciones son (1) desastres fatales, (2) errores descarados que nunca deberían suceder, (3) fallas de diseño donde se usa una excepción para señalar una condición no excepcional, o (4) condiciones exógenas inesperadas . ¿Por qué estas cuatro cosas completamente diferentes están representadas por excepciones? Si tuviera mis druthers, las excepciones descabelladas de "null fue desreferenciado" no serían detectables en absoluto . Es Nunca derecha, y se debe interrumpir el programa antes de que haga más daño . Deberían ser más como afirma.
Eric Lippert
2
@Backwards_Dave: las afirmaciones falsas son malas, no las verdaderas. Las afirmaciones le permiten ejecutar costosas comprobaciones de verificación que no le gustaría ejecutar en producción. Y si una afirmación se viola en producción, ¿qué debe hacer el usuario final al respecto?
Eric Lippert
17

Las afirmaciones se utilizan para comprobar la comprensión del mundo por parte del programador. Una afirmación debe fallar solo si el programador ha hecho algo mal. Por ejemplo, nunca use una aserción para verificar la entrada del usuario.

Afirma la prueba de condiciones que "no pueden suceder". Las excepciones son para las condiciones que "no deberían ocurrir pero sí".

Las afirmaciones son útiles porque en el momento de la compilación (o incluso el tiempo de ejecución) puede cambiar su comportamiento. Por ejemplo, a menudo en las versiones de lanzamiento, las afirmaciones ni siquiera se controlan, porque introducen una sobrecarga innecesaria. Esto también es algo de lo que debe tener cuidado: es posible que sus pruebas ni siquiera se ejecuten.

Si usa excepciones en lugar de afirmaciones, pierde algo de valor:

  1. El código es más detallado, ya que probar y lanzar una excepción tiene al menos dos líneas, mientras que una aserción es solo una.

  2. Su código de prueba y lanzamiento siempre se ejecutará, mientras que las afirmaciones se pueden compilar.

  3. Pierde algo de comunicación con otros desarrolladores, porque las afirmaciones tienen un significado diferente al código de producto que verifica y arroja. Si realmente está probando una aserción de programación, use una aserción.

Más aquí: http://nedbatchelder.com/text/assert.html

Ned Batchelder
fuente
Si "no puede suceder", entonces ¿por qué escribir una afirmación? ¿No es eso redundante? Si realmente puede suceder pero no debería, entonces ¿no es lo mismo que "no debería suceder pero sí" que es para excepciones?
David Klempfner
2
"No puede suceder" está entre comillas por una razón: solo puede suceder si el programador ha hecho algo mal en otra parte del programa. La afirmación es una verificación contra los errores del programador.
Ned Batchelder
@NedBatchelder Sin embargo, el término programador es un poco ambiguo cuando se desarrolla una biblioteca. ¿Es correcto que esas "situaciones imposibles" deben ser imposible por el usuario de la biblioteca, pero podría ser posible cuando el autor de la biblioteca ha cometido un error?
Bruno Zell
12

EDITAR: En respuesta a la edición / nota que hizo en su publicación: parece que usar excepciones es lo correcto en lugar de usar afirmaciones para el tipo de cosas que está tratando de lograr. Creo que el obstáculo mental que está encontrando es que está considerando excepciones y afirmaciones para cumplir el mismo propósito, por lo que está tratando de averiguar cuál sería "correcto" para usar. Si bien puede haber cierta superposición en la forma en que se pueden usar las afirmaciones y excepciones, no confunda que para ellas son soluciones diferentes para el mismo problema, no lo son. Las afirmaciones y las excepciones tienen cada una su propio propósito, fortalezas y debilidades.

Iba a escribir una respuesta con mis propias palabras, pero esto hace que el concepto sea más justo de lo que yo hubiera hecho:

Estación C #: afirmaciones

El uso de declaraciones de aserción puede ser una forma eficaz de detectar errores de lógica de programa en tiempo de ejecución y, sin embargo, se eliminan fácilmente del código de producción. Una vez que se completa el desarrollo, el costo de ejecución de estas pruebas redundantes para errores de codificación se puede eliminar simplemente definiendo el símbolo de preprocesador NDEBUG [que deshabilita todas las afirmaciones] durante la compilación. Sin embargo, asegúrese de recordar que el código colocado en el propio aserción se omitirá en la versión de producción.

Una aserción se usa mejor para probar una condición solo cuando se cumplen todos los siguientes requisitos:

* 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.

Las afirmaciones casi nunca deben usarse para detectar situaciones que surgen durante el funcionamiento normal del software. Por ejemplo, por lo general, las afirmaciones no deben usarse para verificar errores en la entrada de un usuario. Sin embargo, puede tener sentido usar afirmaciones para verificar que una persona que llama ya haya verificado la entrada de un usuario.

Básicamente, use excepciones para cosas que necesitan ser detectadas / tratadas en una aplicación de producción, use aserciones para realizar verificaciones lógicas que serán útiles para el desarrollo pero que se desactivan en producción.

Tom Neyland
fuente
Me doy cuenta de todo eso. Pero la cuestión es que la misma declaración que marcó como negrita también se aplica a las excepciones. Así que, a mi modo de ver, en lugar de una afirmación, podría lanzar una excepción (ya que si la "situación que nunca debería ocurrir" ocurre en una versión implementada, aún me gustaría estar informado al respecto [además, la aplicación puede entrar en un estado dañado, por lo que una excepción es adecuada, es posible que no desee continuar con el flujo de ejecución normal)
1
Las afirmaciones deben usarse en invariantes; las excepciones deben usarse cuando, digamos, algo no debe ser nulo, pero lo será (como un parámetro para un método).
Ed S.
Supongo que todo se reduce a cuán defensivamente quieres codificar.
Ned Batchelder
Estoy de acuerdo, por lo que parece que necesita, las excepciones son el camino a seguir. Dijiste que te gustaría: Fallas detectadas en producción, la capacidad de registrar información sobre errores y control de flujo de ejecución, etc. Esas tres cosas me hacen pensar que lo que necesitas es lanzar algunas excepciones.
Tom Neyland
7

Creo que un ejemplo práctico (artificial) puede ayudar a aclarar la diferencia:

(adaptado de la extensión Batch de 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... */
}

Entonces, como han dicho Eric Lippert et al, solo afirma cosas que espera que sean correctas, en caso de que usted (el desarrollador) lo haya usado accidentalmente en otro lugar, para que pueda corregir su código. Básicamente, lanza excepciones cuando no tiene control o no puede anticipar lo que ingresa, por ejemplo , para la entrada del usuario , de modo que lo que le dio datos incorrectos puede responder de manera apropiada (por ejemplo, el usuario).

drzaus
fuente
¿No son tus 3 afirmaciones completamente redundantes? Es imposible que sus parámetros se evalúen como falso.
David Klempfner
1
Ese es el punto: las afirmaciones están ahí para documentar cosas que son imposibles. ¿Por qué harías eso? Porque es posible que tenga algo como ReSharper que le advierte dentro del método DoSomethingImpl que "podría estar desreferenciando nulo aquí" y quiere decirle "Sé lo que estoy haciendo, esto nunca puede ser nulo". También es una indicación para algún programador posterior, que quizás no se dé cuenta de inmediato de la conexión entre DoSomething y DoSomethingImpl, especialmente si están separados por cientos de líneas.
Marcel Popescu
4

Otra pepita de Code Complete :

"Una aserción es una función o macro que se queja en voz alta si una suposición no es cierta. Utilice aserciones para documentar las suposiciones realizadas en el código y eliminar las condiciones inesperadas ...

"Durante el desarrollo, las afirmaciones eliminan las suposiciones contradictorias, las condiciones inesperadas, los valores incorrectos que se pasan a las rutinas, etc."

Continúa agregando algunas pautas sobre lo que debe y no debe afirmarse.

Por otro lado, excepciones:

"Utilice el manejo de excepciones para llamar la atención sobre casos inesperados. Los casos excepcionales deben manejarse de una manera que los haga obvios durante el desarrollo y recuperables cuando se ejecuta el código de producción".

Si no tiene este libro, debería comprarlo.

Andrew Cowenhoven
fuente
2
He leído el libro, es excelente. Sin embargo ... no respondió a mi pregunta :)
Tienes razón, no respondí. Mi respuesta es no, no estoy de acuerdo contigo. Las afirmaciones y las excepciones son animales diferentes, como se mencionó anteriormente y algunas de las otras respuestas publicadas aquí.
Andrew Cowenhoven
0

Debug.Assert de forma predeterminada solo funcionará en compilaciones de depuración, por lo que si desea detectar cualquier tipo de comportamiento inesperado en sus compilaciones de lanzamiento, deberá usar excepciones o activar la constante de depuración en las propiedades de su proyecto (que se considera en general para no ser una buena idea).

Mez
fuente
La primera oración parcial es verdadera, el resto es en general una mala idea: las afirmaciones son suposiciones y no hay validación (como se indicó anteriormente), habilitar la depuración en la versión realmente no es una opción.
Marc Wittke
0

Use afirmaciones para cosas que SON posibles pero que no deberían suceder (si fuera imposible, ¿por qué pondría una afirmación?).

¿No suena como un caso para usar un Exception? ¿Por qué usarías una aserción en lugar de una Exception?

Porque debería haber un código que se llame antes de su aserción que evitaría que el parámetro de la aserción sea falso.

Por lo general, no hay ningún código antes de su Exceptionque garantice que no se lanzará.

¿Por qué es bueno que Debug.Assert()se compile en prod? Si quiere saberlo en debug, ¿no querría saberlo en prod?

Lo desea solo durante el desarrollo, porque una vez que encuentra Debug.Assert(false)situaciones, escribe código para garantizar que eso Debug.Assert(false)no vuelva a suceder. Una vez finalizado el desarrollo, asumiendo que ha encontrado las Debug.Assert(false)situaciones y las ha solucionado, Debug.Assert()se pueden compilar de forma segura, ya que ahora son redundantes.

David Klempfner
fuente
0

Suponga que es miembro de un equipo bastante grande y hay varias personas trabajando en la misma base de código general, incluida la superposición de clases. Puede crear un método que sea llamado por varios otros métodos, y para evitar la contención de bloqueo, no le agrega un bloqueo separado, sino que "asume" que fue bloqueado previamente por el método de llamada con un bloqueo específico. Por ejemplo, Debug.Assert (RepositoryLock.IsReadLockHeld || RepositoryLock.IsWriteLockHeld); Los otros desarrolladores pueden pasar por alto un comentario que dice que el método de llamada debe usar el bloqueo, pero no pueden ignorarlo.

daniel
fuente