Covarianza y contravarianza en genéricos

Covarianza y contravarianza son términos que hacen referencia a la capacidad de usar un tipo más derivado (más específico) o menos derivado (menos específico) que el indicado originalmente. Los parámetros de tipo genérico admiten la covarianza y contravarianza para proporcionar mayor flexibilidad a la hora de asignar y usar tipos genéricos.

Cuando se hace referencia a un sistema de tipos, la covarianza, contravarianza e invarianza tienen las definiciones siguientes. En el ejemplo se presupone una clase base denominada Base y una clase derivada denominada Derived.

  • Covariance

    Permite usar un tipo más derivado que el especificado originalmente.

    Puede asignar una instancia de IEnumerable<Derived> a una variable de tipo IEnumerable<Base>.

  • Contravariance

    Permite usar un tipo más genérico (menos derivado) que el especificado originalmente.

    Puede asignar una instancia de Action<Base> a una variable de tipo Action<Derived>.

  • Invariance

    Significa que solo se puede usar el tipo especificado originalmente. Un parámetro de tipo genérico invariable no es covariante ni contravariante.

    No se puede asignar una instancia de List<Base> a una variable de tipo List<Derived> o viceversa.

Los parámetros de tipo covariante permiten realizar asignaciones muy similares al polimorfismo, como se muestra en el código siguiente.

IEnumerable<Derived> d = new List<Derived>();
IEnumerable<Base> b = d;
Dim d As IEnumerable(Of Derived) = New List(Of Derived)
Dim b As IEnumerable(Of Base) = 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 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>.

Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new 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())

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 al que está asignada, 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:

  • 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.

  • A partir de C# 9, se admiten los tipos de valor devuelto covariantes. Un método de invalidación puede declarar un tipo de valor devuelto más derivado que el método que invalida, y una propiedad de invalidación, de solo lectura, puede declarar un tipo más derivado.

Interfaces genéricas con parámetros de tipo covariante

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 devuelto 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>.

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;
    }
}
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

Interfaces genéricas con parámetros de tipo contravariante

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 abstractaMustInherit ( Shape 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.

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
 */
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

Delegados genéricos con parámetros de tipo variante

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.

Nota

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 claveout ), mientras que los otros parámetros de tipo genérico son contravariante (palabra clavein ).

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 Basey 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 {}
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;
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

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.
Func<Base, Base> f2 = f1;
Base b2 = f2(new Base());
' Covariant return type.
Dim f2 As Func(Of Base, Base) = f1
Dim b2 As Base = 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.
Func<Derived, Derived> f3 = f1;
Derived d3 = f3(new Derived());
' Contravariant parameter type.
Dim f3 As Func(Of Derived, Derived) = f1
Dim d3 As Derived = 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.
Func<Derived, Base> f4 = f1;
Base b4 = f4(new Derived());
' Covariant return type and contravariant parameter type.
Dim f4 As Func(Of Derived, Base) = f1
Dim b4 As Base = f4(New Derived())

Varianza en delegados 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 Varianza en delegados (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>.

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());
    }
}
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

Definición de interfaces y delegados genéricos variantes

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.

Un parámetro de tipo covariante se marca con la palabra clave out (Out en Visual Basic). 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.

Nota

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 (In en Visual Basic). 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.

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

Lista de tipos

Los siguientes tipos de interfaz y delegado tienen parámetros de tipo covariante 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>
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>

Vea también