Compartir a través de


Covarianza y contravarianza en genéricos

Los parámetros de tipo genérico covariante y contravariante proporcionan mayor flexibilidad a la hora de asignar y utilizar tipos genéricos. Por ejemplo, los parámetros de tipo covariante permiten realizar asignaciones muy similares al polimorfismo. Supongamos que tiene una clase base y una clase derivada, denominadas Base y Derived. El polimorfismo permite asignar una instancia de Derived a una variable de tipo Base. De manera similar, dado que el parámetro de tipo de la interfaz IEnumerable<T> es covariante, se puede asignar una instancia de IEnumerable<Derived> (IEnumerable(Of Derived) en Visual Basic) a una variable de tipo IEnumerable<Base>, tal y como se muestra en el siguiente código.

Dim d As IEnumerable(Of Derived) = New List(Of Derived)
Dim b As IEnumerable(Of Base) = d
IEnumerable<Derived> d = new List<Derived>();
IEnumerable<Base> b = d;

La clase List<T> implementa la interfaz IEnumerable<T>, por lo que List<Derived> (List(Of Derived) en Visual Basic) implementa IEnumerable<Derived>. El parámetro de tipo covariante se encarga del resto.

La covarianza parece muy natural porque es similar al polimorfismo. La contravarianza, sin embargo, parece poco intuitiva. En el siguiente ejemplo, se crea un delegado de tipo Action<Base> (Action(Of Base) en Visual Basic) y, a continuación, se asigna ese delegado a una variable de tipo Action<Derived>.

Dim b As Action(Of Base) = Sub(target As Base) 
                               Console.WriteLine(target.GetType().Name)
                           End Sub
Dim d As Action(Of Derived) = b
d(New Derived())
Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());

Parece un paso hacia atrás, pero lo que se compila y se ejecuta es código con seguridad de tipos. La expresión lambda se corresponde con el delegado que tiene asignado, por lo que define un método que toma un parámetro de tipo Base y no tiene ningún valor devuelto. El delegado resultante puede asignarse a una variable de tipo Action<Derived> porque el parámetro de tipo T del delegado Action<T> es contravariante. El código tiene seguridad de tipos porque T especifica un tipo de parámetro. Cuando se invoca el delegado de tipo Action<Base> como si fuera un delegado de tipo Action<Derived>, su argumento debe ser de tipo Derived. Este argumento siempre se puede pasar de manera segura al método subyacente porque el parámetro del método es de tipo Base.

En general, los parámetros de tipo covariante se pueden utilizar como tipos de valor devuelto de un delegado, y los parámetros de tipo contravariante se pueden usar como tipos de parámetro. En el caso de una interfaz, los parámetros de tipo covariante se pueden utilizar como tipos de valor devuelto de los métodos de la interfaz, y los parámetros de tipo contravariante se pueden usar como tipos de parámetro de los métodos de la interfaz.

La covarianza y la contravarianza se denominan colectivamente varianza. Un parámetro de tipo genérico que no está marcado como covariante ni contravariante se denomina invariable. Un breve resumen de hechos relacionados con la varianza en Common Language Runtime:

  • En .NET Framework versión 4, los parámetros de tipo variante están restringidos a los tipos de interfaz genérica y delegado genérico.

  • Un tipo de interfaz genérica o de delegado genérico puede tener parámetros de tipo covariante y contravariante.

  • La varianza se aplica únicamente a los tipos de referencia; si se especifica un tipo de valor para un parámetro de tipo variante, ese parámetro de tipo es invariable para el tipo construido resultante.

  • La varianza no se aplica a la combinación de delegados. Es decir, si hay dos delegados de tipo Action<Derived> y de tipo Action<Base> (Action(Of Derived) y Action(Of Base) en Visual Basic), no se puede combinar el segundo delegado con el primero aunque el resultado tuviese seguridad de tipos. La varianza permite la asignación del segundo delegado a una variable de tipo Action<Derived>, pero los delegados solo se pueden combinar si tienen exactamente el mismo tipo.

En las próximas subsecciones se describen los parámetros de tipo covariante y contravariante en detalle:

  • Interfaces genéricas con parámetros de tipo covariante

  • Interfaces genéricas con parámetros de tipo genérico contravariante

  • Delegados genéricos con parámetros de tipo variante

  • Definir interfaces y delegados genéricos variantes

  • Lista de tipos de interfaces y delegados genéricos variantes

Interfaces genéricas con parámetros de tipo covariante

A partir de .NET Framework 4, varias interfaces genéricas tienen parámetros de tipo covariante; por ejemplo: IEnumerable<T>, IEnumerator<T>, IQueryable<T> y IGrouping<TKey, TElement>. Todos los parámetros de tipo de estas interfaces son covariantes, por lo que los parámetros de tipo se usan únicamente para los tipos de valor devueltos de los miembros. 

En el ejemplo siguiente, se muestran los parámetros de tipo covariante. Se definen dos tipos: Base tiene un método estático denominado PrintBases que toma una interfaz IEnumerable<Base> (IEnumerable(Of Base) en Visual Basic) e imprime los elementos. Derived hereda de Base. En el ejemplo, se crea un tipo List<Derived> (List(Of Derived) en Visual Basic) vacío y se muestra que este tipo se puede pasar a PrintBases y asignar a una variable de tipo IEnumerable<Base> sin conversión alguna. List<T> implementa IEnumerable<T>, que tiene un solo parámetro de tipo covariante. El parámetro de tipo covariante es el motivo por el cual se puede usar una instancia de IEnumerable<Derived> en lugar de IEnumerable<Base>.

Imports System.Collections.Generic

Class Base
    Public Shared Sub PrintBases(ByVal bases As IEnumerable(Of Base))
        For Each b As Base In bases
            Console.WriteLine(b)
        Next
    End Sub    
End Class

Class Derived
    Inherits Base

    Shared Sub Main()
        Dim dlist As New List(Of Derived)()

        Derived.PrintBases(dlist)
        Dim bIEnum As IEnumerable(Of Base) = dlist
    End Sub
End Class
using System;
using System.Collections.Generic;

class Base
{
    public static void PrintBases(IEnumerable<Base> bases)
    {
        foreach(Base b in bases)
        {
            Console.WriteLine(b);
        }
    }
}

class Derived : Base
{
    public static void Main()
    {
        List<Derived> dlist = new List<Derived>();

        Derived.PrintBases(dlist);
        IEnumerable<Base> bIEnum = dlist;
    }
}

Volver al principio

Interfaces genéricas con parámetros de tipo genérico contravariante

A partir de .NET Framework 4, varias interfaces genéricas tienen parámetros de tipo contravariante; por ejemplo: IComparer<T>, IComparable<T> y IEqualityComparer<T>. Estas interfaces tienen únicamente parámetros de tipo contravariante, por lo que los parámetros de tipo se utilizan solamente como tipos de parámetro en los miembros de las interfaces.

En el ejemplo siguiente se muestran los parámetros de tipo contravariante. En el ejemplo se define clase abstracta Shape (MustInherit en Visual Basic) con una propiedad Area. En el ejemplo también se define una clase ShapeAreaComparer que implementa IComparer<Shape> (IComparer(Of Shape) en Visual Basic). La implementación del método IComparer<T>.Compare se basa en el valor de la propiedad Area, por lo que ShapeAreaComparer se puede usar para ordenar los objetos Shape por área.

La clase Circle hereda Shape e invalida Area. En el ejemplo se crea una colección SortedSet<T> de objetos Circle, usando un constructor que toma IComparer<Circle> (IComparer(Of Circle) en Visual Basic). Sin embargo, en lugar de pasar IComparer<Circle>, en el ejemplo se pasa un objeto ShapeAreaComparer, que implementa IComparer<Shape>. En el ejemplo se puede pasar un comparador de un tipo menos derivado (Shape) cuando el código llama a un comparador de un tipo más derivado (Circle), ya que el parámetro de tipo de la interfaz genérica IComparer<T> es contravariante.

Cuando se agrega un nuevo objeto Circle a SortedSet<Circle>, se llama al método IComparer<Shape>.Compare (IComparer(Of Shape).Compare en Visual Basic) del objeto ShapeAreaComparer cada vez que el nuevo elemento se compara con un elemento existente. El tipo de parámetro del método (Shape) es menos derivado que el tipo que se pasa (Circle), por lo que la llamada tiene seguridad de tipos. La contravarianza permite a ShapeAreaComparer ordenar una colección de cualquier tipo único, así como a una colección mixta de tipos, que derivan de Shape.

Imports System.Collections.Generic

MustInherit Class Shape
    Public MustOverride ReadOnly Property Area As Double
End Class

Class Circle
    Inherits Shape

    Private r As Double 
    Public Sub New(ByVal radius As Double)
        r = radius
    End Sub 
    Public ReadOnly Property Radius As Double
        Get
            Return r
        End Get
    End Property
    Public Overrides ReadOnly Property Area As Double
        Get
            Return Math.Pi * r * r
        End Get
    End Property
End Class

Class ShapeAreaComparer
    Implements System.Collections.Generic.IComparer(Of Shape)

    Private Function AreaComparer(ByVal a As Shape, ByVal b As Shape) As Integer _
            Implements System.Collections.Generic.IComparer(Of Shape).Compare
        If a Is Nothing Then Return If(b Is Nothing, 0, -1)
        Return If(b Is Nothing, 1, a.Area.CompareTo(b.Area))
    End Function
End Class

Class Program
    Shared Sub Main()
        ' You can pass ShapeAreaComparer, which implements IComparer(Of Shape),
        ' even though the constructor for SortedSet(Of Circle) expects 
        ' IComparer(Of Circle), because type parameter T of IComparer(Of T)
        ' is contravariant.
        Dim circlesByArea As New SortedSet(Of Circle)(New ShapeAreaComparer()) _
            From { New Circle(7.2), New Circle(100), Nothing, New Circle(.01) }

        For Each c As Circle In circlesByArea
            Console.WriteLine(If(c Is Nothing, "Nothing", "Circle with area " & c.Area))
        Next
    End Sub
End Class

' This code example produces the following output:
'
'Nothing
'Circle with area 0.000314159265358979
'Circle with area 162.860163162095
'Circle with area 31415.9265358979
using System;
using System.Collections.Generic;

abstract class Shape
{
    public virtual double Area { get { return 0; }}
}

class Circle : Shape
{
    private double r;
    public Circle(double radius) { r = radius; }
    public double Radius { get { return r; }}
    public override double Area { get { return Math.PI * r * r; }}
}

class ShapeAreaComparer : System.Collections.Generic.IComparer<Shape>
{
    int IComparer<Shape>.Compare(Shape a, Shape b) 
    { 
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.Area.CompareTo(b.Area);
    }
}

class Program
{
    static void Main()
    {
        // You can pass ShapeAreaComparer, which implements IComparer<Shape>,
        // even though the constructor for SortedSet<Circle> expects 
        // IComparer<Circle>, because type parameter T of IComparer<T> is
        // contravariant.
        SortedSet<Circle> circlesByArea = 
            new SortedSet<Circle>(new ShapeAreaComparer()) 
                { new Circle(7.2), new Circle(100), null, new Circle(.01) };

        foreach (Circle c in circlesByArea)
        {
            Console.WriteLine(c == null ? "null" : "Circle with area " + c.Area);
        }
    }
}

/* This code example produces the following output:

null
Circle with area 0.000314159265358979
Circle with area 162.860163162095
Circle with area 31415.9265358979
 */

Volver al principio

Delegados genéricos con parámetros de tipo variante

En .NET Framework 4, los delegados genéricos Func, como Func<T, TResult>, tienen tipos de valor devueltos covariante y tipos de parámetro contravariante. Los delegados genéricos Action, como Action<T1, T2>, tienen tipos de parámetro contravariante. Esto significa que los delegados se pueden asignar a variables que tengan tipos de parámetro más derivados y (en el caso de los delegados genéricos Func) tipos de valor devuelto menos derivados.

NotaNota

El último parámetro de tipo genérico de los delegados genéricos Func especifica el tipo del valor devuelto en la firma de delegado.Es covariante (palabra clave out), mientras que los otros parámetros de tipo genérico son contravariante (palabra clave in).

Esto se ilustra en el código siguiente: En el primer fragmento de código, se definen una clase denominada Base, una clase denominada Derived que hereda de Base y otra clase con un método static (Shared en Visual Basic) denominado MyMethod. El método toma una instancia de Base y devuelve una instancia de Derived. (Si el argumento es una instancia de Derived, MyMethod la devuelve; si el argumento es una instancia de Base, MyMethod devuelve una nueva instancia de Derived.) En Main(), se crea en el ejemplo una instancia de Func<Base, Derived> (Func(Of Base, Derived) en Visual Basic) que representa MyMethod, y la almacena en la variable f1.

Public Class Base 
End Class
Public Class Derived
    Inherits Base
End Class

Public Class Program
    Public Shared Function MyMethod(ByVal b As Base) As Derived 
        Return If(TypeOf b Is Derived, b, New Derived())
    End Function

    Shared Sub Main() 
        Dim f1 As Func(Of Base, Derived) = AddressOf MyMethod
public class Base {}
public class Derived : Base {}

public class Program
{
    public static Derived MyMethod(Base b)
    {
        return b as Derived ?? new Derived();
    }

    static void Main() 
    {
        Func<Base, Derived> f1 = MyMethod;

En el segundo fragmento de código, se muestra que el delegado puede asignarse a una variable de tipo Func<Base, Base> (Func(Of Base, Base) en Visual Basic) ya que el tipo de valor devuelto es covariante.

' Covariant return type.
Dim f2 As Func(Of Base, Base) = f1
Dim b2 As Base = f2(New Base())
// Covariant return type.
Func<Base, Base> f2 = f1;
Base b2 = f2(new Base());

En el tercer fragmento de código, se muestra que el delegado puede asignarse a una variable de tipo Func<Derived, Derived> (Func(Of Derived, Derived) en Visual Basic) ya que el tipo de parámetro es contravariante.

' Contravariant parameter type.
Dim f3 As Func(Of Derived, Derived) = f1
Dim d3 As Derived = f3(New Derived())
// Contravariant parameter type.
Func<Derived, Derived> f3 = f1;
Derived d3 = f3(new Derived());

En el último fragmento de código, se muestra que el delegado puede asignarse a una variable de tipo Func<Derived, Base> (Func(Of Derived, Base) en Visual Basic), combinando los efectos del tipo de parámetro contravariante y el tipo de valor devuelto covariante.

' Covariant return type and contravariant parameter type.
Dim f4 As Func(Of Derived, Base) = f1
Dim b4 As Base = f4(New Derived())
// Covariant return type and contravariant parameter type.
Func<Derived, Base> f4 = f1;
Base b4 = f4(new Derived());

La varianza en delegados genéricos y no genéricos

En el código anterior, la signatura de MyMethod coincide exactamente con la signatura del delegado genérico construido: Func<Base, Derived> (Func(Of Base, Derived) en Visual Basic). En el ejemplo, se muestra que este delegado genérico se puede almacenar en variables o en parámetros de método que tengan tipos de parámetro más derivados y tipos de valor devuelto menos derivados, siempre y cuando todos los tipos de delegado se construyan a partir del tipo de delegado genérico Func<T, TResult>.

Este es un aspecto importante. Los efectos de la covarianza y la contravarianza en los parámetros de tipo de los delegados genéricos son similares a los efectos de la covarianza y la contravarianza en el enlace a delegados normal (vea Varianza en delegados (C# y Visual Basic)). Sin embargo, la varianza en el enlace a delegados funciona con todos los tipos de delegado, no solo con tipos de delegado genérico que tienen parámetros de tipo variante. Además, la varianza en el enlace a delegados permite enlazar un método a cualquier delegado que tenga tipos de parámetro más restrictivos y un tipo de valor devuelto menos restrictivo, mientras que la asignación de delegados genéricos solo funciona si ambos tipos de delegado se construyen a partir de la misma definición de tipo genérico.

En el ejemplo siguiente se muestran los efectos combinados de la varianza en el enlace a delegados y la varianza en los parámetros de tipo genérico. En el ejemplo se define una jerarquía de tipos que incluye tres tipos, de menos derivado (Type1) a más derivado (Type3). La varianza en el enlace a delegados normal se usa para enlazar un método con un tipo de parámetro de Type1 y un tipo de valor devuelto de Type3 a un delegado genérico con un tipo de parámetro de Type2 y un tipo de valor devuelto de Type2. A continuación, el delegado genérico resultante se asigna a otra variable cuyo tipo de delegado genérico tiene un parámetro de tipo Type3 y un tipo de valor devuelto de Type1, usando la covarianza y contravarianza de parámetros de tipo genérico. La segunda asignación requiere que tanto el tipo de variable como el tipo de delegado se construyan a partir de la misma definición de tipo genérico, en este caso Func<T, TResult>.

Public Class Type1 
End Class
Public Class Type2
    Inherits Type1
End Class
Public Class Type3
    Inherits Type2
End Class

Public Class Program
    Public Shared Function MyMethod(ByVal t As Type1) As Type3
        Return If(TypeOf t Is Type3, t, New Type3())
    End Function

    Shared Sub Main() 
        Dim f1 As Func(Of Type2, Type2) = AddressOf MyMethod

        ' Covariant return type and contravariant parameter type.
        Dim f2 As Func(Of Type3, Type1) = f1
        Dim t1 As Type1 = f2(New Type3())
    End Sub
End Class
using System;

public class Type1 {}
public class Type2 : Type1 {}
public class Type3 : Type2 {}

public class Program
{
    public static Type3 MyMethod(Type1 t)
    {
        return t as Type3 ?? new Type3();
    }

    static void Main() 
    {
        Func<Type2, Type2> f1 = MyMethod;

        // Covariant return type and contravariant parameter type.
        Func<Type3, Type1> f2 = f1;
        Type1 t1 = f2(new Type3());
    }
}

Volver al principio

Definir interfaces y delegados genéricos variantes

A partir de .NET Framework 4, Visual Basic y C# tienen palabras clave que permiten marcar como covariantes o contravariantes los parámetros de tipo genérico de las interfaces y los delegados.

NotaNota

A partir de .NET Framework versión 2.0, Common Language Runtime admite anotaciones de varianza en parámetros de tipo genérico.Antes de .NET Framework 4, la única manera de definir una clase genérica que tiene estas anotaciones es usar el lenguaje intermedio de Microsoft (MSIL), ya sea compilando la clase con Ilasm.exe (Ensamblador de MSIL) o emitiéndola en un ensamblado dinámico.

Un parámetro de tipo covariante se marca con la palabra clave out (palabra clave Out en Visual Basic, + para el Ensamblador de MSIL). Puede usar un parámetro de tipo covariante como el valor devuelto de un método que pertenece a una interfaz o como el tipo de valor devuelto de un delegado. No puede usar un parámetro de tipo covariante como una restricción de tipo genérico para los métodos de interfaz.

NotaNota

Si un método de una interfaz tiene un parámetro que es un tipo de delegado genérico, se puede usar un parámetro de tipo covariante del tipo de interfaz para especificar un parámetro de tipo contravariante del tipo de delegado.

Un parámetro de tipo contravariante se marca con la palabra clave in (palabra clave In en Visual Basic, - para el Ensamblador de MSIL). Puede usar un parámetro de tipo contravariante como el tipo de un parámetro de un método que pertenece a una interfaz o como el tipo de un parámetro de un delegado. Puede usar un parámetro de tipo contravariante como una restricción de tipo genérico para un método de interfaz.

Solo los tipos de interfaz y los tipos de delegado pueden tener parámetros de tipo variante. Un tipo de interfaz o un tipo de delegado puede tener parámetros de tipo covariante y contravariante.

Visual Basic y C# no le permiten infringir las reglas de uso de parámetros de tipo covariante y contravariante ni agregar anotaciones de covarianza y contravarianza a los parámetros de tipo de tipos distintos de interfaces y delegados. El Ensamblador de MSIL no realiza esas comprobaciones, pero se produce TypeLoadException si intenta cargar un tipo que infringe las reglas.

Para obtener información y código de ejemplo, vea Varianza en interfaces genéricas (C# y Visual Basic).

Volver al principio

Lista de tipos de interfaces y delegados genéricos variantes

En .NET Framework 4, los siguientes tipos de interfaz y delegado tienen parámetros de tipo covariante y/o contravariante. 

Tipo

Parámetros de tipo covariante

Parámetros de tipo contravariante

Action<T> a Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>

Comparison<T>

Converter<TInput, TOutput>

Func<TResult>

De Func<T, TResult> a Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, TResult>

IComparable<T>

Predicate<T>

IComparer<T>

IEnumerable<T>

IEnumerator<T>

IEqualityComparer<T>

IGrouping<TKey, TElement>

IOrderedEnumerable<TElement>

IOrderedQueryable<T>

IQueryable<T>

Volver al principio

Vea también

Conceptos

Varianza en delegados (C# y Visual Basic)

Otros recursos

Covarianza y contravarianza (C# y Visual Basic)