lunes, 27 de abril de 2009

INVARIANZA, COVARIANZA & CONTRAVARIANZA

Ultimamente, el mundo de los desarrolladores que utilizan C# está intrigado por el significado de dos palabras inusuales: covarianza y contravarianza.

Hay ahora una buena cantidad de artículos por ahí que intentan explicar los conceptos detrás de esas palabras príncipalmente a través de ejemplos, y porqué estos importan más para la versión 4.0 venidera de C#.

Para mencionar unos pocos artículos sobre el tema:

Puesto que este tópico es nuevo para muchos, y además, y seamos honestos aquí, NO ES TAN fácil de entender a primera vista, he decidido agregar mi granito de arena escribiendo otro artículo más (explicando la forma cómo yo lo entiendo) con la esperanza que eventualmente ayude a otros a comprenderlo de una buena vez.

Ok, suficiente introducción! Empecemos …

Desde un punto de vista de diseño, al querer referirnos a herencia, tendríamos que utilizar las palabras “generalización” y “especificación”. Pero en la práctica o en términos de implementación, usamos la palabra “herencia” en sí misma, más las siguientes dos palabras: “tipos base” y “tipos derivados”.

En lo que sigue, usaré el último grupo de términos a efectos de procurar un mejor entendimiento. Así que empecemos con unos conceptos básicos, ¿les parece?

(I) Lo Básico

En teoría, hablando de tipos por referencia, se establece que un tipo base is mayor a un tipo derivado porque puede contener una instancia de su propio tipo o bien una instancia de un tipo derivado.

Por lo tanto, un tipo derivado es menor que un tipo base pués el primero no puede contener a una instancia del último.

Esto es usualmente indicado como T >= S, siendo T el tipo base y S el tipo derivado. Por ejemplo,

Object >= String

Ahora bien, estamos en presencia de covarianza cuando la referencia a un objeto es declarada como su tipo real o como uno de sus tipos bases (y por “real” me refiero al tipo usado para crear la instancia del mismo, por ejemplo: “new Foo()”).

En otras palabras, un tipo derivado es covariante con su tipo base porque la dirección de asignación (en lo que sigue la llamaré “movimiento”), que va desde un tipo derivado a un tipo base, se permite por tanto que suceda.

Siguiendo este razonamiento, hablamos por ende de contravarianza cuando el “movimiento” se da en el sentido opuesto.

Y este es probablemente el concepto más difícil de visualizar. Esperemos que este ejemplo lo aclare: si tuviéramos un delegado que, digamos, tomara un string como parámetro de entrada y retornara un bool, entonces podríamos pasar un método que tome un object como parámetro de entrada y retornara el mismo tipo, en el ejemplo: un bool. ¿Por qué? Porque si podemos tratar con un tipo derivado dentro del método esperado etonces podremos también tratar específicamente con uno de sus tipos bases (veremos un ejemplo de ello más adelante).

Finalmente, hay un tercer concepto: si no se permite que suceda “ningún movimiento”, nos estamos refiriendo entonces a invarianza.

Por ejemplo, para prevenir conflictos de varianza, arreglos mutables “deberían” ser invariantes sobre el tipo base, siempre. Si estuviéramos creando un arreglo de objectos entonces no se nos debería permitir insertar, digamos, un string en el arreglo (covarianza). Y por ende, si estuviéramos creando un arreglo de strings no se nos debería permitir borrar una instancia de un objecto del mismo (contravarianza).

Ergo, de object >= string, podríamos inferir que object[] >= string[], SOLO SI ambos arreglos fueran inmutables.

Toda la explicación antedicha está en línea con el denominado Principio de Sustitución de Liskov, en especial en el sentido de que la forma en la cual se hace referencia a un objecto en un programa nunca debe alterar ningúna de sus propiedades (“deseables”).

(II) El Problema

En un lenguaje estático como C#, “type-safety” (seguridad de tipos) implica que el compilador es capáz de capturar errores de “transformación de tipos” (casting) en el código fuente en tiempo de compilación.

Como expliqué más arriba, los arreglos mutables deberían ser invariantes para así hacer valer la seguridad de tipos. Pero éste no es el caso de los arreglos en C#, los cuales son covariantes al tipo base. ¿Confundidos? Entonces prosigan con la lectura de los párrafos siguientes.

Esto significa que “un error” en el código fuente que debería ser capturado siempre por el compilador de C# ha sido convertido en una excepción que sólo puede ser detectada en tiempo de ejecución por el CLR!

¿Por qué? Para responder esta pregunta y de aquí en más definamos tres clases que últimamente a muchos parece gustarle utilizarlas en ejemplos de código sobre la materia: Animal, Gato y Perro, siendo Gato y Perro ambos tipos derivados de la misma clase base: Animal.

¿Y bien? Siempre pueden almacenar un Gato en un arreglo de Animales, pero con la covarianza de arreglos el tipo real a almacenar podría ser un arreglo de Perros.

Tomemos como ejemplo el siguiente código:

   1: Animal[] animales = new Perro[10];
   2: animal[1] = new Gato();

¿Qué esperan que pase en el código anterior? Que posiblemente el compilador detecte un error aquí, ¿cierto? ¿Cierto?!

Errado! Créanlo o no, dicho código sólo lanza una excepción en tiempo de ejecución en vez de un error en tiempo de compilación, en cualquiera de las versiones de C#!

Si esto está mal, ¿por qué se permite entonces? El tema es que el compilado sabe que ambos tipos derivados son Animales, y por ende se permiten las conversiones implícitas (implicit casting) para arreglos que declaran esperar Animales, incluso a pesar que la instancia real declara contener un tipo derivado totalmente distinto pero con un mismo tipo base común (en el código de ejemplo más arriba: Perro[10]).

Recuerden que en C# existe conversión implícita (implicit casting) de un tipo derivado hacia su tipo base porque es una operación segura en los tipos, pero lo mismo no aplica en el sentido opuesto. Y por ello, siempre deben usar la conversión de tipo explícita para revertir un tipo base al tipo derivado.

Desafortunadamente, este problema existe en C# desde su versión 1 y permanecerá para la versión 4 (shhhhh! … no lo digan en voz alta, pero es algo relacionado a Java).

Pero vayamos a lo que si va a cambiar …

En la sección anterior hablé de delegados. Pues bien, en C#, los delegados son covariantes para tipos de retorno de referencia y también contravariantes para parámetros de tipos de referencia. ¿Confundidos de nuevo? Sólo sigan leyendo …

Continuando con esta cosa animal, miremos el siguiente ejemplo para covarianza:

   1: public delegate Animal MiDelegado(int i);
   2:  
   3: MiDelegado miDelegado = new MiDelegado(MiMetodo);
   4:  
   5: public Gato MiMetodo(int i) { … }

Si la declaración de un delegado pide un método que espera por un Animal como tipo de retorno, el compilador sabe que un Gato es un Animal y dada la conversión implícita de un tipo derivado hacia su clase base, recibirán un Animal.

Sin embargo, lo opuesto no es cierto. Si el tipo de retorno es un Gato, no pueden pasar un Animal en cambio sin una conversión explícita al tipo Gato. ¿Por qué? Debido al hecho que un objeto Animal podría en realidad haber sido concebido como un Perro!

Ahora vayamos a un ejemplo de contravarianza en los parámetros de los delegados:

   1: public delegate int MiDelegado(Gato myCat);
   2:  
   3: MiDelegado miDelegado = new MiDelegado(MiMetodo);
   4:  
   5: public int MiMetodo(Animal miAnimal) { … }

Si la declaración de un delegado pide un método que espera a un Gato como parámetro, entonces pueden manejar su clase base. ¿Por qué? Debido a que el Gato es un Animal y por herencia Uds. saben como tratar con el como un Animal. Así que todo aquello que pueda ser asignado como un Gato puede pasarse y ser tratado específicamente como un Animal con seguridad de tipos, siempre y cuando el delegado declarado y el método pasado, ambos retornen el mismo tipo (en el ejemplo anterior, un entero).

Entendido. Pero, ¿por qué es ésto contravarianza? Estamos en realidad cambiando el sentido en la dirección de asignación puesto que estamos pasando un método que toma a un Animal como parámetro a un puntero de función (o delegado) que espera a un Gato como argumento.

¿Recuerdan la definición de contravarianza? Aquí estamos pasando un tipo mayor a uno menor. Así que estamos cambiando el sentido en la dirección de asignación.

Como en el ejemplo de la covarianza, lo opuesto no es verdad. Si el delegado espera a un Animal como parámetro, no podrán tratarlo como un Gato en el método pasado, puesto que no hay garantía alguna que el parámetro que se pasó como Animal sea un Gato. Podría ser un Perro, en cambio.

Un gran ejemplo para entender ésto se relaciona con dos delegados que esperan métodos con parámetros de los tipos MouseEventArgs y KeyEventArgs; ambos podrían en cambio hacer referencia a un único método que esperara un parámetro del tipo EventArgs. ¿Lo cuál significa? En tal situación, en ambos casos se espera el mismo comportamiento. Así que , como no les importaría la funcionalidad agregada de las dos clases “derivadas”, podrían tratar con ambos argumentos igualitariamente usando el tipo base en común.

Ok, si esto funciona para delegados en versiones actuales de C#, ¿donde está está el problema a arreglar entonces? Bien, la regla antedicha no es aplicable cuando almacenamos delegados genéricos, los cuales son siempre invariantes en C# 3.0, tanto para parámetros cuanto para tipos de retorno (lo mismo aplica para interfaces).

Dos ejemplos:

  • No pueden retornar un IEnumerable<string> si el método retorna IEnumerable<object> (ésto sería, covarianza), y
  • No pueden usar un delegado del tipo de Action<object> para reemplazar un delegado del tipo de Action<string> (ésto sería pués, contravarianza). Por favor fíjense que me estoy refiriendo aquí a asignar, digamos:
   1: Action<Gato> myDelegate = new Action<Animal>
   2: ( myAnimal => myAnimal.HacerAlgoConElAnimal() );

Y no a algo como –lo cual por cierto funciona bien:

   1: Action<Gato> miDelegado = MiMetodo;
   2:  
   3: public void MiMetodo(Animal miAnimal) { … }

Y si, por favor! Inténtenlo con C# 3.0 si dudan de mis palabras.

(III) La Solución

Y, ¿cuál es todo este lío con el nuevo uso agrgado a las palabras reservadas existentes "in" y "out" en el venidero C# 4.0?

Para tipos de referencia:

  • in = contravarianza (sólo pasar argumentos),
  • out = covarianza (sólo retornar tipos).

De los ejemplos anteriores:

  • Covarianza: IEnumerable<T> se convertirá en IEnumerable<out T>, así que un delegado que declara retornar un IEnumerable<object> de hecho podrá retornar un IEnumerable<string>, y
  • Contravarianza: con Action<in T>, podrán asignar un delegado del tipo Action<object> cada vez que esperen uno de Action<string>.

Les aconsejo dar un vistazo a los artículos listados al principio de este artículo en busca de ejemplos de código completos sobre este tema.

Fiú, eso es todo! Como pueden ver, la teoría detrás de estos conceptos no es tan fácil de entender pero tampoco tan difícil, así que espero que realmente hayan encontrado esta explicación útil para culminar la tarea de comprensión

Hasta la próxima,
~Pete

> Vínculo a la versión en inglés.

4 comentarios:

aaron dijo...

excelente, muy buena explicacion, y bien sencilla

Ultrahead dijo...

Gracias por tu comentario!

Anónimo dijo...

Excelente explicación, fue de mucha utilidad para entender estos conceptos complejos.

Anónimo dijo...

Excelente explicaciòn!!
Muchas Gracias...