Ковариация и контрвариация C#
В этой статье я бы хотел дать свое объяснение как работает ковариация и контрвариация в C#.
Что такое ковариация и контрвариация
Предположим у нас есть два типа - тип Animal и тип Cat и Cat наследуется от Animal - то есть мы можем Cat привести к Animal и есть третий тип, который использует типы Cat и Animal, допустим это тип Zoo. Тип Zoo является ковариантным, если мы можем Zoo<Cat>
привести к Zoo<Animal>
и является контрвариантным, если мы можем Zoo<Animal>
привести к Zoo<Cat>
. Я использовал синтаксис обобщения в примере, но вообще - это будет не обязательно обобщение - смысл в том, что один тип использует другие типы и поддерживает приведения этих типов в контексте себя.
Классическим является пример с приведением списка:
IEnumerable<string> strCollection = new List<string>() {"string"};
IEnumerable<object> objectCollection = strCollection;
string - наследуется от object и коллекцию string мы можем привести к коллекции object.
То есть, если некоторый метод, в качестве принимаемого параметра использует IEnumerable<object>
, то мы можем передать в этот метод переменную с типом IEnumerable<string>
и она будет приведена к нужному типу.
Смысл ковариации и контрвариации в C#
Ковариация и контрвариация в C# существует только в контексте методов. Давайте представим, что под типом Zoo из примера выше мы подразумеваем метод, а типы Cat и Animal - это типы возвращемого значения или входного параметра - то есть под Zoo<SomeType>
(где SomeType - Cat или Animal) мы подразумеваем:
public SomeType Zoo()
{
}
или
public void Zoo(SomeType param)
{
}
Теперь давайте представим, что у нас есть следующий метод:
public Animal Zoo()
{
//someCode...
return Cat;
}
Сигнатурой метода определен возвращаемый тип Animal, а мы возвращаем Cat, вот как этот метод будет использоваться:
Animal someAnimal = Zoo();
Данный код полностью работоспособен - мы можем возвращать из метода, тип производный от заявленного сигнатурой метода и он будет приведен к базовому.
Мы, фактически, могли бы использовать метод с сигнатурой:
public Cat Zoo()
{
}
Вместо:
public Animal Zoo()
{
}
И не заметили бы этого в вызывающем коде:
Animal someAnimal = Zoo();
Это и есть ковариация - у нас есть метод Zoo, который использует в качестве возвращаемого значения тип Animal, но вместо него, мы можем использовать метод с возвращаемым значением типа Cat. Можно сказать, что сигнатура метода:
public Cat Zoo()
{
}
приводится к сигнатуре:
public Animal Zoo()
{
}
или можно записать так:
Zoo<Cat> приводится к Zoo<Animal>, где Cat наследуется от Animal
И вы можете запомнить первое правило: Ковариация разрешена для возвращаемого значения метода.
Теперь, другой пример, представьте сигнатуру метода:
public void Zoo(Animal param)
{
}
И код его вызова:
var cat = new Cat();
Zoo(cat);
Данный код, тоже полностью работоспособен - мы можем передать в качестве параметра производный тип и он безболезненно будет приведен к базовому и метод нормально отработает.
Мы можем вместо сигнатуры:
public void Zoo(Animal param)
{
}
использовать сигнатуру:
public void Zoo(Cat param)
{
}
Это - контрвариация. У нас есть метод Zoo, который в качестве параметра использует тип Animal и мы можем использовать его вместо метода Zoo, который в качестве параметра принимает Cat, то есть, можно сказать, что сигнатура метода:
public void Zoo(Animal param)
{
}
приводится к сигнатуре:
public void Zoo(Cat param)
{
}
или это можно записать так:
Zoo<Animal> приводится к Zoo<Cat>, где Cat наследуется от Animal
И запомнить второе правило: Контрвариация разрешена для параметров метода
Весь смысл ковариации и контрвариации в C# сводится к двум утверждениям:
- Из метода мы можем возвращать производные типы (наследников) от заявленной сигнатуры метода - и это будет ковариация.
- При вызове метода, мы можем передавать в него производные типы (наследников) от тех, что заявлены сигнатурой метода - и это будет контрвариация.
Оба эти случая, используют вполне привычное приведение производного класса к родительскому.
Ковариация и контрвариация в делегатах
Примеры выше несколько умозрительные, давайте попробуем ковариацию и контрвариацию, используя делегаты.
Код примера с ковариацией:
namespace ConsoleApplication
{
class Animal
{
}
class Cat : Animal
{
}
class Program
{
delegate Animal delegateAnimal();
static void Main(string[] args)
{
var param = new Program();
delegateAnimal dA = MethodCat;
Animal result = dA();
}
static Cat MethodCat()
{
return new Cat();
}
}
}
В переменную с типом делегата delegate Animal delegateAnimal();
- возвращаемое значение Animal
Мы помещаем метод с сигнатурой:
Cat MethodCat()
{
return new Cat();
}
Возвращаемое значение Cat.
Пример с контрвариацией используя делегат, предлагаю сделать самостоятельно в качестве домашней работы.
Логичным шагом дальше была бы возможность присвоить переменной с типом delegate Animal delegateAnimal();
переменную с типом delegate Cat delegateCat();
но C# не поддерживает ковариацию и контрвариацию используя переменные с типами обычных делегатов, зато поддерживает используя обобщенные делегаты.
Ковариация и контрвариация в обобщенных делегатах
Рассмотрим обобщенный делегат Func<out TResult>
- этот делегат используется для методов с сигнатурой:
TResult SomeMethod()
{
}
Перепишем пример выше, используя этот делегат:
namespace ConsoleApplication
{
class Animal
{
}
class Cat : Animal
{
}
class Program
{
static void Main(string[] args)
{
Func<Animal> fA = MethodCat;
Func<Cat> fC = MethodCat;
fA = fC; // работает
// fC = fA; // ошибка компиляции
Animal result = fA(); // вызывается метод MethodCat(), а возвращаемое значение с типом Cat приводится к Animal
}
static Cat MethodCat()
{
return new Cat();
}
static Animal MethodAnimal()
{
return new Animal();
}
}
}
Как видно из примера, мы можем как присвоить переменной с типом обобщенного делегата Func<Animal>
, метод с типом возвращаемого значения Cat: Func<Animal> fA = MethodCat;
, так и можем переменной с типом Func<Animal>
присвоить переменную с типом Func<Cat>
:
fA = fC;
То есть привести переменную с типом Func<Cat>
к переменной с типом Func<Animal>
. Компилятор позволяет нам произвести такое присваивание из-за ключевого слова out
у типа обобщения делегата Func:
delegate Func<out T>();
Это ключевое слово, говорит компилятору, что тип T будет использоваться только в качестве возвращаемого значения метода, а это значит для него разрешена ковариация (вместо типа возвращаемого значения мы можем возвратить производный тип).
Теперь рассмотрим другой стандартный обобщенный делегат: Action<T>
Его сигнатура: delegate Action<in T>(T param)
Он используется для методов без возвращаемого значения и одним параметром:
void SomeMethod(T param)
{
}
А так как в параметры метода мы можем передавать тип производный от требуемого, тот этот делегат должен поддерживать контрвариацию.
Пример:
namespace ConsoleApplication
{
class Animal
{
}
class Cat : Animal
{
}
class Program
{
static void Main(string[] args)
{
Action<Animal> fA = MethodAnimal;
Action<Cat> fC = MethodAnimal;
fC = fA; // работает
// fA = fC; //ошибка компиляции
fC(new Cat()); // вызывается метод MethodAnimal(), а Cat приводится к Animal
}
static void MethodCat(Cat param)
{
}
static void MethodAnimal(Animal param)
{
}
}
}
Мы можем как присвоить делегату с типом Action<Cat>
метод с типом принимаемого параметра Animal
, так и можем переменной с типом Action<Cat>
присвоить переменную с типом Action<Animal>
- то есть привести Action<Animal>
к Action<Cat>
- что является контрвариацией.
Компилятор разрашает нам это сделать из-за ключевого слова in
в объявлении делегата:
delegate void Action(in T param);
Оно говорит компилятору, что данный тип будет использоваться только в качестве параметра метода - а значит вместо этого типа мы можем использовать производный.
Пример использования:
-
Ковариация в обобщенных делегатах, позволяет нам вместо делегата
Func<object>
использовать делегатFunc<string>
. -
Контрвариация, вместо обобщенного делегата
Action<string>
, позволяет использовать делегатAction<object>
.
При написании своих обобщенных делегатов, вы тоже можете использовать ключевые слова in и out
у типов обобщения - и это автоматом включит ковариацию и контрвариацию для этих делегатов.
Самостоятельная работа:
А что насчет делегата Func<in T, out TResult>
? - как для него будет работать ковариация и контрвариация?
Ковариация и контрвариация в обобщенных интерфейсах
Ковариацию обобщенного интерфейса, мы уже видели - в самом начале, когда переменной с типом IEnumerable<object>
мы присвоили переменную с типом IEnumerable<string>
, работает она точно также как и в обобщенных делегатах благодаря использованию ключевого слова в типе обобщения:
IEnumerable<out T>
И обозначает, что данный тип T в этом интерфейсе будет использоваться только в качестве возвращаемого значения методов, давайте изучим этот интерфейс:
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
смотрим интерфейс IEnumerator:
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
T Current { get; }
}
T - это возвращаемое значение свойства Current (свойства фактически реализуются как методы, свойство только с get - идентично методу, который не имеет входных параметров, а только возвращаемое значение), настоящий тип возвращаемого значения будет string
, но оно будет приводиться к типу Object
.
Пример контрвариация в стандартных обобщенных интерфейсах, найти сложнее, поэтому напишем его сами:
namespace ConsoleApplication
{
class Animal
{
}
class Cat : Animal
{
}
interface IZoo<in T>
{
void PutZoo(T zoo);
}
class ZooAnimal : IZoo<Animal>
{
public void PutZoo(Animal zoo)
}
class ZooCat : IZoo<Cat>
{
public void PutZoo(Cat zoo)
}
class Program
{
static void Main(string[] args)
{
IZoo<Animal> zooAnimal = new ZooAnimal();
IZoo<Cat> zooCat = zooAnimal;
zooCat.PutZoo(new Cat());
}
}
}
Переменной с типом IZoo<Cat>
мы можем присвоить переменную с типом IZoo<Animal>
:
IZoo<Animal> zooAnimal = new ZooAnimal();
IZoo<Cat> zooCat = zooAnimal;
При вызове метода PutZoo(new Cat())
будет вызван метод PutZoo()
класса ZooAnimal, который принимает параметр с типом Animal
- мы же передаем Cat
, который может быть приведен к типу Animal
, так как является его наследником.
Ограничения
Ковариация и контрвариация не разрешена для обобщенных классов
Компилятор не разрешит вам определить тип обобщения у класса используя ключевые слова in/out
и соотвественно не будет поддерживать ковариацию и контрвариацию для этого класса.
Ковариация и контрвариация работает только для ссылочных типов
Вообще наследование структур - не поддерживается языком, поэтому сделать какую-то иерархию структур, которые можно было бы приводить друг к другу не получится при всем желании. Но язык поддерживает такую возможность как упаковка, когда значимый тип мы можем упаковать в тип object
:
object obj = 5;
Однако делать приведения типа:
IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;
Нельзя.
Ковариация в массивах
Ковариация в массивах - это поведение доставшееся по наследству.
Массив с типом Cat[]
мы можем привести к массиву с типом Animal[]
Cat[] catArr =Cat[5];
Animal[] animalArr = catArr;
Итог
Ковариация и контрвариация в C# поддерживается для обобщенных делегатов и интерфейсов, при этом тип обобщения может быть только ссылочным типом.
Ковариация позволяет приводить производный тип обобщения к базовому и работает только для типов использующихся в качестве выходного значения методов, благодаря тому, что возвращать мы можем тип производный от того, что требуется и он будет приведен к требуемому родительскому типу.
Контрвариация позволяет приводить базовый тип обобщения к производному и работает только для типов использующихся в качестве параметров методов, благодаря тому, что передать в метод мы можем тип производный от того, что требуется и он будет приведен к требуемому родительскому типу.