language agnostic - ¿Cuándo debo usar Debug.Assert()?
language-agnostic exception (20)
Cita tomada de The Pragmatic Programmer: De Journeyman a Master
Dejar aserciones activadas
Hay un malentendido común acerca de las afirmaciones, promulgado por las personas que escriben compiladores y entornos de lenguaje. Es algo parecido a esto:
Las aserciones agregan algo de sobrecarga al código. Debido a que verifican las cosas que nunca deberían suceder, solo se activarán por un error en el código. Una vez que el código ha sido probado y enviado, ya no son necesarios, y deben desactivarse para que el código se ejecute más rápido. Las afirmaciones son una facilidad de depuración.
Hay dos suposiciones evidentemente erróneas aquí. Primero, asumen que las pruebas encuentran todos los errores. En realidad, para cualquier programa complejo es poco probable que pruebe incluso un porcentaje minúsculo de las permutaciones a las que se someterá su código (consulte Pruebas implacables).
En segundo lugar, los optimistas están olvidando que su programa se ejecuta en un mundo peligroso. Durante las pruebas, es probable que las ratas no corroan un cable de comunicaciones, alguien que juegue un juego no agotará la memoria y los archivos de registro no llenarán el disco duro. Estas cosas pueden suceder cuando su programa se ejecuta en un entorno de producción. Su primera línea de defensa está revisando cualquier posible error, y la segunda está usando aserciones para tratar de detectar las que ha perdido.
Desactivar las afirmaciones cuando se entrega un programa a la producción es como cruzar un cable alto sin una red, porque una vez lo logró en la práctica . Hay un valor dramático, pero es difícil obtener un seguro de vida.
Incluso si tiene problemas de rendimiento, desactive solo las afirmaciones que realmente lo afectaron .
He sido un ingeniero de software profesional durante aproximadamente un año, habiéndome graduado con un título de CS. He sabido de aserciones por un tiempo en C ++ y C, pero no tenía idea de que existieran en C # y .NET hasta hace poco.
Nuestro código de producción no contiene afirmaciones de ningún tipo y mi pregunta es esta ...
¿Debo comenzar a utilizar Asserts en nuestro código de producción? Y si es así, ¿cuándo es más apropiado su uso? ¿Tendría más sentido hacer?
Debug.Assert(val != null);
o
if ( val == null )
throw new exception();
Coloque Debug.Assert()
en todas partes del código donde desee tener controles de seguridad para garantizar invariantes. Cuando compila una versión de compilación (es decir, no es una constante del compilador DEBUG
), las llamadas a Debug.Assert()
se eliminarán para que no afecten el rendimiento.
Aún debe lanzar excepciones antes de llamar a Debug.Assert()
. La afirmación solo se asegura de que todo sea como se esperaba mientras aún se está desarrollando.
8 Programación Defensiva
8.2 Afirmaciones
Una afirmación es un código que se usa durante el desarrollo, generalmente una rutina o macro, que permite que un programa se verifique a sí mismo mientras se ejecuta. Cuando una afirmación es verdadera, eso significa que todo funciona como se espera. Cuando es falso, eso significa que ha detectado un error inesperado en el código. Por ejemplo, si el sistema asume que un archivo de información del cliente nunca tendrá más de 50,000 registros, el programa podría contener una afirmación de que el número de registros es menor o igual a 50,000. Mientras el número de registros sea menor o igual a 50,000, la afirmación será silenciosa. Sin embargo, si encuentra más de 50,000 registros, "afirmará" en voz alta que hay un error en el programa.
Las afirmaciones son especialmente útiles en programas grandes y complicados y en programas de alta confiabilidad. Permiten a los programadores eliminar más rápidamente las suposiciones de interfaz no coincidentes, los errores que surgen cuando se modifica el código, etc.
Una aserción generalmente toma dos argumentos: una expresión booleana que describe la suposición que se supone que es verdadera y un mensaje para mostrar si no lo es.
(…)
Normalmente, no desea que los usuarios vean los mensajes de afirmación en el código de producción; Las afirmaciones son principalmente para uso durante el desarrollo y mantenimiento. Las aserciones normalmente se compilan en el código en el momento del desarrollo y se compilan del código para la producción. Durante el desarrollo, las afirmaciones eliminan las suposiciones contradictorias, las condiciones inesperadas, los valores incorrectos pasados a las rutinas, etc. Durante la producción, se compilan del código para que las afirmaciones no degraden el rendimiento del sistema.
De acuerdo con el estándar IDesign , debería
Hacer valer todos los supuestos. En promedio, cada quinta línea es una afirmación.
using System.Diagnostics;
object GetObject()
{...}
object someObject = GetObject();
Debug.Assert(someObject != null);
Como descargo de responsabilidad, debo mencionar que no me ha resultado práctico implementar este IRL. Pero esta es su norma.
Debería usar Debug.Assert para probar errores lógicos en sus programas. El compilador solo puede informarle de los errores de sintaxis. Por lo tanto, definitivamente debe usar las declaraciones Assert para probar errores lógicos. Por ejemplo, al probar un programa que vende autos, solo los BMW que son azules deberían obtener un descuento del 15%. El complaciente no podría decirle nada sobre si su programa es lógicamente correcto al realizar esto, pero sí una declaración de afirmación.
En aplicaciones de depuración de Microsoft .NET 2.0, John Robbins tiene una gran sección de aserciones. Sus principales puntos son:
- Afirmar generosamente. Nunca puedes tener demasiadas afirmaciones.
- Las aserciones no reemplazan las excepciones. Las excepciones cubren las cosas que su código exige; Las aserciones cubren las cosas que asume.
- Una afirmación bien escrita puede decirle no solo qué sucedió y dónde (como una excepción), sino por qué.
- Un mensaje de excepción a menudo puede ser críptico, requiriendo que trabaje hacia atrás a través del código para recrear el contexto que causó el error. Una afirmación puede preservar el estado del programa en el momento en que ocurrió el error.
- Las aseveraciones se doblan como documentación, y le dicen a otros desarrolladores de qué supuestos implícitos depende su código.
- El cuadro de diálogo que aparece cuando falla una aserción le permite adjuntar un depurador al proceso, para que pueda hurgar en la pila como si hubiera puesto un punto de interrupción allí.
PD: Si te gustó Code Complete, te recomiendo que lo sigas con este libro. Lo compré para aprender sobre el uso de WinDBG y los archivos de volcado, pero la primera mitad está llena de consejos para ayudar a evitar errores en primer lugar.
FWIW ... Encuentro que mis métodos públicos tienden a usar el if () { throw; }
if () { throw; }
patrón para asegurar que el método se está llamando correctamente. Mis métodos privados tienden a usar Debug.Assert()
.
La idea es que con mis métodos privados, yo soy el que está bajo control, así que si comienzo a llamar a uno de mis propios métodos privados con parámetros que son incorrectos, entonces rompí mi propia suposición en algún lugar, nunca debería haber recibido en ese estado En producción, estas afirmaciones privadas deberían ser idealmente un trabajo innecesario, ya que se supone que debo mantener mi estado interno válido y consistente. Contraste con los parámetros dados a los métodos públicos, que pueden ser llamados por cualquier persona en el tiempo de ejecución: todavía necesito hacer cumplir las restricciones de los parámetros al lanzar excepciones.
Además, mis métodos privados aún pueden generar excepciones si algo no funciona en tiempo de ejecución (error de red, error de acceso a datos, datos incorrectos recuperados de un servicio de terceros, etc.). Mis afirmaciones están ahí solo para asegurarme de que no he roto mis propias suposiciones internas sobre el estado del objeto.
He leído las respuestas aquí y pensé que debería agregar una distinción importante. Hay dos formas muy diferentes en las que se utilizan las afirmaciones. Uno es como método abreviado de desarrollador temporal para "Esto realmente no debería suceder, así que si me lo hace saber, puedo decidir qué hacer", como un punto de interrupción condicional, para los casos en que su programa puede continuar. El otro, es una forma de poner suposiciones sobre estados de programa válidos en su código.
En el primer caso, las afirmaciones ni siquiera necesitan estar en el código final. Debe usar Debug.Assert
durante el desarrollo y puede eliminarlos si / cuando ya no sea necesario. Si quieres dejarlos o si olvidas eliminarlos, no hay problema, ya que no tendrán ninguna consecuencia en las compilaciones de Release.
Pero en el segundo caso, las aseveraciones son parte del código. Ellos, bueno, afirman que sus suposiciones son ciertas y también las documentan. En ese caso, realmente desea dejarlos en el código. Si el programa está en un estado no válido, no se debe permitir que continúe. Si no pudieras permitirte el impacto de rendimiento, no estarías usando C #. Por un lado, podría ser útil poder adjuntar un depurador si sucede. Por otro lado, no desea que el seguimiento de la pila aparezca en sus usuarios y, tal vez, lo más importante es que no desee que puedan ignorarlo. Además, si está en un servicio siempre será ignorado. Por lo tanto, en la producción, el comportamiento correcto sería lanzar una excepción y utilizar el manejo normal de excepciones de su programa, que podría mostrar al usuario un mensaje agradable y registrar los detalles.
Trace.Assert
tiene la manera perfecta de lograr esto. No se eliminará en producción, y se puede configurar con diferentes escuchas usando app.config. Por lo tanto, para el desarrollo, el controlador predeterminado está bien, y para la producción puede crear un TraceListener simple como el que muestra una excepción y activarlo en el archivo de configuración de producción.
using System.Diagnostics;
public class ExceptionTraceListener : DefaultTraceListener
{
[DebuggerStepThrough]
public override void Fail(string message, string detailMessage)
{
throw new AssertException(message);
}
}
public class AssertException : Exception
{
public AssertException(string message) : base(message) { }
}
Y en el archivo de configuración de producción:
<system.diagnostics>
<trace>
<listeners>
<remove name="Default"/>
<add name="ExceptionListener" type="Namespace.ExceptionTraceListener,AssemblyName"/>
</listeners>
</trace>
</system.diagnostics>
Los avisos se utilizan para detectar el error del programador (el suyo), no el error del usuario. Deben usarse solo cuando no hay posibilidad de que un usuario pueda hacer que se active la afirmación. Si está escribiendo una API, por ejemplo, las afirmaciones no deben usarse para verificar que un argumento no sea nulo en ningún método que un usuario de API pueda llamar. Pero podría usarse en un método privado no expuesto como parte de su API para afirmar que SU código nunca pasa un argumento nulo cuando no se supone que lo haga.
Normalmente prefiero las excepciones a las afirmaciones cuando no estoy seguro.
Mayormente nunca en mi libro. En la gran mayoría de las ocasiones, si desea comprobar si todo está en su sano juicio, lance si no lo está.
Lo que no me gusta es el hecho de que hace que una construcción de depuración sea funcionalmente diferente a una versión de lanzamiento. Si falla una aserción de depuración pero la funcionalidad funciona en el lanzamiento, ¿cómo tiene eso sentido? Es incluso mejor cuando el asertor ha dejado la compañía por mucho tiempo y nadie sabe esa parte del código. Luego tienes que perder parte de tu tiempo explorando el problema para ver si realmente es un problema o no. Si es un problema, ¿por qué la persona no está lanzando en primer lugar?
Para mí, esto sugiere que al usar Debug.Asserts, le está transfiriendo el problema a otra persona, trate el problema usted mismo. Si algo se supone que es el caso y no es entonces tirar.
Supongo que hay posiblemente escenarios críticos de rendimiento en los que desea optimizar sus aserciones y son útiles allí, sin embargo, todavía tengo que encontrar ese escenario.
Pensé que agregaría cuatro casos más, donde Debug.Assert puede ser la opción correcta.
1) Algo que no he visto mencionado aquí es la cobertura conceptual adicional que los Asignaciones pueden proporcionar durante las pruebas automatizadas . Como un simple ejemplo:
Cuando un autor que modifica a una persona que llama de nivel superior cree que ha expandido el alcance del código para manejar escenarios adicionales, idealmente (!) Escribirán pruebas unitarias para cubrir esta nueva condición. Puede ser que el código totalmente integrado parezca funcionar bien.
Sin embargo, en realidad se ha introducido un defecto sutil, pero no se ha detectado en los resultados de las pruebas. El beneficiario de la llamada se ha vuelto no determinista en este caso, y solo sucede para proporcionar el resultado esperado. O tal vez ha producido un error de redondeo que pasó desapercibido. O causó un error que fue compensado igualmente en otros lugares. O concedió no solo el acceso solicitado, sino privilegios adicionales que no deberían otorgarse. Etc.
En este punto, las declaraciones Debug.Assert () contenidas en el destinatario junto con el nuevo caso (o caso de borde) conducido por pruebas unitarias pueden proporcionar una notificación invaluable durante la prueba de que las suposiciones del autor original se han invalidado, y el código no debe Ser lanzado sin revisión adicional. Afirmaciones con pruebas unitarias son los compañeros perfectos.
2) Además, algunas pruebas son fáciles de escribir, pero de alto costo e innecesarias dadas las suposiciones iniciales . Por ejemplo:
Si solo se puede acceder a un objeto desde un determinado punto de entrada seguro, ¿se debe realizar una consulta adicional en una base de datos de derechos de red desde cada método de objeto para garantizar que la persona que llama tenga permisos? Seguramente no. Quizás la solución ideal incluye el almacenamiento en caché o alguna otra expansión de características, pero el diseño no lo requiere. Un Debug.Assert () mostrará inmediatamente cuando el objeto se ha adjuntado a un punto de entrada inseguro.
3) A continuación, en algunos casos, su producto puede no tener una interacción de diagnóstico útil para todas o parte de sus operaciones cuando se implementa en modo de lanzamiento . Por ejemplo:
Supongamos que es un dispositivo integrado en tiempo real. Lanzar excepciones y reiniciar cuando encuentra un paquete mal formado es contraproducente. En su lugar, el dispositivo puede beneficiarse de la operación de mejor esfuerzo, incluso hasta el punto de generar ruido en su salida. Es posible que tampoco tenga una interfaz humana, un dispositivo de registro, o que el humano no pueda acceder físicamente en absoluto cuando se implementa en modo de lanzamiento, y la mejor manera de conocer los errores es evaluar el mismo resultado. En este caso, las afirmaciones liberales y las pruebas exhaustivas previas al lanzamiento son más valiosas que las excepciones.
4) Por último, algunas pruebas son innecesarias solo porque la persona que recibe la llamada se percibe como extremadamente confiable . En la mayoría de los casos, cuanto más reutilizable es el código, más esfuerzo se ha puesto para hacerlo confiable. Por lo tanto, es común la excepción para los parámetros inesperados de las personas que llaman, pero afirmar para obtener resultados inesperados de las personas que llaman. Por ejemplo:
Si una operación principal de String.Find
indica que devolverá un -1
cuando no se encuentren los criterios de búsqueda, es posible que pueda realizar una operación de manera segura en lugar de tres. Sin embargo, si realmente devolvió -2
, es posible que no tenga un curso de acción razonable. Sería inútil reemplazar el cálculo más simple con uno que pruebe por separado un valor de -1
, e irrazonable en la mayoría de los entornos de lanzamiento para ensuciar su código con pruebas que aseguren que las bibliotecas principales funcionen como se espera. En este caso las afirmaciones son ideales.
Si desea Asserts en su código de producción (es decir, compilaciones de lanzamiento), puede usar Trace.Assert en lugar de Debug.Assert.
Por supuesto, esto agrega sobrecarga a su ejecutable de producción.
Además, si su aplicación se ejecuta en modo de interfaz de usuario, el cuadro de diálogo de Aserción se mostrará de forma predeterminada, lo que puede ser un poco desconcertante para sus usuarios.
Puede anular este comportamiento eliminando DefaultTraceListener: consulte la documentación de Trace.Listeners en MSDN.
En resumen,
Use Debug.Assert generosamente para ayudar a detectar errores en las versiones de Debug.
Si usa Trace.Assert en modo de interfaz de usuario, probablemente desee eliminar DefaultTraceListener para evitar desconcertar a los usuarios.
Si la condición que está probando es algo que su aplicación no puede manejar, probablemente esté mejor lanzando una excepción, para asegurarse de que la ejecución no continúe. Tenga en cuenta que un usuario puede optar por ignorar una aserción.
Si yo fuera tú lo haría:
Debug.Assert(val != null);
if ( val == null )
throw new exception();
O para evitar la condición repetida.
if ( val == null )
{
Debug.Assert(false,"breakpoint if val== null");
throw new exception();
}
Siempre debe utilizar el segundo enfoque (lanzar excepciones).
Además, si está en producción (y tiene una versión de lanzamiento), es mejor lanzar una excepción (y dejar que la aplicación se bloquee en el peor de los casos) que trabajar con valores no válidos y tal vez destruir los datos de su cliente (que pueden costar miles de dólares). de dólares).
Todas las afirmaciones deben ser códigos que podrían optimizarse para:
Debug.Assert(true);
Porque está comprobando que algo que ya has asumido es cierto. P.ej:
public static void ConsumeEnumeration<T>(this IEnumerable<T> source)
{
if(source != null)
using(var en = source.GetEnumerator())
RunThroughEnumerator(en);
}
public static T GetFirstAndConsume<T>(this IEnumerable<T> source)
{
if(source == null)
throw new ArgumentNullException("source");
using(var en = source.GetEnumerator())
{
if(!en.MoveNext())
throw new InvalidOperationException("Empty sequence");
T ret = en.Current;
RunThroughEnumerator(en);
return ret;
}
}
private static void RunThroughEnumerator<T>(IEnumerator<T> en)
{
Debug.Assert(en != null);
while(en.MoveNext());
}
En lo anterior, hay tres enfoques diferentes para los parámetros nulos. El primero lo acepta como permisible (simplemente no hace nada). El segundo lanza una excepción para que el código de llamada se maneje (o no, lo que generará un mensaje de error). El tercero asume que no puede suceder, y afirma que es así.
En el primer caso, no hay problema.
En el segundo caso, hay un problema con el código de llamada: no debería haber llamado a GetFirstAndConsume
con null, por lo que recibe una excepción.
En el tercer caso, hay un problema con este código, porque ya debería haberse comprobado que en != null
antes de llamarlo, para que no sea cierto es un error. O, en otras palabras, debería ser un código que, en teoría, podría optimizarse para Debug.Assert(true)
, sicne en != null
Null siempre debe ser true
!
Use aserciones para verificar las suposiciones del desarrollador y las excepciones para verificar las suposiciones ambientales.
Use aserciones solo en los casos en que desee que se elimine la comprobación para las versiones de lanzamiento. Recuerde, sus afirmaciones no se activarán si no compila en el modo de depuración.
Teniendo en cuenta su ejemplo de comprobación de nulo, si se trata de una API solo interna, es posible que use una aserción. Si está en una API pública, definitivamente usaría la comprobación y el lanzamiento explícitos.
En breve
Asserts
se utilizan para guardias y para verificar las restricciones de Diseño por Contrato, a saber:
-
Asserts
deben ser solo para versiones de depuración y no de producción. Las compilaciones generalmente ignoran las aserciones en las versiones de lanzamiento. -
Asserts
pueden buscar errores / condiciones inesperadas que ESTÁN bajo el control de su sistema -
Asserts
NO son un mecanismo para la validación de primera línea de las aportaciones del usuario o las reglas comerciales -
Asserts
no deben usarse para detectar condiciones ambientales inesperadas (que están fuera del control del código), por ejemplo, sin memoria, falla de la red, falla de la base de datos, etc. Aunque son raras, estas condiciones son de esperar (y su código de aplicación no puede solucionar problemas) como fallo de hardware o agotamiento de recursos). Por lo general, se lanzarán excepciones: su aplicación puede tomar medidas correctivas (por ejemplo, volver a intentar una base de datos o una operación de red, intentar liberar memoria en caché), o abortar con gracia si no se puede manejar la excepción. - Una aserción fallida debería ser fatal para su sistema, es decir, a diferencia de una excepción, no intente capturar ni manejar
Asserts
fallidas: su código funciona en un territorio inesperado. Los seguimientos de pila y los volcados de volcado se pueden usar para determinar qué salió mal.
Las afirmaciones tienen un enorme beneficio:
- Para ayudar a encontrar la validación faltante de las entradas del usuario, o errores ascendentes en el código de nivel superior.
- Las afirmaciones en la base del código transmiten claramente las suposiciones hechas en el código al lector
- Assert se verificará en tiempo de ejecución en las versiones de
Debug
. - Una vez que se haya probado exhaustivamente el código, la reconstrucción del código como Versión eliminará la sobrecarga de rendimiento de la verificación de la suposición (pero con el beneficio de que una compilación de Depuración posterior siempre revertirá los cheques, si es necesario).
... Mas detalle
Debug.Assert
expresa una condición que se ha asumido sobre el estado por el resto del bloque de código dentro del control del programa. Esto puede incluir el estado de los parámetros proporcionados, el estado de los miembros de una instancia de clase o que el retorno de una llamada de método se encuentre en su rango contratado / diseñado. Normalmente, las afirmaciones deben bloquear el subproceso / proceso / programa con toda la información necesaria (Stack Trace, Crash Dump, etc.), ya que indican la presencia de un error o condición no considerada que no ha sido diseñada para (es decir, no intente atrapar o manejar fallos de afirmación), con una posible excepción de cuando una afirmación en sí misma podría causar más daño que el error (por ejemplo, los controladores de tráfico aéreo no querrían un YSOD cuando un avión se vuelve submarino, aunque es discutible si se debe implementar una versión de depuración para producción ...)
¿Cuándo debes usar Asserts?
- En cualquier punto de un sistema, API de biblioteca o servicio donde se asume que las entradas a una función o estado de una clase son válidas (por ejemplo, cuando la validación ya se realizó en las entradas del usuario en el nivel de presentación de un sistema, el negocio y las clases de niveles de datos generalmente asumen que ya se han realizado las comprobaciones nulas, las comprobaciones de rango, las comprobaciones de longitud de cadena, etc. en la entrada). - Las comprobaciones de Assert
comunes incluyen cuando una suposición no válida daría lugar a una falta de referencia a un objeto nulo, a un divisor de cero, a un desbordamiento aritmético numérico o de fecha, y a un comportamiento general fuera de banda / no diseñado para comportamiento edad, sería prudente Assert
que la edad es en realidad entre 0 y 125 o así, los valores de -100 y 10 ^ 10 no fueron diseñados para).
Contratos de Código .Net
En la pila .Net, los contratos de código se pueden usar como complemento o como alternativa al uso de Debug.Assert
. Los contratos de código pueden formalizar aún más la verificación del estado y pueden ayudar a detectar violaciones de suposiciones en el momento de la compilación (o poco después, si se ejecutan como una verificación de antecedentes en un IDE).
Los cheques de diseño por contrato (DBC) disponibles incluyen:
-
Contract.Requires
- Condiciones previas contratadas -
Contract.Ensures
- Condiciones Postcontratadas -
Invariant
: expresa una suposición sobre el estado de un objeto en todos los puntos de su vida útil. -
Contract.Assumes
: pacifica el verificador estático cuando se realiza una llamada a métodos decorados sin contrato.
No los usaría en código de producción. Lanzar excepciones, captura y registro.
También debe tener cuidado en asp.net, ya que una aserción puede aparecer en la consola y congelar la (s) solicitud (es).
No sé cómo está en C # y .NET, pero en C, assert () solo funcionará si se compila con -DDEBUG; el usuario final nunca verá un assert () si se compila sin. Es solo para desarrolladores. Lo uso muy a menudo, a veces es más fácil rastrear errores.