Covariance and Contravariance

by Misael Monterroca 9. September 2009 19:58

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

Tags:

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
  • Comment
  • Preview
Loading



MVP