Checked y Unchecked en cálculos numéricos con C#
Introducción
En esta entrada y a colación de una breve pero interesante discusión en Twitter acerca del uso de int.MaxValue, se me pasó por la cabeza hacer esta entrada que profundiza un poco más de lo que la propia discusión sobre int.MaxValue podría sugerir, y es que aprovechando la instrucción comentada, me acordé de algunas particularidades en C# que muchas veces pasan desapercibidas y que quizás convenga mencionar o recordar.
Checked vs Unchecked
En C# podemos ejecutar instrucciones en lo que se denomina contexto comprobado (checked) o contexto no comprobado (unchecked).
Por defecto, de acuerdo al compilador de C#, éste ejecuta sus instrucciones aritméticas en un contexto no comprobado (unchecked).
¿Qué significa o qué implicaciones tiene esto para nuestros programas?.
Básicamente y para entenderlo de una forma más adecuada, pensemos en las siguientes instrucciones de código:
1: int value1 = int.MaxValue;
2: int value2 = value1 + 1;
3: int value3 = value2 - 1;
4: int value4 = value1 * value1;
5: int value5 = value2 * value2;
6: MessageBox.Show(value1.ToString() +
7: Environment.NewLine +
8: value2.ToString() +
9: Environment.NewLine +
10: value3.ToString() +
11: Environment.NewLine +
12: value4.ToString() +
13: Environment.NewLine +
14: value5.ToString());
¿Qué salida esperaremos obtener aquí?.
La variable value1 arrojará un resultado igual a 2147483647.
La variable value2 arrojará un resultado igual a -2147483648
La variable value3 arrojará un resultado igual a 2147483647
La variable value4 arrojará un resultado igual a 1
La variable value5 arrojará un resultado igual a 0
Lo normal quizás hubiera sido obtener una excepción, pero al estar ejecutando las instrucciones dentro de un contexto no comprobado (por defecto se ejecutan dentro de este tipo de contexto tal y como he comentado anteriormente), el cálculo da la vuelta.
Pero antes de continuar, expliquemos bien cómo es posible que el cálculo de la vuelta.
Unchecked y los límites de cálculo
La variable value1 obtiene el valor máximo de un tipo de dato int.
La variable value2 sin embargo, agrega 1 al valor que tiene la variable value1 y que es el máximo valor posible de un tipo int.
Como value2 es también un int, estaríamos desbordando el tipo de dato int, sin embargo, al ejecutarse dentro de un contexto no comprobado, el valor simplemente se pasa de rango.
El tipo int tiene un valor que oscila de –2147483648 a 2147483647.
La variable value3 demuestra el mismo comportamiento pero al contrario, de –2147483648 que es el valor de value2 pasa a 2147483647.
Ahora bien… ¿cómo es esto posible?.
Los cálculos aritméticos con unchecked
Vayamos entonces a los cálculos aritméticos para explicar este comportamiento con más precisión.
¿Cuál es el valor binario de 2147483647?.
2147483647 queda representado por 0x7FFFFFFF, o lo que es lo mismo en binario, por 01111111 11111111 11111111 11111111.
-2147483648 por su parte, queda representado por 0x80000000, o lo que es lo mismo en binario, por 10000000 00000000 00000000 00000000.
¿Cuál es entonces el cálculo de sumar 1 a 01111111 11111111 11111111 11111111?
10000000 00000000 00000000 00000000, ¿te recuerda a algo?. Efectivamente al valor 2147483648, por lo que C# le da la vuelta, trunca su resultado, y lo pone “al otro lado” (con signo negativo). Recuerda que el último bit se reserva para el signo (int es signed, no unsigned).
Ahora bien, ¿y los valores 1 y 0 que resultan de multiplicar 2147483647 por sí mismo y –2147483648 por sí mismo?.
En el caso de calcular 2147483647 * 2147483647 y si ahora mismo no estoy haciendo el cálculo mal (a veces tanto cálculo binario a uno le nubla la vista), el resultado obtenido sería:
00111111 11111111 11111111 11111111 00000000 00000000 00000000 00000001
Teniendo en cuenta que el dato int sólo representa los 32 primeros valores binarios truncando el resto, se quedaría con 00000000 00000000 00000000 00000001 que representa… ¡un 1!.
En el caso de calcular -2147483648 * –2147483648, el cálculo binario representa el siguiente resultado:
10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
Al igual que antes, tomando los 32 primeros valores binarios y truncando el resto, se quedaría con 00000000 00000000 00000000 00000000 que representa… ¡un 0!.
Bien, ya tenemos ahora más claro el porqué del comportamiento inicial, ahora bien… ¿qué o cómo controlar estos desbordamientos en C#?.
Controlando los desbordamientos aritméticos en C#
Sin lugar a dudas, el desbordamiento lo tendremos que controlar nosotros mismos.
Por defecto, en C# las operaciones aritméticas se realizan en contextos no comprobados.
Esto nos devuelve una pregunta… ¿cómo forzar o cambiar a C# para que las operaciones aritméticas se realicen en contextos comprobados?.
Esto se logra utilizando la palabra reservada checked.
La porción de código que vimos al principio de esta entrada no devuelve ninguna excepción y se ejecuta sin problemas. El resultado no obstante no es lo esperado.
A diferencia del código anterior, el siguiente código genera una excepción que podemos controlar dentro de try/catch.
1: try
2: {
3: checked
4: {
5: int value1 = int.MaxValue;
6: int value2 = value1 + 1;
7: int value3 = value2 - 1;
8: int value4 = value1 * value1;
9: int value5 = value2 * value2;
10: MessageBox.Show(value1.ToString() +
11: Environment.NewLine +
12: value2.ToString() +
13: Environment.NewLine +
14: value3.ToString() +
15: Environment.NewLine +
16: value4.ToString() +
17: Environment.NewLine +
18: value5.ToString());
19: }
20: }
21: catch (Exception ex)
22: {
23: MessageBox.Show(ex.Message.ToString());
24: }
El mensaje general que devuelve la excepción es “La operación aritmética ha provocado un desbordamiento.”.
La excepción que en realidad se captura es una excepción de tipo OverflowException.
Aunque esta es una demostración a base de pruebas muy generalistas, de acuerdo a detectar este desbordamiento, deberemos realizar o llevar a cabo las operaciones concretas de acuerdo a los requisitos Software que tengamos.
Otra aproximación forzando al compilador
Aún y así, existe una posibilidad diferente a la planteada para controlar estos desbordamientos.
Supongamos el siguiente código nuevamente:
1: try
2: {
3: int value1 = int.MaxValue;
4: int value2 = value1 + 1;
5: int value3 = value2 - 1;
6: int value4 = value1 * value1;
7: int value5 = value2 * value2;
8: MessageBox.Show(value1.ToString() +
9: Environment.NewLine +
10: value2.ToString() +
11: Environment.NewLine +
12: value3.ToString() +
13: Environment.NewLine +
14: value4.ToString() +
15: Environment.NewLine +
16: value5.ToString());
17: }
18: catch (Exception ex)
19: {
20: MessageBox.Show(ex.Message.ToString());
21: }
Si ejecutamos este código, lo estaremos haciendo en un contexto no comprobado.
Si utilizamos la palabra checked tal y como hemos visto, controlaremos los desbordamientos… pero… ¿qué pasa si realizamos multitud de operaciones aritméticas en nuestras aplicaciones?. ¿Y si olvidamos poner checked en alguna porción de código?. ¿Existe alguna forma de automatizar el comportamiento de checked en toda la aplicación?.
La respuesta es sí.
Para hacer esto, tenemos dos opciones. Una primera indicándoselo al compilador, y una segunda indicándoselo a Visual Studio.
Para indicárselo al compilador, deberíamos utilizar la palabra reservada /checked.
Para indicárselo al proyecto, dentro de Visual Studio 2010, bastará con acceder a las Propiedades del proyecto y en concreto, a la solapa Generar.
En la ventana que aparecerá, seleccionaremos el botón Avanzadas.
Aparecerá entonces la ventana de Configuración de compilación avanzada.
Ahí, seleccionaremos la opción Comprobar el desbordamiento y subdesbordamiento aritmético.
Con esto, nuestra aplicación entera se comportará en un contexto comprobado para operaciones aritméticas sin tener que estar utilizando la palabra reservada checked.
Estaría bien que por defecto, el compilador se comportara como checked en su contexto controlado para cálculos aritméticos, no obstante, es preciso apuntar un aspecto vinculado a todo esto y al posible «porqué» el equipo de .NET tiene el compilador configurado por defecto para contextos no controlados en cálculos aritméticos, y es debido a rendimientos.
Habilitar el contexto controlado afecta de forma directa al rendimiento de nuestras aplicaciones. Es por esto que como suele ocurrir casi siempre, habilitar el contexto controlado o no, dependa. 😉
Referencias
Documentación oficial de MSDN sobre checked y unchecked.
Documentación oficial de MSDN sobre la compilación con /checked.