Ковариантность и контрвариантность в универсальных шаблонах
Термины ковариантность и контрвариантность относятся к возможности использовать более производный (более конкретный) или менее производный (менее конкретный) тип, чем задано изначально. Параметры универсальных типов поддерживают ковариантность и контрвариантность и обеспечивают большую гибкость в назначении и использовании универсальных типов.
Ниже приведены определения терминов "ковариантность", "контрвариантность" и "инвариантность" в контексте системы типов. В этих примерах предполагается наличие базового класса с именем Base и производного класса с именем Derived .
Позволяет использовать тип с большей глубиной наследования, чем задано изначально.
Экземпляр IEnumerable<Derived> можно присвоить переменной типа IEnumerable<Base> .
Позволяет использовать более универсальный тип (с меньшей глубиной наследования), чем заданный изначально.
Экземпляр Action<Base> можно присвоить переменной типа Action<Derived> .
Это означает, что можно использовать только изначально указанный тип. Таким образом, параметр инвариантного универсального типа не является ни ковариантным, ни контрвариантным.
Экземпляр List<Base> нельзя присвоить переменной типа List<Derived> (и наоборот).
Параметры ковариантного типа позволяют создавать назначения, которые выглядят очень похоже на обычный полиморфизм, как показывает следующий код.
Класс List<T> реализует интерфейс IEnumerable<T> , поэтому List<Derived> ( List(Of Derived) в Visual Basic) реализует IEnumerable<Derived> . Параметр ковариантного типа делает все остальное.
Контрвариантность, с другой стороны, выглядит нелогичной. Следующий пример создает делегат типа Action<Base> ( Action(Of Base) в Visual Basic), а затем назначает этот делегат переменной типа Action<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> .
Универсальные интерфейсы с контрвариантными параметрами типа
Несколько универсальных интерфейсов имеют контрвариантные параметры типа, например 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 .
Универсальные делегаты с параметрами вариантного типа
Универсальные методы-делегаты 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 .
Вторая часть кода показывает, что делегат может быть присвоен переменной типа Func<Base, Base> ( Func(Of Base, Base) в Visual Basic), так как возвращаемый тип является ковариантным.
Третья часть кода показывает, что делегат может быть присвоен переменной типа Func<Derived, Derived> ( Func(Of Derived, Derived) в Visual Basic), так как тип параметра является контравариантным.
Заключительная часть кода показывает, что делегат может быть присвоен переменной типа Func<Derived, Base> ( Func(Of Derived, Base) в Visual Basic), объединяя эффекты контравариантного параметра типа и ковариантного возвращаемого типа.
Вариативность в неуниверсальных делегатахВ предыдущем коде сигнатура метода 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>.
Определение вариантных универсальных интерфейсов и делегатов
Языки Visual Basic и C# содержат ключевые слова, позволяющие помечать параметры универсального типа для интерфейсов и делегатов как ковариантные или контрвариантные.
Параметр ковариантного типа помечается ключевым словом out (ключевым словом Out в Visual Basic). Параметр ковариантного типа можно использовать как возвращаемое значение метода, принадлежащего интерфейсу, или как возвращаемый тип делегата. Параметр ковариантного типа нельзя использовать как ограничение универсального типа для методов интерфейса.
Если метод интерфейса имеет параметр с типом универсального метода-делегата, параметр ковариантного типа этого типа интерфейса может использоваться для указания параметра контравариантного типа этого типа делегата.
Параметр контрвариантного типа помечается ключевым словом in (ключевым словом In в Visual Basic). Параметр контравариантного типа можно использовать как тип параметра метода, принадлежащего интерфейсу, или как тип параметра делегата. Параметр контравариантного типа можно использовать как ограничение универсального типа для метода интерфейса.
Параметры вариантного типа могут иметь только типы интерфейса и типы делегата. Тип интерфейса или тип делегата может иметь как ковариантные, так и контравариантные параметры типа.
Языки Visual Basic и C# не позволяют нарушать правила использования параметров ковариантного и контравариантного типов или добавлять заметки ковариантности или контрвариантности в параметры типа, имеющие тип, отличный от интерфейсов и делегатов.
Список типов
Перечисленные ниже типы интерфейсов и делегатов имеют параметры ковариантного и/или контрвариантного типа.