Ковариантность и контрвариантность в универсальных шаблонах

Термины ковариантность и контрвариантность относятся к возможности использовать более производный (более конкретный) или менее производный (менее конкретный) тип, чем задано изначально. Параметры универсальных типов поддерживают ковариантность и контравариантность и обеспечивают большую гибкость в назначении и использовании универсальных типов.

Ниже приведены определения терминов "ковариантность", "контрвариантность" и "инвариантность" в контексте системы типов. В этих примерах предполагается наличие базового класса с именем Base и производного класса с именем Derived.

  • Covariance

    Позволяет использовать тип с большей глубиной наследования, чем задано изначально.

    Экземпляр IEnumerable<Derived> можно присвоить переменной типа IEnumerable<Base>.

  • Contravariance

    Позволяет использовать более универсальный тип (с меньшей глубиной наследования), чем заданный изначально.

    Экземпляр Action<Base> можно присвоить переменной типа Action<Derived>.

  • Invariance

    Это означает, что можно использовать только изначально указанный тип. Таким образом, параметр инвариантного универсального типа не является ни ковариантным, ни контрвариантным.

    Экземпляр List<Base> нельзя присвоить переменной типа List<Derived> (и наоборот).

Параметры ковариантного типа позволяют создавать назначения, которые выглядят очень похоже на обычный полиморфизм, как показывает следующий код.

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

Класс List<T> реализует интерфейс IEnumerable<T> , поэтому List<Derived> (List(Of Derived) в Visual Basic) реализует IEnumerable<Derived>. Параметр ковариантного типа делает все остальное.

Контрвариантность, с другой стороны, выглядит нелогичной. Следующий пример создает делегат типа Action<Base> (Action(Of Base) в Visual Basic), а затем назначает этот делегат переменной типа 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())

Это может показаться шагом назад, но это типобезопасный код, который компилируется и выполняется. Лямбда-выражение соответствует делегату, которому присвоено, и определяет метод, который принимает один параметр типа Base и не имеет возвращаемого значения. Результирующий делегат может быть присвоен переменной типа Action<Derived> , так как параметр типа T делегата Action<T> является контравариантным. Код является типобезопасным, потому что T задает тип параметра. Когда делегат типа Action<Base> вызван так, как если бы он был делегатом типа Action<Derived>, его аргумент должен быть аргументом типа Derived. Этот аргумент всегда может быть безопасно передан базовому методу, потому что параметр метода является параметром типа Base.

В общем случае, параметр ковариантного типа может быть использован в качестве возвращаемого типа делегата, и параметры контравариантного типа могут быть использованы в качестве типов параметра. Для интерфейса параметры ковариантного типа могут быть использованы в качестве возвращаемых типов методов интерфейса, и параметры контравариантного типа могут быть использованы как типы параметра методов интерфейса.

Вместе ковариантность и контравариантность называются вариацией. Параметр универсального типа, который не отмечен как ковариантный или контравариантный, называется инвариантным. Краткие сведения о вариативности в общеязыковой среде выполнения:

  • Параметры вариантного типа ограничены типами универсального интерфейса и универсального метода-делегата.

  • Тип универсального интерфейса или универсального метода-делегата может иметь как ковариантные, так и контравариантные параметры типа.

  • Вариативность применяется только к ссылочным типам; если указать тип значения для параметра вариантного типа, этот параметр типа является инвариантным для типа, созданного в результате.

  • Вариативность не применима к объединению делегатов. Поэтому для заданных двух делегатов типов Action<Derived> и Action<Base> (Action(Of Derived) и Action(Of Base) в Visual Basic) нельзя объединять первый делегат со вторым, несмотря на то что результат будет безопасным типом. Вариативность позволяет присвоить второй делегат переменной типа Action<Derived>, но делегаты можно объединять, только если их типы точно совпадают.

  • Начиная с C# 9, поддерживаются ковариантные типы возвращаемого значения. В переопределяющем методе может объявляться более производный тип возвращаемого значения, чем в переопределяемом, и для переопределяющего доступного только для чтения свойства может объявляться более производный тип.

Универсальные интерфейсы с ковариантными параметрами типа

Несколько универсальных интерфейсов имеют ковариантные параметры типа, например IEnumerable<T>, IEnumerator<T>, IQueryable<T> и IGrouping<TKey,TElement>. Эти интерфейсы имеют только параметры ковариантного типа. Таким образом, параметры типа используются только для возвращаемых типов в членах.

В следующем примере демонстрируются ковариантные параметры типа. В примере определяются два типа: Base имеет статический метод с именем PrintBases , принимающий IEnumerable<Base> (IEnumerable(Of Base) в Visual Basic) и выводящий эти элементы. Интерфейс Derived наследуется от интерфейса Base. В примере создается пустой список List<Derived> (List(Of Derived) в Visual Basic) и показывается, что этот тип может быть передан методу PrintBases и назначен переменной типа IEnumerable<Base> без приведения. КлассList<T> реализует интерфейс IEnumerable<T>, который имеет единственный параметр ковариантного типа. Параметр ковариантного типа — это причина, по которой экземпляр IEnumerable<Derived> может быть использован вместо 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

Универсальные интерфейсы с контрвариантными параметрами типа

Несколько универсальных интерфейсов имеют контрвариантные параметры типа, например IComparer<T>, IComparable<T> и IEqualityComparer<T>. Эти интерфейсы имеют только параметры контравариантного типа, таким образом, параметры типа используются только как типы параметра в членах интерфейсов.

В следующем примере демонстрируются контравариантные параметры типа. В примере определяется абстрактный (MustInherit в Visual Basic) класс Shape со свойством Area . В примере также определяется класс ShapeAreaComparer , реализующий IComparer<Shape> (IComparer(Of Shape) в Visual Basic). Реализация метода IComparer<T>.Compare основывается на значении свойства Area , поэтому с помощью ShapeAreaComparer можно сортировать объекты Shape по областям.

Класс Circle наследует Shape и переопределяет Area. В примере создается набор SortedSet<T> объектов Circle с помощью конструктора, принимающего IComparer<Circle> (IComparer(Of Circle) в Visual Basic). Однако вместо передачи IComparer<Circle>, в примере передается объект ShapeAreaComparer , реализующий IComparer<Shape>. В примере может передаваться компаратор типа меньшей глубины наследования (Shape), когда код вызывает компаратор типа большей глубины наследования (Circle), поскольку параметр типа универсального интерфейса IComparer<T> контрвариантен.

При добавлении нового объекта Circle в SortedSet<Circle> метод IComparer<Shape>.Compare (методIComparer(Of Shape).Compare в Visual Basic) объекта ShapeAreaComparer вызывается всякий раз, когда новый элемент сравнивается с существующим элементом. Тип параметра метода (Shape) является менее производным, чем передаваемый тип (Circle), поэтому этот вызов является типобезопасным. Контрвариантность позволяет объекту ShapeAreaComparer сортировать коллекцию какого-либо одного типа, а также смешанную коллекцию типов, унаследованных от 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

Универсальные делегаты с параметрами вариантного типа

Универсальные методы-делегаты Func, такие как Func<T,TResult>, имеют ковариантные типы возвращаемого значения и контрвариантные типы параметров. Универсальные методы-делегаты Action , такие как Action<T1,T2>, имеют контравариантные типы параметров. Это означает, что делегаты можно присваивать переменным, имеющим более производные типы параметров и (в случае универсальных методов-делегатов Func) менее производные возвращаемые типы.

Примечание.

Последний параметр универсального типа универсальных методов-делегатов Func указывает тип возвращаемого значения в сигнатуре делегата. Он является ковариантным (ключевое словоout ), в то время как остальные параметры универсального типа являются контравариантными (ключевое словоin ).

Это показано в следующем коде. Первая часть кода определяет класс с именем Base, класс с именем Derived , наследующий от класса Base, и еще один класс с методом типа static (Shared в Visual Basic) и именем MyMethod. Этот метод принимает экземпляр класса Base и возвращает экземпляр класса Derived. (Если аргумент является экземпляром Derived, MyMethod возвращает его; если аргумент является экземпляром Base, MyMethod возвращает новый экземпляр Derived.) В Main()примере создается экземпляр Func<Base, Derived> (Func(Of Base, Derived) в Visual Basic), который представляет MyMethodи сохраняет его в переменной 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

Вторая часть кода показывает, что делегат может быть присвоен переменной типа Func<Base, Base> (Func(Of Base, Base) в Visual Basic), так как возвращаемый тип является ковариантным.

// 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())

Третья часть кода показывает, что делегат может быть присвоен переменной типа Func<Derived, Derived> (Func(Of Derived, Derived) в Visual Basic), так как тип параметра является контравариантным.

// 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())

Заключительная часть кода показывает, что делегат может быть присвоен переменной типа Func<Derived, Base> (Func(Of Derived, Base) в Visual Basic), объединяя эффекты контравариантного параметра типа и ковариантного возвращаемого типа.

// 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())

Вариативность в неуниверсальных делегатах

В предыдущем коде сигнатура метода MyMethod точно соответствует сигнатуре сконструированного универсального делегата: Func<Base, Derived> (Func(Of Base, Derived) в Visual Basic). Пример показывает, что этот универсальный делегат может храниться в параметрах переменных или метода, имеющих более производные типы параметров и менее производные возвращаемые типы, при условии, что все типы делегата сконструированы из универсального типа делегата Func<T,TResult>.

Это важное правило. Влияние ковариантности и контрвариантности в параметрах типа универсального делегата аналогично влиянию ковариантности и контрвариантности в обыкновенной привязке делегата (см. статьи Вариативность в делегатах (C#) и Вариативность в делегатах (Visual Basic)). Однако вариативность в привязке делегата работает для всех типов делегата, а не только с типами универсального метода-делегата, имеющего вариантные параметры типа. Более того, вариативность в привязке делегата допускает привязку метода к любому делегату, имеющему более строгие типы параметров и менее строгий возвращаемый тип, в то время как назначение универсальных делегатов работает только в том случае, если оба типа делегата сконструированы из одного определения универсального типа.

В следующем примере показан суммарный эффект вариантности в привязке делегата и в параметрах универсального типа. В примере определяется иерархия типов, содержащая три типа, от наименее производного (Type1) до наиболее производного (Type3). Вариантность в обычной привязке делегата используется для привязки метода с типом параметра Type1 и возвращаемым типом Type3 к универсальному делегату с типом параметра Type2 и возвращаемым типом Type2. Получившийся универсальный метод-делегат затем присваивается другой переменной, тип универсального метода-делегата которой имеет тип параметра Type3 и тип возвращаемого значения Type1, с использованием ковариантности и контрвариантности параметров универсального типа. Для второго присваивания требуется, чтобы тип переменной и тип делегата были сконструированы из одного определения универсального типа, в данном случае — 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

Определение вариантных универсальных интерфейсов и делегатов

Языки Visual Basic и C# содержат ключевые слова, позволяющие помечать параметры универсального типа для интерфейсов и делегатов как ковариантные или контрвариантные.

Параметр ковариантного типа помечается ключевым словом out (ключевым словом Out в Visual Basic). Параметр ковариантного типа можно использовать как возвращаемое значение метода, принадлежащего интерфейсу, или как возвращаемый тип делегата. Параметр ковариантного типа нельзя использовать как ограничение универсального типа для методов интерфейса.

Примечание.

Если метод интерфейса имеет параметр с типом универсального метода-делегата, параметр ковариантного типа этого типа интерфейса может использоваться для указания параметра контравариантного типа этого типа делегата.

Параметр контрвариантного типа помечается ключевым словом in (ключевым словом In в Visual Basic). Параметр контравариантного типа можно использовать как тип параметра метода, принадлежащего интерфейсу, или как тип параметра делегата. Параметр контравариантного типа можно использовать как ограничение универсального типа для метода интерфейса.

Параметры вариантного типа могут иметь только типы интерфейса и типы делегата. Тип интерфейса или тип делегата может иметь как ковариантные, так и контравариантные параметры типа.

Языки Visual Basic и C# не позволяют нарушать правила использования параметров ковариантного и контравариантного типов или добавлять заметки ковариантности или контрвариантности в параметры типа, имеющие тип, отличный от интерфейсов и делегатов.

Дополнительные сведения и пример кода см. в разделах Вариативность в универсальных интерфейсах (C#) и Вариативность в универсальных интерфейсах (Visual Basic).

Список типов

Перечисленные ниже типы интерфейсов и делегатов имеют параметры ковариантного и/или контрвариантного типа.

Тип Параметры ковариантного типа Параметры контравариантного типа
Action<T>...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>...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> Да

См. также