Una de las nuevas características de C# 4.0 es la covarianza y contravarianza en los parámetros de tipo que ahora es soportado por los delegados genéricos y las interfaces genéricas. En primer lugar vamos a ver ¿qué significan estas palabras?
En general, si tenemos alguna entidad (interfaz o delegado) que es genérico en el tipo T podriamos suponer que tengamos una definición del tipo Entidad<T>, suponiendo que tengamos dos entidades concretas Entidad<A> y Entidad<B> donde B hereda de A, en este caso no hay relaciones de herencia entre Entidad<A> y Entidad<B>. Covarianza (y contravarianza) añade precisamente este tipo de relaciones:
Covariance y Contravariance agrega algunas restricciones a la interfaz correspondiente o delegado. Covarianza se admite sólo en algunos casos, y en algunos otros casos se admite la contravarianza. Vamos a detallar cuáles son esas restricciones.
Restrincciones
La primera restricción es que los temas de covarianza y contravarianza están disponibles únicamente a los delegados e interfaces.
La segunda es que el tipo genérico utilizado para la covarianza y contravarianza debe ser un tipo de referencia. Sin embargo el tipo genérico no se limita a ser un tipo de referencia.El tipo de valor se puede utilizar también, pero no habrá relaciones de herencia para ello.
interface ICovariant<out T> { }
interface IInterface { }
struct MiEstructura : IInterface { } // tipo por valor, hereda de IInterface
class CovariantS : ICovariant<MiEstructura> { }
static void Main(string[] args)
{
ICovariant<MiEstructura> covariantS = new CovariantS();
ICovariant<IInterface> covariantI = covariantS; // <-- Aqui daria un error de compilación
}
Y la tercera (y más importante) es lo siguiente: El tipo que se va a utilizar para la covarianza sólo se puede utilizar como tipo para devolver valores en la interfaz correspondiente de delegado. Y el tipo que se va a utilizar para contravarianza sólo se puede utilizar como tipo de parámetros de entrada en la interfaz correspondiente de delegado. (Aquí y en todo el artículo me refiero a ese tipo utilizando para el método setter es lo mismo que usar como parámetro de entrada, y usarlo para el método getter es lo mismo que usar como valor de retorno.) Tanto covariantes y contravariantes tipos. Asimismo, no se puede utilizar como tipos ref (o out).
delegate T /* Permitido */ CovariantProcessor<out T>(
T value /* No Permitido */, ref T reference /* not allowed */);
delegate T /* No Permitido */ ContravariantProcessor<in T>(
T value /* Permitido */, ref T reference /* No Permitido */);
interface ICovariant<out T>
{
T Generate(); // permitido
void Use(T value); // no permitido
void Change(ref T reference); // no permitido
T Value
{
get; // permitido
set; // no permitido
}
}
interface IContravariant<in T>
{
T Generate(); // no permitido
void Use(T value); // permitiddo
void Change(ref T reference); // no permitido
T Value
{
get; // no permitido
set; // permitido
}
}
Sin esas restricciones vamos a llegar a una gran cantidad de conflictos. Es mejor que hagamos un ejemplo.
Crearemos dos clases de mamiferos, Perro y Gato
class Mamifero { }
class Perro : Mamifero { }
class Gato : Mamifero { }
interface ICovariantWrapper<out T>
{
T Value
{
get;
set; // no permitido
}
}
class Wrapper<T> : ICovariantWrapper<T>
{
T Value { get; set; }
}
static void Main(string[] args)
{
ICovariantWrapper<Perro> wrappedDog = new Wrapper<Perro>();
// creando un objeto de mamiferos del mismo tipo del perro
ICovariantWrapper<Mamifero> wrappedMammal = wrappedDog;
// poniendo el objeto gato dentro del objeto perro
wrappedMammal.Value = new Gato();
Perro dog = wrappedDog.Value; // el objeto en realidad ya es un gato
}
La única diferencia es que en primer lugar IContravariantWrapper<Mamifero> y luego se realizo un cast a IContravariantWrapper<Perro>. Después de esto el objeto Gatose puede asignar al objeto Perro.
Veamos unos ejemplos ya que la siempre teoría no siempre es suficiente.
Ejemplo Covarianza
Vamos a utilizar el interfaz ICreator<T> que es capaz de crear instancias de T diferentes fuentes. También vamos a utilizar dos clases: Entity y una subclase SerializableEntity.
public interface ICreator<T>
{
T CreateDefault();
T CreateFromXDocument(XDocument xDocument);
T CreateFromStream(Stream stream);
}
public class Entity
{
/* ... */
}
public class SerializableEntity : Entity
{
/* ... */
}
Ahora imaginemos que tenemos la clase SerializableEntityCreator que implementa la interfaz ICreator de SerializableEntity. Y también imagina que tenemos la clase EntityManager que necesita una instancia de ICreator<Entity> que funcione correctamente.
public class SerializableEntityCreator : ICreator<SerializableEntity>
{
/* ... */
}
public class EntityManager
{
public EntityManager(ICreator<Entity> entityCreator)
{
/* ... */
}
}
Vamos a ver ahora lo que tenemos. Por un lado tenemos SerializableEntityCreator que es capaz de crear instancias de SerializableEntity. también puede crear instancias de Entity porque cada SerializableEntity es una Entity también. Por otro lado tenemos EntityManager que necesita algo que se puede crear instancias de Entity. Parece que SerializableEntityCreator va a coincidir con EntityManager pero sin covarianza no es cierto y el siguiente código no compilará:
ICreator<SerializableEntity> entityCreator
= new SerializableEntityCreator();
EntityManager manager = new EntityManager(entityCreator); // error
Para resolver este problema en C # 2.0/3.0 tenemos que aplicar tanto ICreator<Entity> y ICreator <SerializableEntity> en SerializableEntityCreator. Pero en C # 4.0 esto puede hacerse al sólo se declarar como covariant a la interfaz
public interface ICreator<out T>
{
/* ... */
}
Ahora ICreator<T> es covariante por T. Eso significa que SerializableEntityCreator no sólo es ICreator<SerilizableEntity>, pero es una ICreator<Entity> también.
ICreator<SerializableEntity> entityCreator
= new SerializableEntityCreator();
EntityManager manager = new EntityManager(entityCreator); // ya no dará error y compilara