Pull to refresh

C# 4.0 и вариантность

Reading time3 min
Views922
Original author: Joe Seymour
Вариантность всегда была для меня слишком сложной темой, чтобы разобраться в ней. Недавно я делал доклад о новых особенностях C# 4.0 и вариантность была одной из охваченных мной тем. Я хотел бы начать с распространенного сценария, который мне всегда было трудно понять. Возьмем вот такой фрагмент кода:

  1. IList<object> stuff = new List<string>();
* This source code was highlighted with Source Code Highlighter.

Это не будет компилироваться ни в одной из существующих версий .NET Framework. Я всегда пытался понять, почему бы этому не работать. Тип System.String, безусловно, удовлетворяет все требования System.Object. Так почему же это не работает? Обобщение List – это ссылочный тип, что означает что каждый раз когда кто-то сошлется на объект этого типа в коде, он получит указатель на некоторое место в куче. И неважно сколько раз вы используете List, вы всегда получаете ту же самую ссылку т.к. это ссылочный тип. Так вот, давайте разовьем наш предыдущий пример:

  1. IList<object> stuff = new List<string>();
  2. stuff.Add("Joe is awesome");
  3. stuff.Add(9);
  4. stuff.Add(false);
* This source code was highlighted with Source Code Highlighter.


Вот тут и есть проблема! Переменная stuff – это IList<object>. Значит, мы можем добавить в нее любой объект, в том числе int или bool. Но обернитесь на то, что мы только что устроили – .NET Framework хранит ссылку на основное объявление типа, т.е. фактически на List<string>. Мы не можем добавить в список этого типа int или bool, поскольку это не строковые типы. Это кое-что проясняет из того, что меня интересовало так долго. Давайте исследуем теперь такой код:

  1. IEnumerable<object> stuff = new List<string>();
* This source code was highlighted with Source Code Highlighter.


В настоящее время, как выяснено ранее, .NET не позволяет нам такое делать. Подождите, вы считаете, что этот код должен работать? Да, я вынужден согласиться. Такой код должен работать, потому что IEnumerable – это особый тип. Дело в том, что этот тип не принимает модификаций. Единственное, за что IEnumerable отвечает, так это за возврат объектов, т.е. он возвращает объекты (objects coming out). Это сеет некоторые сомнения касательно наших прежних выводов. Вот у нас есть абсолютно корректный сценарий, когда вы должны иметь возможность выполнить присваивание потому что мы не будем делать никаких внутренних модификаций и можем, следовательно, избежать любых таких проблем с типизацией, обнаруженных в предыдущем примере.
Здесь, IEnumerable<string> определенно может выполнить роль IEnumerable<object>, потому что string наследник от object, а значит object — более общий тип. Иначе говоря, тип string ковариантен типу object. Единственная проблема – .NET 3.5 и более ранние версии не имеют способа для разрешения этой ситуации. Переключимся на .NET 4.0. Вот несколько определений интерфейсов:
  1.  
  2. public interface IEnumerable<out T> : IEnumerable
  3. {
  4.   IEnumerator<T> GetEnumerator();
  5. }
  6.  
  7. public interface IEnumerator<out T> : IEnumerator
  8. {
  9.   bool MoveNext();
  10.   T Current { get; }
  11. }
* This source code was highlighted with Source Code Highlighter.


Новое ключевое слово, которое вы видите, “out”, определяет что мы только разрешаем T быть возвращенным из нашего интерфейса. Это разрешит наши предыдущие попытки присвоить IEnumerable<string> к IEnumerable<object> потому что мы просто говорим компилятору, что T никогда не будет передан в наш интерфейс.

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

  1. class Program
  2. {
  3.   void Manipulate(object obj)
  4.   { 
  5.   }
  6.   void ContravariantGoodness()
  7.   {
  8.     Action<object> manipulateObject = Manipulate;
  9.     Action<string> manipulateString = Manipulate;
  10.     manipulateString = manipulateObject;
  11.   }
  12. }
* This source code was highlighted with Source Code Highlighter.


Единственное, что вам здесь не видно, это то, что тип Action в C# 4.0 содержит ключевое слово “in” для обобщенного параметра. Как указано ранее, это не будет работать в .NET 3.5 или более ранних версиях. В этом примере тип object контрвариантен типу string потому что object относится к string как более общий класс к более конкретному. Поскольку в этом случае переменная передается как object, мы можем в самом деле передать все что угодно (почти) в .NET, что удовлетворяет этому контракту, включая тип string. Весьма классная примочка.
Нужно помнить главное, что это все основано на ссылочных типах. Нечто типа такого никогда не будет работать в любом из сценариев:

  1. public struct Parent<T>
  2. {
  3.   public T GetT()
  4.   { 
  5.     ...
  6.   }
  7. }
* This source code was highlighted with Source Code Highlighter.


Я очень раз видеть это в .NET 4.0 и я лично считаю, что это лучшее из добавленных новых функций. Теперь идите и проверьте это.
Tags:
Hubs:
+15
Comments15

Articles