lunes, 1 de septiembre de 2014

Tipos Anulables (Nullable) en C#

Índice

0. Introducción
1. Introducción a los Tipos Nullable
2. La Estructura Nullable<T>
3. Operaciones Sobre Tipos Nullable
3.1 Ejemplo de introducción
3.2 Conversión implícita y explícita
3.3 Boxing y unboxing
4. Elevación o Lifting de Operadores
4.1 Operadores de igualdad: == y !=
4.2 Operadores relacionales: <, <=, >=, >
4.3 Otros operadores
4.4 Combinación de tipos nullable con tipos no-nullable
5. Operadores de Coalescencia
6. Aplicaciones
7. Alternativas
Conclusiones
Glosario
Literatura & Enlaces

0. Introducción

A continuación entraremos a estudiar los tipos anulables (o en inglés, nullable) para los tipos integrados numéricos de la biblioteca base de clases de .NET. De vital importancia comprender y, a través de la práctica afianzar el conocimiento sobre este tipo operación que lograremos por medio del uso implícito o explícito de la estructura Nullable. Cubriremos los detalles de esta última estructura, conversiones implícitas y explícitas, operaciones de envoltura y des-envoltura: boxing y unboxing, uso de operadores lógicos y de comparación, aplicaciones y alternativas al uso de tipos anulables.

1. Introducción a los Tipos Nullable

Hasta este punto ya tenemos un conocimiento teórico y práctico decente sobre el manejo de la palabra clave null sobre tipo de datos por referencia. En breve, podemos recordar, que se trata de la asignación de un valor inexistente en el dominio de valores posibles para un tipo de dato de naturaleza referencial. Sabemos, además, que el intento de uso de esta palabra clave con los tipos numéricos integrados o las estructuras (cfr. Structs) en la biblioteca base de clases de .NET Framework genera un error de compilación:

// Operación válida: Tipo por referencia:
string cadena = null;
// Operación inválida: tipo por valor:
int entero = null;

Esta última instrucción es una operación inválida y generará el error C# CS0037 [2]:

error CS0037: Cannot convert null to `int' because it is a value type

Sin embargo, como es de esperar, la robustez y completitud del lenguaje de programación C# provee al programador una construcción natural e integral para resolver este último caso de error:

// Operación válida - asignación de null a un entero:
int? entero = null;

Console.WriteLine (entero == null); // True

Notemos el uso explícito del operador o símbolo ? para denotar que un tipo por valor ahora cuenta con la posibilidad asignación de null. A esta operación se le conoce como asignación de un tipo anulable.

2. La Estructura Nullable<T>

Podemos reemplazar cualquiera de los tipos por valor o una estructura (integradas o construidas por el programador) de la biblioteca base de clases de .NET por la representación genérica, es decir, T? por su equivalente en .NET System.Nullable<T> [3].

Esta estructura posee las siguientes características:
  • Inmutable
  • Miembros:
    • Propiedades:
      • Value
      • HasValue
    • Métodos:
      • GetValueOrDefault
Como manifiestan en [1], System.Nullable<T>, en esencia, está integrada por:

public struct Nullable<T> where T : struct
{
public T Value { get; }
public bool HasValue { get; }

public T GetValueOrDefault();
public T GetValueOrDefault (T defaultValue);

// Otros miembros
}

Para llevar a cabo una demostración simple y directa de esta estructura, consideremos este fragmento de código en el que asignamos el valor no existente null a una variable de tipo por valor entero (de 32 bits):

int? entero = null;

Console.WriteLine (entero == null); // True

se representa internamente como:

Nullable<int> entero = new Nullable<int>();

Console.WriteLine (!entero.HasValue); // True

La sintaxis declarativa en este caso, la de la primera línea, corresponde con la declaración de tipos genéricos (cfr. Tipos Genéricos). Hemos especificado como tipo particular un entero de 32 bits (Int32) -int-. Cuando esta operación ocurre, el valor asignado a la variable entero es null. Ahora, el uso de la propiedad HasValue podemos comprobar si la variable entero posee un valor válido (es decir, en el dominio de los números enteros de 32 bits: -2.147.483.648 a 2.147.483.647).

Continuando, como veremos más adelante, el intento de obtener el valor de un tipo nullable, precisamente cuando el valor retornado por la propiedad HasValue sea false, la CLR en tiempo de compilación generará la excepción InvalidOperationException (cfr. Ejemplos de Excepciones Comunes).

3. Operaciones sobre Tipos Nullables

Ahora procedamos a entrar en los detalles prácticos con las operaciones permitidas sobre los tipos nullables.

3.1 Ejemplo de introducción

Para empezar un ejemplo de introducción o de calentamiento acerca del uso de tipos nullables en C#.

En la línea 11 usamos el símbolo ? frente al tipo de dato int para permitir la asignación del valor inexistente null a la variable recién declarada numero. Con la instrucción de flujo de control if en la línea 15 validamos si la variable numero contiene un valor dentro del dominio de los enteros de 32 de bits, dado que hemos asignado null a esta variable, el flujo de control pasa al bloque else de las líneas 19-22, per se, se ha de mostrar el mensaje generado por la línea 21.


Avanzando en la descripción, sobre la línea 26 intentamos obtener el valor de la variable numero a través de la propiedad Value y asignarlo a la variable entera nuevoNumero; debido a que numero tiene asignado el valor null, se genera la excepción InvalidOperationException, la cual es atrapada en el bloque catch de las líneas 36-39.


Compilación:


  1. csc /target:exe IntroTiposNullables.cs

Ejecución assembly:


  1. .\IntroTiposNullables.exe

> Prueba de ejecución (ideone.com).

> Prueba de ejecución (local):
Ejecución assembly IntroTiposNullables.exe
Figura 1. Ejecución assembly IntroTiposNullables.exe.

3.2 Conversión implícita y explícita

Contamos con dos formas de conversión a un tipo nullable, y desde nullable a una variable de tipo por valor; o en términos más técnicos, como en [1], la conversión desde T a T? es implícita, y desde T? a T es explícita. Todo esto lo podemos resumir en las siguientes instrucciones en código C#:

int? x = 3; // Conversión implícita
int y = (int)x; // Conversión explícita

En el caso de la conversión explícita la podemos equiparar con la invocación de la propiedad Value sobre un objeto tipo Nullable:

int y = x.Value; // Conversión explícita con la propiedad `Value`

Sin embargo, hay que recordar como en el ejemplo de la sección 3.1 que en el caso de que la variable x sea igual null, entonces se generará la excepción InvalidOperationException.

3.3 Boxing y unboxing

En cuanto a la operación boxing, cuando asignamos, por ejemplo, a una variable tipo object un tipo nullable -T?-, el valor adyacente aparecerá en la memoria heap (cfrBoxing y Unboxing) en su versión natural (es decir, referencial). Además, como nos informan en [1]:
«This optimization is possible because a boxed value is a reference type that can already express null
Para la operación contraria -unboxing-, debemos proceder con el uso del operador as (cfr. Conversión y Promoción de Referencias); en código tendríamos las siguientes expresiones:

object obj = "xCSw";
int? x = obj as int?;
Console.WriteLine ("Valor de `x`: {0}", x.HasValue.ToString());

Debido a que la operación de conversión con as no es satisfactoria, el valor a asignar a x (nullable) es null. Por esa misma razón la salida de la tercera línea de ejemplo, generará en pantalla:

Valor de `x`: False

4. Elevación o Lifting de Operadores

La estructura Nullable<T> no cuenta con versiones sobrecargadas de los operadores relacionales o de comparación. Por ejemplo:

int? numero1 = 11;
int? numero2 = 13;

// Comparación de valores gracias a elevación 
// o lifting de operadores::
bool menorQue = numero1 < numero2;

Console.WriteLine ("Valor de `menorQue`: {0}", menorQue.HasValue.ToString());

El compilador de C# logra realizar esta comparación gracias a una representación sintáctica alternativa, pero de igual sentido semántico:

bool menorQue = (numero1.HasValue && numero2.HasValue) ? (numero1.Value < numero2.Value) : false;

Esta representación hace uso del operador ternario ?: (cfr. Operadores Condicionales). En la primera parte de este operador sobre la expresión anterior validamos que ambas variables -numero1 y numero2- contengan un valor diferente de null. En caso de superarse esta prueba, se realiza una nueva en la segunda parte del operador ternario ?: para comprar los valores de las dos variables; en el caso de pasar la prueba en la variable menorQue se asignará true; en caso contrario false.

Ahora, procedamos a crear un ejemplo para resaltar la semántica del uso de los operadores de igualdad con la propiedad lifting:

En este ejemplo podemos observar cómo el compilador de C# es capaz de efectuar las operaciones de elevación o lifting de operadores de forma automática y transparente para el programador. Aquí es importante volver a resaltar la representación sintáctica alternativa que se discutió al principio de esta sección, es decir, la del uso del operador ternario ?:.

Compilación:


  1. csc /target:exe LiftingOperadores.cs

Ejecución assembly:


  1. .\LiftingOperadores.exe

> Prueba de ejecución (ideone.com).


> Prueba de ejecución (local):
Ejecución assembly LiftingOperadores.exe
Figura2. Ejecución assembly LiftingOperadores.exe.

A continuación se resaltarán varias de las diferencias lógicas en el uso de los operadores probados en esta sección.

4.1 Operadores de igualdad: == y !=

Para los operadores de igualdad, los tipos nullable siguen las las mismas reglas lógicas de los tipos por referencia. Así:

Console.WriteLine ( null == null); // True

Console.WriteLine ((bool?)null == (bool?)null); // True

Otros puntos a considerar:
  • Si uno de los operandos es null, entonces los operandos son desiguales.
  • Si ambos operandos son distintos de null, se procede a comprobar los valores adyacentes.

4.2 Operadores relacionales: <, <=, >=, >

De acuerdo con [1], la comparación de un valor null con otro, e inclusive con un valor concreto, genera false. Este principio también aplica para los tipos nullable:

int? numero1 = 11;
int? numero2 = null;

bool resultado = numero1 < numero2;

En este caso, el uso del operador menor que (<) es equivalente a:

bool resultado = (numero1.HasValue && numero2.HasValue) ? (numero1.Value < numero2.Value) : false;

Ambas expresiones generarán como resultado false.

4.3 Otros operadores

Otros operadores como:
  • +
  • -
  • *
  • /
  • %
  • &
  • |
  • ^
  • <<
  • >>
  • +
  • ++
  • --
  • !
  • ~
dan como resultado null cuando cualquiera de los operandos son null.

Para el caso de la suma la expresión:

int? suma = numero1 + numero2;

es equivalente a:

int? suma = (numero1.HasValue && numero2.HasValue)
? (int?) (numero1.Value + numero2.Value)
: null;

Una nota importante en [1]:
«An exception is when the & and operators are applied to bool?, ...»

4.4 Combinación de tipos nullables con tipos no-nullables

La combinación de tipos nullables con sus opuestos es una operación válida debido a que el compilador de C# lleva a cabo de forma transparente una conversión de T a T?:

int? x = null;
int y = 3;
int? z = x + y; // z es null

Sobre el intento de suma entre x e y se realiza una conversión implícita de y:

int? z = x + (int?)y;

5. Operador de Coalesciencia

El operador de coalescencia (ver definición en [10]?? [9] sigue esta lógica para asignar un valor a una variable nullable:

  • Si el operador del lado izquierdo es distinto de null, asignar este valor a la variable nullable.
  • De lo contrario, asignar el valor del segundo operando (el del lado derecho).
int? numero1 = null;
int numero2 = numero1 ?? 5; // numero2 igual a 5

u otro ejemplo podría ser:

int? x = null, x = 2, z = 3;
Console.WriteLine (x ?? y ?? z); // Asignar el primer valor no nulo

El valor que se mostrará en pantalla en la última sentencia será 1.

6. Aplicaciones

En [1] se manifiesta que un escenario de aplicación de uso de tipos nullable comprende la representación de valores desconocidos en la definición de un tipo de dato. Para aquellos que ya han experimentado con el diseño de bases de datos, la asignación o mapeo de una identidad (o clase) a una tabla (o relación) puede involucrar miembros que no tienen asignado ningún valor:

// Clase mapeada a la tabla Cliente en la base de datos:
public class Cliente
{
public decimal? balance;

// Otros miembros
}

7. Alternativas

Antes de la aparición de C# 2.0, una de las estrategias para asignar valores análogos a la funcionalidad de null para los tipos por valor consistía (consiste, inclusive aún por motivos históricos) la asignación de valores bandera (también llamados: valores centinela, o valores mágicos). Por ejemplo para el método IndexOf de la clase String, existe un valor bandera, -1, para indicar que el carácter pasado como argumento no existe en la cadena original.

Sin embargo, la anterior estrategia no es un patrón de uso generalizado para todos los tipos por valor, por eso la preferencia sobre los nullable. En [1] se describen con mayor detalle esta y otras razones acerca de los problemas que se pueden generar por el uso de valores bandera o centinela:
Razones de los problemas de uso de valores bandera
Figura 3. Razones de los problemas de uso de valores bandera.

8. Artefactos

Conjunto de artefactos producidos a lo largo del desarrollo de este artículo C#:

9. Conclusiones

El conocimiento teórico y práctico de los tipos anulables (o más formalmente nullable) de acuerdo a lo estudiado en este artículo, es fundamental para el modelamiento de clases con miembros consistentes con otros modelos, como podría ser el modelo relacional para base de datos. Además, con la asignación de null a tipos por valor evitamos caer en la trampa del uso de valores centinela o bandera para representar un valor inválido: esta es una forma más intuitiva de reconocer la no existencia de un valor sobre una variable. En el próximo artículo estudiaremos la sobrecarga de operadores.

10. Glosario

  • Anulable
  • Aplicación
  • Base de datos
  • CLR
  • Coalescencia
  • Comparación
  • Modelo relacional
  • Nullable
  • Operador
  • Propiedad
  • Tipo por referencia
  • Tipo por valor

11. Literatura & Enlaces

[1]: C# 5.0 in a Nutshell by Joseph Albahari and Ben Albahari. Copyright 2012 Joseph Albahari and Ben Albahari, 978-1-449-32010-2.
[2]: Compiler Error CS0037 - http://msdn.microsoft.com/en-us/library/6z0y399a(v=vs.90).aspx
[3]: Nullable(T) Structure (System) - http://msdn.microsoft.com/en-us/library/b3h38hb0.aspx
[4]: Tipos Genéricos en C# - Parte 1: Introducción | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2014/05/genericos-en-c-parte-1-introduccion.html
[5]: Excepciones en C# - Parte 5: Ejemplos de Excepciones Comunes | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2014/07/excepciones-en-csharp-parte-5-ejemplos-de-excepciones-comunes.html
[6]: Boxing y Unboxing en C# | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2014/03/boxing-y-unboxing-en-c.html
[7]: Conversión y Promoción de Referencias en C# | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2013/12/conversion-y-promocion-de-referencias.html
[8]: Operadores Condicionales en C# | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2013/08/operadores-condicionales-en-c.html
[9]: ?? Operator (C# Reference) - http://msdn.microsoft.com/en-us/library/ms173224.aspx
[10]: coalescing - definition of coalescing by The Free Dictionary - http://www.thefreedictionary.com/coalescing


J

No hay comentarios:

Publicar un comentario

Envíe sus comentarios, dudas, sugerencias, críticas. Gracias.