Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

El operador de fusión y los tipos anulables

C# 2.0 introdujo, casi a última hora, los tipos anulables: una técnica muy conveniente que permite, entre otras cosas, imitar el comportamiento de los valores nulos en casi todos los sistemas de bases de datos basados en SQL (un buen preludio a los cambios que se anuncian para C# 3.0). A pesar de las prisas, estos tipos están correctamente documentados, desde la beta 2 de mayo del 2005. Rebuscando en esta documentación, con el propósito de pensar la implementación de estos tipos en Freya, di con una aplicación (para mí) inesperada de un operador asociado a los tipos anulables: el operador de fusión (coalescence operator). Para entrar en calor, daré un repaso rápido a la técnica, antes de explicar el uso especial que podemos darle al operador mencionado.

LOS TIPOS ANULABLES

  1. El tipo Nullable.

Los nuevos tipos anulables son tipos instanciados a partir del tipo genérico System.Nullable<T>. Este tipo genérico restringe los tipos aceptables para su argumento genérico T exigiendo que sean tipos de valor: es correcto usar Nullable<int>, pero no podemos usar Nullable<string>. No obstante, hay restricciones adicionales sui generis. Por ejemplo, aunque el propio Nullable está declarado como una estructura, un tipo cerrado basado en Nullable no se puede usar como parámetro del propio Nullable. Es decir, no podemos usar tipos como Nullable<Nullable<int>>.

Una instancia de Nullable<T> contiene un valor del tipo T, al que podemos acceder mediante la propiedad Value, y un valor de tipo lógico, al que accedemos mediante otra propiedad: HasValue. Sólo podemos acceder a Value cuando HasValue devuelve true; en caso contrario, la llamada a Value provoca una excepción.

Hay varias formas de interpretar estas reglas de juego. La más natural, como veremos, es ver en los tipos instanciados a partir de Nullable una forma muy útil y sencilla de simular los valores nulos con los que trabaja el lenguaje SQL. Enseguida veremos cómo esa interpretación se refuerza con la ayuda de otras características, como la compatibilidad para la asignación de punteros nulos y los llamados lifted operators.

Si hacemos caso al documento "oficial" de la documentación de C# 2.0 fechado en mayo del 2004 (al que todavía nos remite la MSDN en las navidades del 2005), sí se podrían declarar tipos anulables basados en otros tipos anulables. Es verdad que esto no aportaría nada en expresividad al lenguaje, pero no sé si es bueno haber creado una regla ad hoc para impedirlo... a no ser que se me escape alguna otra consecuencia indeseable. En todo caso, esta restricción se introdujo después de mayo del 2004.

  1. La abreviatura '?' para declarar tipos anulables.

En vez de escribir Nullable<DateTime>, podemos abreviar y escribir DateTime?, utilizando el signo de interrogación a continuación de la referencia de tipo. No se trata exactamente de una novedad revolucionaria, pero reconozcamos que es muy conveniente. En primer lugar, por la claridad que aporta. En segundo, por lo que nos ahorra en tiempo y espacio. Y en tercer lugar, porque hace más digerible la restricción de no declarar tipos como int??

  1. Compatibilidad con el puntero vacío null.

Ya sé que acabo de escribir una herejía, y que cierto ministro amontillado me puede cerrar la página por ello... pero es que hablar de "referencias nulas" es estúpido, y qué demonios, null (o nil, en Freya) es en realidad un puntero vacío (¡qué manía tienen algunos de cambiarle el nombre a las cosas!).

Lo importante ahora es saber que podemos asignar null a una variable de un tipo anulable. Que yo sepa, no existe forma alguna de lograr esta compatibilidad con las reglas del sistema de tipos de C#, por lo que ésta sería una más entre las particularidades del tipo Nullable. Puede que en otros sistemas de tipos, una regla de esta especie sea más natural. Pongamos como caso el de Eiffel: en este lenguaje, el tipo object se llama ANY, y de él descienden por herencia las restantes clases. Pero existe otro tipo especial: VOID, que teóricamente tiene a todas las otras clases como ancestros (¡Eiffel soporta herencia múltiple!). El puntero nulo de Eiffel pertenece precisamente a esta clase especial. Si existiese un tipo parecido en el CLR, sería posible definir un operador de conversión implícita, del tipo VOID a cada una de las clases instanciadas a partir de Nullable.

Por supuesto, esta es una disgresión teórica. Desde el punto de vista práctico, lo importante es que podemos asignar null a cualquier variable o parámetro de tipo anulable.

  1. Lifted operators.

Renuncio a traducir la frase. Está claro que lift quiere decir elevar, pero en este contexto es más probable que signifique promover. En todo caso, se trata de la semántica de los operadores aplicables al tipo base de un tipo anulable, que como es natural, cambia cuando se aplica a tipos anulables. Tomemos el siguiente fragmento de código en C# 2.0:

int? x = 1;
int? y = null;
int? z = x + y;

En la primera instrucción, asignamos un valor entero a una variable de tipo anulable. Hay una conversión implícita de un valor int al tipo int?. La segunda instrucción es posible gracias a la compatibilidad antes mencionada con el puntero nulo. En este caso, la propiedad HasValue aplicada a la variable y devolvería false. Si sumamos ambas variables, ¿qué es lo que debería contener la variable z? Si interpretamos los tipos anulables como si se tratase de columnas de una base de datos, cualquier operación que incluya al menos un valor nulo, debe dar nulo como resultado (la excepción son los operadores and y or). Pues bien, esto mismo es lo que ocurre con los tipos anulables. La mencionada suma se debe implementar de forma parecida a la siguiente:

if (x.HasValue && y.HasValue)
    z = x.Value + y.Value;
else
    z = null;

EL OPERADOR DE FUSIÓN

Ya podemos hablar del operador de fusión. Este es un operador binario, que se representa con el símbolo ??. Sea x una expresión perteneciente a un tipo anulable T?, y sea y una expresión de tipo T:

x ?? y

Entonces, la expresión anterior se evalúa del siguiente modo, y su tipo resultado es T:

x.HasValue ? x.Value : y

Si conoce SQL Server, se dará cuenta de que el operador de fusión hace algo muy parecido a la función coalesce de Transact SQL. He simplificado un poco asumiendo que el tipo de y es el tipo base del tipo anulable al que pertenece el valor de x. Hay unas cuantas variantes para este aspecto, pero no me interesan ahora.

Lo verdaderamente interesante se descubre al leer la letra pequeña de la descripción del operador. Ya hemos visto que el primer operando puede ser un tipo anulable. ¡Pero también se permite que el primer operando pertenezca a un tipo de referencia! Veamos un ejemplo:

if (interfaceList != null)
    this.interfaces = interfaceList;
else
    this.interfaces = new TypeSymbol[0];

El fragmento anterior se repite con frecuencia en el código del compilador de Freya (y en casi cualquier aplicación). En él, hacemos referencia a un parámetro interfaceList y a un campo interfaces, y ambos son vectores (arrays). Para C# no es igual un puntero vacío a un vector que un vector de cero elementos. Por lo tanto, y con el noble objetivo de simplificar el código que más adelante trabajará con el campo interfaces, al inicializar este campo comprobamos si alguien intenta asignarle un puntero vacío, y lo corregimos en el acto.

Teniendo en cuenta el elevado precio de la tinta y el tóner de impresoras, creo que encontrará justificada mi manía de abreviar este tipo de comprobaciones. Con la ayuda del tradicional operador condicional de C#, podemos reescribir el fragmento de esta manera:

this.interfaces = interfaceList != null ?
    interfaceList : new TypeSymbol[0];

¿Una tontería? Bueno, también estamos ahorrando papel. No es que me preocupe excesivamente lo de la deforestación, sino que el precio del papel también está por las nubes. Por estos motivos, comprenderá que haya dado un salto de alegría al descubrir que podía seguir con la reducción de código:

this.interfaces = interfaceList ?? new TypeSymbol[0];

Esto se puede leer de la siguiente manera:

Asigna el valor del parámetro interfaceList en el campo interfaces...
pero si éste es nulo, asigna un nuevo vector vacío.

Reconozco que no siempre me guían motivos "nobles" o siquiera presentables... pero debe comprender mi hastío por tener que trabajar más de la mitad del año para pagar los impuestos con los que mantengo a una panda de gobernantes impresentables, para que luego me digan qué está bien y qué está mal, o qué es verdadero y qué es falso. En todo caso, espero que, al menos, le haya servido para comprender mejor todo este asunto de los tipos anulables y el operador de fusión.