En programación informática , específicamente cuando se utiliza el paradigma de programación imperativa , una aserción es un predicado (una función de valor booleano sobre el espacio de estados , generalmente expresada como una proposición lógica utilizando las variables de un programa) conectado a un punto en el programa, que siempre debe evaluarse como verdadero en ese punto en la ejecución del código. Las aserciones pueden ayudar a un programador a leer el código, ayudar a un compilador a compilarlo o ayudar al programa a detectar sus propios defectos.
En el caso de estos últimos, algunos programas comprueban las afirmaciones evaluando el predicado a medida que se ejecutan. Luego, si no es cierto (error de afirmación), el programa se considera a sí mismo como defectuoso y, por lo general, se bloquea deliberadamente o lanza una excepción de error de afirmación .
El siguiente código contiene dos afirmaciones, x > 0
y x > 1
, y de hecho son verdaderas en los puntos indicados durante la ejecución:
x = 1 ; afirmar x > 0 ; x ++ ; afirmar x > 1 ;
Los programadores pueden usar aserciones para ayudar a especificar programas y razonar sobre la corrección de los mismos. Por ejemplo, una condición previa (una aserción colocada al principio de una sección de código) determina el conjunto de estados bajo los cuales el programador espera que se ejecute el código. Una condición posterior (colocada al final) describe el estado esperado al final de la ejecución. Por ejemplo: x > 0 { x++ } x > 1
.
El ejemplo anterior utiliza la notación para incluir afirmaciones que utilizó CAR Hoare en su artículo de 1969. [1] Esa notación no se puede utilizar en los lenguajes de programación convencionales existentes. Sin embargo, los programadores pueden incluir afirmaciones no verificadas utilizando la función de comentarios de su lenguaje de programación. Por ejemplo, en C++ :
x = 5 ; x = x + 1 ; // {x > 1}
Las llaves incluidas en el comentario ayudan a distinguir este uso de un comentario de otros usos.
Las bibliotecas también pueden proporcionar funciones de aserción. Por ejemplo, en C, utilizando glibc con soporte C99:
#include <afirmación.h> int f ( void ) { int x = 5 ; x = x + 1 ; afirmar ( x > 1 ); }
Varios lenguajes de programación modernos incluyen aserciones comprobadas, es decir, declaraciones que se comprueban en tiempo de ejecución o, a veces, de forma estática. Si una aserción se evalúa como falsa en tiempo de ejecución, se produce un error de aserción, que normalmente hace que se cancele la ejecución. Esto llama la atención sobre la ubicación en la que se detecta la inconsistencia lógica y puede ser preferible al comportamiento que se produciría de otro modo.
El uso de afirmaciones ayuda al programador a diseñar, desarrollar y razonar sobre un programa.
En lenguajes como Eiffel , las afirmaciones forman parte del proceso de diseño; otros lenguajes, como C y Java , las utilizan solo para comprobar suposiciones en tiempo de ejecución . En ambos casos, se puede comprobar su validez en tiempo de ejecución, pero normalmente también se pueden suprimir.
Las afirmaciones pueden funcionar como una forma de documentación: pueden describir el estado que el código espera encontrar antes de ejecutarse (sus precondiciones ) y el estado en el que el código espera que resulte cuando termine de ejecutarse ( poscondiciones ); también pueden especificar invariantes de una clase . Eiffel integra dichas afirmaciones en el lenguaje y las extrae automáticamente para documentar la clase. Esto forma una parte importante del método de diseño por contrato .
Este enfoque también es útil en lenguajes que no lo admiten explícitamente: la ventaja de utilizar declaraciones de aserción en lugar de aserciones en comentarios es que el programa puede comprobar las aserciones cada vez que se ejecuta; si la aserción ya no se cumple, se puede informar un error. Esto evita que el código se desincronice con las aserciones.
Se puede utilizar una aserción para verificar que una suposición realizada por el programador durante la implementación del programa sigue siendo válida cuando se ejecuta el programa. Por ejemplo, considere el siguiente código Java :
int total = countNumberOfUsers (); if ( total % 2 == 0 ) { // el total es par } else { // el total es impar y no negativo assert total % 2 == 1 ; }
En Java , %
es el operador de resto ( módulo ) y, en Java, si su primer operando es negativo, el resultado también puede ser negativo (a diferencia del módulo utilizado en matemáticas). Aquí, el programador ha asumido que total
no es negativo, por lo que el resto de una división con 2 siempre será 0 o 1. La afirmación hace explícita esta suposición: si countNumberOfUsers
devuelve un valor negativo, el programa puede tener un error.
Una ventaja importante de esta técnica es que, cuando se produce un error, se detecta de inmediato y directamente, en lugar de más tarde, a través de efectos que suelen pasar desapercibidos. Dado que un error de aserción suele informar la ubicación del código, a menudo se puede localizar el error sin necesidad de realizar más depuraciones.
Las aserciones también se colocan a veces en puntos a los que no se supone que llegue la ejecución. Por ejemplo, las aserciones se pueden colocar en la default
cláusula de la switch
declaración en lenguajes como C , C++ y Java . Cualquier caso que el programador no maneje intencionalmente generará un error y el programa se interrumpirá en lugar de continuar silenciosamente en un estado erróneo. En D, una aserción de este tipo se agrega automáticamente cuando una switch
declaración no contiene una default
cláusula.
En Java , las aserciones han sido parte del lenguaje desde la versión 1.4. Los errores de aserción dan como resultado la emisión de un AssertionError
cuando el programa se ejecuta con los indicadores adecuados, sin los cuales las declaraciones de aserción se ignoran. En C , se agregan mediante el encabezado estándar assert.h
que se define como una macro que señala un error en caso de falla, generalmente terminando el programa. En C++ , tanto los encabezados como proporcionan la macro. assert (assertion)
assert.h
cassert
assert
El peligro de las afirmaciones es que pueden causar efectos secundarios, ya sea modificando los datos de la memoria o modificando los tiempos de los subprocesos. Las afirmaciones deben implementarse con cuidado para que no provoquen efectos secundarios en el código del programa.
Las construcciones de afirmación en un lenguaje permiten un fácil desarrollo basado en pruebas (TDD) sin el uso de una biblioteca de terceros.
Durante el ciclo de desarrollo , el programador normalmente ejecutará el programa con las aserciones habilitadas. Cuando se produce un error de aserción, el programador recibe una notificación inmediata del problema. Muchas implementaciones de aserciones también detendrán la ejecución del programa: esto es útil, ya que si el programa continúa ejecutándose después de que se produjo una violación de aserción, podría corromper su estado y hacer que la causa del problema sea más difícil de localizar. Utilizando la información proporcionada por el error de aserción (como la ubicación del error y quizás un seguimiento de la pila , o incluso el estado completo del programa si el entorno admite volcados de memoria o si el programa se ejecuta en un depurador ), el programador normalmente puede solucionar el problema. Por lo tanto, las aserciones proporcionan una herramienta muy poderosa para la depuración.
Cuando se implementa un programa en producción , las aserciones suelen estar desactivadas para evitar cualquier sobrecarga o efectos secundarios que puedan tener. En algunos casos, las aserciones están completamente ausentes del código implementado, como en C/C++, las aserciones a través de macros. En otros casos, como Java, las aserciones están presentes en el código implementado y se pueden activar en el campo para la depuración. [2]
Las afirmaciones también se pueden utilizar para prometerle al compilador que una determinada condición de borde no es realmente alcanzable, lo que permite ciertas optimizaciones que de otro modo no serían posibles. En este caso, deshabilitar las afirmaciones podría reducir el rendimiento.
Las afirmaciones que se comprueban en tiempo de compilación se denominan afirmaciones estáticas.
Las aserciones estáticas son particularmente útiles en la metaprogramación de plantillas en tiempo de compilación , pero también se pueden usar en lenguajes de bajo nivel como C introduciendo código ilegal si (y solo si) la aserción falla. C11 y C++11 admiten aserciones estáticas directamente a través de static_assert
. En versiones anteriores de C, una aserción estática se puede implementar, por ejemplo, de la siguiente manera:
#define SASSERT(pred) switch(0){caso 0:caso pred:;}SASSERT ( CONDICIÓN BOOLEANA );
Si la (BOOLEAN CONDITION)
parte se evalúa como falsa, el código anterior no se compilará porque el compilador no permitirá dos etiquetas de caso con la misma constante. La expresión booleana debe ser un valor constante en tiempo de compilación, por ejemplo, sería una expresión válida en ese contexto. Esta construcción no funciona en el ámbito de archivo (es decir, no dentro de una función), por lo que debe estar dentro de una función.(sizeof(int)==4)
Otra forma popular [3] de implementar afirmaciones en C es:
static char const static_assertion [ ( CONDICIÓN BOOLEANA ) ? 1 : -1 ] = { '!' };
Si la (BOOLEAN CONDITION)
parte se evalúa como falsa, el código anterior no se compilará porque las matrices no pueden tener una longitud negativa. Si, de hecho, el compilador permite una longitud negativa, el byte de inicialización (la '!'
parte) debería hacer que incluso compiladores tan indulgentes se quejen. La expresión booleana debe ser un valor constante en tiempo de compilación; por ejemplo, (sizeof(int) == 4)
sería una expresión válida en ese contexto.
Ambos métodos requieren un método para construir nombres únicos. Los compiladores modernos admiten una __COUNTER__
definición de preprocesador que facilita la construcción de nombres únicos, al devolver números que aumentan de manera monótona para cada unidad de compilación. [4]
D proporciona afirmaciones estáticas mediante el uso de static assert
. [5]
La mayoría de los lenguajes permiten habilitar o deshabilitar las afirmaciones de forma global y, a veces, de forma independiente. Las afirmaciones suelen habilitarse durante el desarrollo y deshabilitarse durante las pruebas finales y en el lanzamiento al cliente. No verificar las afirmaciones evita el costo de evaluar las afirmaciones mientras que (asumiendo que las afirmaciones no tienen efectos secundarios ) sigue produciendo el mismo resultado en condiciones normales. En condiciones anormales, deshabilitar la verificación de afirmaciones puede significar que un programa que se habría abortado seguirá ejecutándose. Esto a veces es preferible.
Algunos lenguajes, incluidos C , YASS y C++ , pueden eliminar completamente las afirmaciones en tiempo de compilación utilizando el preprocesador .
De manera similar, al iniciar el intérprete de Python con "-O" (para "optimizar") como argumento, el generador de código Python no emitirá ningún código de bytes para las afirmaciones. [6]
Java requiere que se pase una opción al motor de ejecución para habilitar las aserciones. Si no existe la opción, las aserciones se pasan por alto, pero siempre permanecen en el código a menos que un compilador JIT las optimice en tiempo de ejecución o las excluya en tiempo de compilación mediante la colocación manual por parte del programador de cada aserción detrás de una if (false)
cláusula.
Los programadores pueden incorporar controles en su código que estén siempre activos eludiendo o manipulando los mecanismos normales de verificación de afirmaciones del lenguaje.
Las aserciones son distintas del manejo rutinario de errores . Las aserciones documentan situaciones lógicamente imposibles y descubren errores de programación: si ocurre lo imposible, entonces algo fundamental está claramente mal con el programa. Esto es distinto del manejo de errores: la mayoría de las condiciones de error son posibles, aunque algunas pueden ser extremadamente improbables de ocurrir en la práctica. El uso de aserciones como un mecanismo de manejo de errores de propósito general es imprudente: las aserciones no permiten la recuperación de errores; un error de aserción normalmente detendrá la ejecución del programa abruptamente; y las aserciones a menudo se deshabilitan en el código de producción. Las aserciones tampoco muestran un mensaje de error fácil de usar.
Considere el siguiente ejemplo de uso de una afirmación para manejar un error:
int * ptr = malloc ( sizeof ( int ) * 10 ); assert ( ptr ); // usa ptr ...
Aquí, el programador es consciente de que malloc
devolverá un NULL
puntero si no se asigna memoria. Esto es posible: el sistema operativo no garantiza que cada llamada a malloc
se realice correctamente. Si se produce un error de falta de memoria, el programa se interrumpirá inmediatamente. Sin la afirmación, el programa seguiría ejecutándose hasta que ptr
se desreferenciara, y posiblemente durante más tiempo, según el hardware específico que se utilice. Mientras las afirmaciones no estén deshabilitadas, se garantiza una salida inmediata. Pero si se desea una falla elegante, el programa tiene que manejar la falla. Por ejemplo, un servidor puede tener varios clientes, o puede contener recursos que no se liberarán de forma limpia, o puede tener cambios no confirmados para escribir en un almacén de datos. En tales casos, es mejor que falle una sola transacción que interrumpirla abruptamente.
Otro error es confiar en los efectos secundarios de las expresiones utilizadas como argumentos de una aserción. Siempre hay que tener en cuenta que las aserciones pueden no ejecutarse en absoluto, ya que su único propósito es verificar que una condición que siempre debería ser verdadera en realidad lo es. En consecuencia, si se considera que el programa está libre de errores y se libera, las aserciones pueden quedar deshabilitadas y ya no se evaluarán.
Consideremos otra versión del ejemplo anterior:
int * ptr ; // La siguiente declaración falla si malloc() devuelve NULL, // pero no se ejecuta en absoluto al compilar con -NDEBUG! assert ( ptr = malloc ( sizeof ( int ) * 10 )); // use ptr: ptr no se inicializa al compilar con -NDEBUG! ...
Esto puede parecer una forma inteligente de asignar el valor de retorno de malloc
to ptr
y verificar si está NULL
en un solo paso, pero la malloc
llamada y la asignación a to ptr
son un efecto secundario de evaluar la expresión que forma la assert
condición. Cuando el NDEBUG
parámetro se pasa al compilador, como cuando se considera que el programa está libre de errores y se libera, la assert()
instrucción se elimina, por lo que malloc()
no se llama, lo que la deja ptr
sin inicializar. Esto podría potencialmente resultar en un error de segmentación o un error de puntero nulo similar mucho más adelante en la ejecución del programa, lo que causa errores que pueden ser esporádicos y/o difíciles de rastrear. Los programadores a veces usan una definición VERIFY(X) similar para aliviar este problema.
Los compiladores modernos pueden emitir una advertencia cuando encuentran el código anterior. [7]
En los informes de 1947 de von Neumann y Goldstine [8] sobre su diseño para la máquina IAS , describieron algoritmos que utilizaban una versión temprana de diagramas de flujo , en los que incluían afirmaciones: "Puede ser cierto que, siempre que C alcance un cierto punto en el diagrama de flujo, una o más variables ligadas necesariamente poseerán ciertos valores especificados, o poseerán ciertas propiedades, o satisfarán ciertas propiedades entre sí. Además, podemos, en ese punto, indicar la validez de estas limitaciones. Por esta razón, denotaremos cada área en la que se afirma la validez de tales limitaciones, mediante un cuadro especial, que llamamos cuadro de afirmación".
El método asertivo para probar la corrección de los programas fue defendido por Alan Turing . En una charla titulada "Checking a Large Routine" (Comprobación de una gran rutina) en Cambridge, el 24 de junio de 1949, Turing sugirió: "¿Cómo se puede comprobar una gran rutina en el sentido de asegurarse de que es correcta? Para que el encargado de la comprobación no tenga que hacer una tarea demasiado difícil, el programador debería hacer una serie de afirmaciones concretas que se puedan comprobar individualmente y de las que se deduzca fácilmente la corrección de todo el programa". [9]