Когда обобщения(generic) пришли к нам вместе с C# 2.0, они стали одной из лучших возможностей в этом языке. Те, кто когда-либо создавал классы строготипизированных коллекций в C# 1.0 знает, насколько они упростили нам жизнь и уменьшили количество кода. Единственная проблема заключалась в том, что обобщенные типы не следовали тем же правилам наследования, которые были в силе для обычных типов.
Начнем, пожалуй с определения двух простых классов, которые мы будем использовать в этой статье:
В C# 4.0 у нас есть путь заставить это работать — нам нужно просто добавить ключевое слово out к параметру-типу в нашем определении интерфейса IContainer (замечу, что ковариантность в C# 4.0 ограничена интерфейсами и делегатам).
Вам, наверное, интересно как это все реализовано в CLR 4.0? В реализации нет нужды. Обобщенные типы работали в CLR 2.0 и они уже позволяли подобное поведение. Так как C# пытался сохранить безопасность типов — нам такое не дозволялось, но CLR справляется с этим на раз. И, небольшая ремарка — массивы в C# позволяют такое поведение, так что попробуйте. Я надеюсь, что вам понравилась эта статья и новая в этой серии не за горами!
Перевод статьи C# 4.0 New Features Part 3 — Generic Covariance
Кросспост из моего блога
Начнем, пожалуй с определения двух простых классов, которые мы будем использовать в этой статье:
- public class Shape
- {
- }
-
- public class Circle : Shape
- {
- }
* This source code was highlighted with Source Code Highlighter.
Это классическая иерархия классов, которая пока ничего конкретного не делает, но это нам сейчас и не нужно. Теперь определим dummy-класс, который может быть контейнером для любого типа.
- public interface IContainer<T>
- {
- T GetItem();
- }
-
- public class Container<T>: IContainer<T>
- {
- private T item;
-
- public Container(T item)
- {
- this.item = item;
- }
-
- public T GetItem()
- {
- return item;
- }
- }
* This source code was highlighted with Source Code Highlighter.
У нас есть иерархия и контейнер. Теперь посмотрим на то, что мы сейчас не можем делать в текущей версии C# — 3.0.
- static void Main(string[] args)
- {
- IContainer<Shape> list = GetList();
- }
-
- public static IContainer<Shape> GetList()
- {
- return new Container<Circle>(new Circle());
- }
* This source code was highlighted with Source Code Highlighter.
У нас есть метод GetList, у которого тип возращаемого значения определен как IContainer<Shape>, а возвращает он Container<Circle>. Так как Circle наследуется от Shape, а Container реализует интерфейс IContainer может показаться, что это сработает. Но, как вы догадались, C# 3.0 на это не способен.В C# 4.0 у нас есть путь заставить это работать — нам нужно просто добавить ключевое слово out к параметру-типу в нашем определении интерфейса IContainer (замечу, что ковариантность в C# 4.0 ограничена интерфейсами и делегатам).
- public interface IContainer<out T>
- {
- T GetItem();
- }
* This source code was highlighted with Source Code Highlighter.
Данная конструкция говорит компилятору, что тип T ковариантен, что означет то, что любой IContainer<T> будет принимать любой тип эквивалентный или более конкретный, чем T. Как мы видели выше, типом возвращаемого значения был IContainer<Shape>, но если мы поставим параметр out к нашему интерфейсу, то мы легко сможем вернуть и IContainer<Circle>.Так почему же решено использовать ключевое слово out? Все потому, что когда вы определяете параметр-тип как ковариантный вы сможете только вернуть этот тип из интерфейса. Например такая контрукция недопустима:
- public interface IContainer<out T>
- {
- void SetItem(T item);
- T GetItem();
- }
* This source code was highlighted with Source Code Highlighter.
Но почему это не будет работать? Ответ на самом деле очень прост — безопасность типов. Давайте посмотрим на последствия того, что мы сделали:
- static void Main(string[] args)
- {
- IContainer<Shape> container = new Container<Circle>();
- SetItem(container);
- }
-
- public static void SetItem(IContainer<Shape> container)
- {
- container.SetItem(new Square()); // BOOM!!!
- }
* This source code was highlighted with Source Code Highlighter.
Так как T ковариантен и, поэтому, мы можем присвоить Container<Circle> к переменной типа IContainer<Shape>, передав его дальше в наш статический метод SetItem, который принимает параметр типа IContainer<Shape> и, затем мы берем этот парметр и пытаемся добавить переменную типа Square в него. Кажется, что все верно — тип параметра IContainer<Shape> и это дает нам право добавить в него Square. Неверно. Это выражение «взорвется», так как мы пытаемся добавить Square в контейнер, который держит Circle. Поэтому, ключевым словом out, они ограничили ковариантность только одним направлением.Вам, наверное, интересно как это все реализовано в CLR 4.0? В реализации нет нужды. Обобщенные типы работали в CLR 2.0 и они уже позволяли подобное поведение. Так как C# пытался сохранить безопасность типов — нам такое не дозволялось, но CLR справляется с этим на раз. И, небольшая ремарка — массивы в C# позволяют такое поведение, так что попробуйте. Я надеюсь, что вам понравилась эта статья и новая в этой серии не за горами!
Перевод статьи C# 4.0 New Features Part 3 — Generic Covariance
Кросспост из моего блога