Pregunta

He sido ingeniero de software profesional durante aproximadamente un año, después de graduarme con un título de CS. Conozco las afirmaciones durante un tiempo en C ++ y C, pero no tenía idea de que existían en C # y .NET hasta hace poco.

Nuestro código de producción no contiene ningún tipo de afirmación y mi pregunta es esta ...

¿Debo comenzar a usar Asserts en nuestro código de producción? Y si es así, ¿cuándo es su uso más apropiado? ¿Tendría más sentido hacer

Debug.Assert(val != null);

o

if ( val == null )
    throw new exception();
¿Fue útil?

Solución

En Depuración de aplicaciones Microsoft .NET 2.0 John Robbins tiene una gran sección sobre afirmaciones Sus puntos principales son:

  1. Afirmar generosamente. Nunca puedes tener demasiadas afirmaciones.
  2. Las afirmaciones no reemplazan las excepciones. Las excepciones cubren las cosas que exige su código; aserciones cubren las cosas que asume.
  3. Una afirmación bien escrita puede decirle no solo qué sucedió y dónde (como una excepción), sino también por qué.
  4. Un mensaje de excepción a menudo puede ser críptico, lo que requiere 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.
  5. Las afirmaciones se duplican como documentación, indicando a otros desarrolladores de qué suposiciones implícitas depende su código.
  6. 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 seguirlo con este libro. Lo compré para aprender a usar WinDBG y volcar archivos, pero la primera mitad está repleta de consejos para ayudar a evitar errores en primer lugar.

Otros consejos

Ponga Debug.Assert () en todas partes del código donde desee tener controles de cordura para asegurar invariantes. Cuando compila una compilación de lanzamiento (es decir, no hay una constante del compilador DEBUG ), las llamadas a Debug.Assert () se eliminarán para que ganen no afecta el rendimiento.

Debería lanzar excepciones antes de llamar a Debug.Assert () . La afirmación solo se asegura de que todo esté como se esperaba mientras aún se está desarrollando.

De Código completo

  

8 Programación defensiva

     

8.2 Afirmaciones

     

Una aserción es un código que & # 8217; s se usa durante el desarrollo & # 8212; generalmente una rutina   o macro & # 8212; que permite que un programa se verifique a sí mismo mientras se ejecuta. Cuando un   La afirmación es cierta, eso significa que todo está funcionando como se esperaba.   Cuando es falso, eso significa que ha detectado un error inesperado en el   código. Por ejemplo, si el sistema supone que una información del cliente   el archivo 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. Siempre que el número de registros sea menor o igual que   50,000, la afirmación será silenciosa. Si encuentra más de   50,000 registros, sin embargo, en voz alta & # 8220; afirman & # 8221; 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 a más rápidamente   eliminar las suposiciones de interfaz no coincidentes, errores que se arrastran cuando   se modifica el código, y así sucesivamente.

     

Una aserción generalmente toma dos argumentos: una expresión booleana que   describe la suposición de que se supone que es verdad y un mensaje para   mostrar si no es & # 8217; t.

     

(& # 8230;)

     

Normalmente, no desea que los usuarios vean mensajes de afirmación en   Codigo de producción; las afirmaciones son principalmente para uso durante el desarrollo   y mantenimiento. Las afirmaciones normalmente se compilan en el código en   tiempo de desarrollo y compilado del código para producción. Durante   desarrollo, las afirmaciones eliminan suposiciones contradictorias,   condiciones inesperadas, valores incorrectos pasados ??a rutinas, etc.   Durante la producción, se compilan del código para que el   aserciones no degradan el rendimiento del sistema.

FWIW ... Me parece que mis métodos públicos tienden a usar el if () {throw; } para asegurarse de que el método se llama 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 empiezo a llamar a uno de mis propios métodos privados con parámetros incorrectos, entonces he roto mi propia suposición en alguna parte: debería Nunca he entrado en ese estado. En la 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 cualquier persona puede invocar en tiempo de ejecución: todavía necesito imponer restricciones de parámetros lanzando excepciones.

Además, mis métodos privados aún pueden arrojar 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í para asegurarme de que no he roto mis propias suposiciones internas sobre el estado del objeto.

Use afirmaciones para verificar los supuestos del desarrollador y las excepciones para verificar los supuestos ambientales.

Si yo fuera tú, haría:

Debug.Assert(val != null);
if ( val == null )
    throw new exception();

O para evitar verificaciones de condición repetidas

if ( val == null )
{
    Debug.Assert(false,"breakpoint if val== null");
    throw new exception();
}

Si desea Asserts en su código de producción (es decir, versiones de lanzamiento), puede usar Trace.Assert en lugar de Debug.Assert.

Esto, por supuesto, agrega gastos generales 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 Afirmació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,

  • Utilice Debug.Assert generosamente para ayudar a detectar errores en las compilaciones de depuración.

  • Si usa Trace.Assert en modo de interfaz de usuario, probablemente desee eliminar DefaultTraceListener para evitar usuarios desconcertantes.

  • Si la condición que está probando es algo que su aplicación no puede manejar, probablemente sea mejor lanzar una excepción, para garantizar que la ejecución no continúe. Tenga en cuenta que un usuario puede elegir ignorar una aserción.

Las afirmaciones se utilizan para detectar el error del programador (su), 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 pueda llamar un usuario de la API. 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 se supone que no debe hacerlo.

Normalmente prefiero las excepciones a las afirmaciones cuando no estoy seguro.

Casi nunca en mi libro. En la gran mayoría de las ocasiones, si desea verificar si todo está sano, tírelo si no lo está.

Lo que no me gusta es el hecho de que hace que una compilación de depuración sea funcionalmente diferente a una compilación de lanzamiento. Si una afirmación de depuración falla pero la funcionalidad funciona en la versión, ¿cómo tiene sentido eso? Es aún mejor cuando el afirmador ha dejado la compañía por mucho tiempo y nadie conoce esa parte del código. Luego tienes que matar algo 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á tirando en primer lugar?

Para mí, esto sugiere usar Debug.Aserta que estás difiriendo el problema a otra persona, trata el problema tú mismo. Si se supone que algo es el caso y no lo es, entonces tirar.

Supongo que posiblemente existan escenarios críticos de rendimiento en los que desee optimizar sus afirmaciones y que sean útiles allí, sin embargo, aún no he encontrado ese escenario.

En resumen

Los

Asserts se utilizan para los guardias y para comprobar las restricciones de Diseño por contrato, a saber:

  • Asserts debe ser solo para versiones de depuración y no de producción. Los compiladores generalmente ignoran las afirmaciones en las versiones de lanzamiento.
  • Asserts puede verificar errores / condiciones inesperadas que ESTÁN bajo el control de su sistema
  • Los
  • Asserts NO son un mecanismo para la validación de primera línea de la entrada del usuario o las reglas comerciales
  • Asserts debería not usarse para detectar condiciones ambientales inesperadas (que están fuera del control del código) p. ej. falta de memoria, falla de la red, falla de la base de datos, etc. Aunque es raro, estas condiciones son de esperar (y el código de su aplicación no puede solucionar problemas como fallas de hardware o agotamiento de recursos). Por lo general, se generarán excepciones: su aplicación puede tomar medidas correctivas (por ejemplo, volver a intentar una operación de base de datos o de red, intentar liberar memoria en caché) o abortar con gracia si la excepción no se puede manejar.
  • Una Afirmación fallida debería ser fatal para su sistema, es decir, a diferencia de una excepción, no intente atrapar o manejar Asserts fallidos - su código está operando en un territorio inesperado. Los seguimientos de pila y los volcados de memoria pueden usarse 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 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 Debug builds.
  • Una vez que el código se ha probado exhaustivamente, la reconstrucción del código como versión eliminará la sobrecarga de rendimiento de verificar la suposición (pero con el beneficio de que una compilación de depuración posterior siempre revertirá las comprobaciones, si es necesario).

... Más detalles

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 al método está en su rango contratado / diseñado. Por lo general, 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 una condición no considerada para la que no se ha diseñado (es decir, no intente capturar o manejar fallas de aserción), con una posible excepción de cuando una aserció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 hace submarino, aunque es discutible si una construcción de depuración debe implementarse en producción ...)

¿Cuándo debería usar Asserts?  - En cualquier punto de un sistema, API de biblioteca o servicio donde las entradas a una función o estado de una clase se suponen válidas (por ejemplo, cuando la validación ya se ha realizado en la entrada del usuario en el nivel de presentación de un sistema, el negocio y las clases de niveles de datos generalmente suponen que ya se han realizado verificaciones nulas, verificaciones de rango, verificaciones de longitud de cadena, etc. en la entrada).  - Las comprobaciones comunes de Assert incluyen cuando una suposición no válida daría como resultado una desreferencia de objeto nulo, un divisor cero, un desbordamiento aritmético numérico o de fecha, y fuera de banda general / no diseñado para el comportamiento (por ejemplo, si es un bit de 32 bits int se usó para modelar la edad de un humano, sería prudente Assert que la edad en realidad está entre 0 y 125 más o menos, los valores de -100 y 10 ^ 10 no fueron diseñados para).

Contratos de código .Net
En la pila .Net, se pueden usar Contratos de código además de, o como alternativa a usar Debug.Assert . Los contratos de código pueden formalizar aún más la verificación estatal y pueden ayudar a detectar violaciones de supuestos en el momento de la compilación (o poco después, si se ejecutan como verificación de antecedentes en un IDE).

Las comprobaciones de diseño por contrato (DBC) disponibles incluyen:

  • Contract.Requires - Condiciones previas contratadas
  • Contract.Ensures - Condiciones posteriores contratadas
  • Invariante : 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 que no son Contract.

De acuerdo con el IDesign Standard , debe

  

Afirma cada suposición. 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 este es su estándar.

Utilice las aserciones solo en los casos en que desee que se elimine la verificación para las versiones de lanzamiento. Recuerde, sus afirmaciones no se activarán si no compila en modo de depuración.

Dado su ejemplo de comprobación de nulo, si está en una API solo interna, podría usar una aserción. Si está en una API pública, definitivamente usaría la comprobación y el lanzamiento explícitos.

Todas las afirmaciones deben ser código que pueda optimizarse para:

Debug.Assert(true);

Porque está comprobando algo que ya asumiste que es cierto. Por ejemplo:

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 permitido (simplemente no hace nada). El segundo arroja una excepción para que el código de llamada lo maneje (o no, lo que resulta en un mensaje de error). El tercero supone 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 GetFirstAndConsume con nulo, por lo que se devuelve 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 que se llamara, por lo que no es 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 siempre debería ser true !

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 Activos pueden proporcionar durante las pruebas automatizadas . Como un simple ejemplo:

Cuando un autor que modifica un llamador de nivel superior cree que ha ampliado el alcance del código para manejar escenarios adicionales, idealmente (!) escribirán pruebas unitarias para cubrir esta nueva condición. Entonces puede ser que el código totalmente integrado parece funcionar bien.

Sin embargo, en realidad se ha introducido una falla sutil, pero no se detectó en los resultados de la prueba. La persona que llama se ha vuelto no determinista en este caso, y solo sucede para proporcionar el resultado esperado. O tal vez ha arrojado un error de redondeo que pasó desapercibido. O provocó un error que fue compensado igualmente en otros lugares. O no solo se le otorga el acceso solicitado, sino también privilegios adicionales que no se deben otorgar. Etc.

En este punto, las declaraciones Debug.Assert () contenidas en el destinatario junto con el nuevo caso (o caso límite) conducido por las pruebas unitarias pueden proporcionar una notificación invaluable durante la prueba de que los supuestos del autor original han sido invalidados, y el el código no debe publicarse sin una revisión adicional. Las afirmaciones con pruebas unitarias son los socios perfectos.

2) Además, algunas pruebas son simples 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 a 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. Debug.Assert () mostrará inmediatamente cuando el objeto se haya adjuntado a un punto de entrada inseguro.

3) Luego, 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:

Suponga que es un dispositivo embebido en tiempo real. Lanzar excepciones y reiniciar cuando encuentra un paquete mal formado es contraproducente. En cambio, el dispositivo puede beneficiarse de la operación de mejor esfuerzo, incluso hasta el punto de generar ruido en su salida. Tampoco puede tener una interfaz humana, un dispositivo de registro, o incluso ser físicamente accesible por humanos cuando se implementa en modo de liberación, y la mejor manera de obtener conocimiento de los errores es evaluando la misma salida. 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 el destinatario 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 hecho para hacerlo confiable. Por lo tanto, es común a Exception para parámetros inesperados de los llamantes, pero Assert para resultados inesperados de los llamados. Por ejemplo:

Si una operación principal de String.Find indica que devolverá un -1 cuando no se encuentran los criterios de búsqueda, puede realizar una operación de forma segura en lugar de Tres. Sin embargo, si en realidad 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 para un valor -1 , y que no sea razonable en la mayoría de los entornos de lanzamiento llenar su código con pruebas que garanticen que las bibliotecas principales funcionen como se espera. En este caso, las afirmaciones son ideales.

Cita tomada de El programador pragmático: de oficial a maestro

  

Deje las afirmaciones activadas

     

Existe un malentendido común sobre las afirmaciones, promulgado por   Las personas que escriben compiladores y entornos de lenguaje. Va   algo como esto:

     

Las aserciones agregan algo de sobrecarga al código. Porque revisan las cosas   eso nunca debería 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   es necesario y debe apagarse para que el código se ejecute más rápido.   Las afirmaciones son una instalación de depuración.

     

Aquí hay dos supuestos evidentemente incorrectos. Primero, suponen que   las pruebas encuentran todos los errores. En realidad, para cualquier programa complejo usted   es poco probable que prueben incluso un porcentaje minúsculo de las permutaciones   su código se pondrá a prueba (ver Pruebas despiadadas).

     

Segundo, los optimistas están olvidando que su programa se ejecuta en un   mundo peligroso Durante las pruebas, las ratas probablemente no roerán un   cable de comunicaciones, alguien que juega un juego no agotará la memoria, y   los archivos de registro no llenarán el disco duro. Estas cosas pueden pasar cuando   Su programa se ejecuta en un entorno de producción. Tu primera linea de   defensa está buscando posibles errores, y su segundo está usando   afirmaciones para tratar de detectar aquellos que te has perdido.

     

Desactivar las afirmaciones cuando entrega un programa a producción es   como cruzar un cable alto sin red porque una vez lo hiciste   en la práctica . Hay un valor dramático, pero es difícil obtener vida   seguro.

     

Incluso si tiene problemas de rendimiento, apague solo aquellos   afirmaciones que realmente te golpearon .

Siempre debe usar el segundo enfoque (lanzar excepciones).

Además, si está en producción (y tiene una versión de compilación), 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 puede costar miles de dólares).

Debería usar Debug.Assert para probar errores lógicos en sus programas. El cumplidor solo puede informarle de los errores de sintaxis. Por lo tanto, definitivamente debe usar declaraciones de afirmación para probar errores lógicos. Como por ejemplo, probar un programa que vende autos que solo los BMW que son azules deberían obtener un descuento del 15%. El cumplidor podría no decirle nada acerca de si su programa es lógicamente correcto al realizar esto, pero una declaración de afirmación podría hacerlo.

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 afirmaciones. Uno es como un acceso directo de desarrollador temporal para "Esto realmente no debería suceder, así que si me lo hace saber para que yo pueda decidir qué hacer", algo así como un punto de interrupción condicional, en los casos en que su programa pueda continuar. El otro, es una forma de poner suposiciones sobre estados de programa válidos en su código.

En el primer caso, las aserciones 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 desea dejarlos o si olvida eliminarlos, no hay problema, ya que no tendrán ninguna consecuencia en las compilaciones de lanzamiento.

Pero en el segundo caso, las aserciones son parte del código. Ellos, bueno, afirman que sus suposiciones son ciertas, y también las documentan. En ese caso, realmente quieres dejarlos en el código. Si el programa está en un estado no válido, no se debe permitir que continúe. Si no pudiera permitirse el éxito en el rendimiento, no estaría usando C #. Por un lado, puede ser útil poder adjuntar un depurador si sucede. Por otro lado, no desea que el seguimiento de la pila aparezca en sus usuarios y quizás lo más importante es que no desea que puedan ignorarlo. Además, si está en un servicio, siempre se ignorará. Por lo tanto, en producción, el comportamiento correcto sería lanzar una Excepción y usar el manejo normal de excepciones de su programa, lo que podría mostrarle al usuario un buen mensaje 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 oyentes 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 siguiente, que genera una excepción y lo activa 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>

No sé cómo está en C # y .NET, pero en C afirmará () solo funcionará si se compila con -DDEBUG; el usuario final nunca verá una afirmación () si se compila sin él. Es solo para desarrolladores. Lo uso con mucha frecuencia, a veces es más fácil rastrear errores.

No los usaría en el código de producción. Lanzar excepciones, capturar y registrar.

También es necesario tener cuidado en asp.net, ya que una afirmación puede aparecer en la consola y congelar las solicitudes.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top