Корпоративный блог DataArt

avatar

Коварная ковариантность

25 ноября, 2010

Кажется, нет другой фичи языка, которую я бы понимал так долго. Вчера я взял себя в руки, прочёл все посты Эрика Липперта по теме, и, наконец-то, мой разум прояснился, чего и вам желаю.

Я не буду объяснять значение слов — только суть.

Итак.
Все мы знаем, что люди разные нужны. Рыжие, например, нужны точно.

Пусть в основе нашего обзора будет такая иерархия. Человек (Person) — базовый класс.
Ginger — прямой наследник Person.

Заметим, что некоторые люди в силу своего слабого пола способны рожать. Их называют матерями.
А над некоторыми матерями висит злой рок: они в силу своих генов и этого самого рока рожают только определённых детей. Кто-то рожает только девочек, кто-то рожает только музыкантов, а кто-то — только рыжих. Я вспоминаю о Молли Уизли.

Итак, у нас есть интерфейс IMother<TChild>:


interface IMother<TChild>
        where TChild : Person
{
        TChild GiveBirth();
}

Большинство мам способно рожать любых детей и реализуют IMother<Person>. Молли Уизли реализует интерфейс IMother<Ginger>.

А теперь давайте поговорим о ковариантности.
Рыжий мальчик или девочка является человеком.
Рождение рыжего мальчика или девочки является рождением человека, правда?
Запомните: когда направление «подвидности» осталось прежним, речь идёт о ковариантности.

Теперь посмотрим, что появилось нового для поддержки такой абстракции в C#.
Допустим, у нас есть Молли Уизли. Все мы помним, что она — мать, рожающая рыжих.

IMother<Ginger> molly = new MollyWeasley(); 

А является ли она при этом просто матерью?
В реальном мире очевидно, что Молли Уизли определённо является матерью. Кого бы она не рожала, они все являются людьми, т. е. наследниками Person.

А в коде?

IMother<Ginger> molly = new MollyWeasley();
IMother<Person> justMother = molly; // C# 3.0: ошибка компиляции 

C# 3.0 не позволяет нам рассматривать мать рыжих как просто мать. С точки зрения системы типов это понятно — мать рыжих это одно, мать людей — это другое.

В C# 4.0 мы должны определить параметр TChild как ковариантный ключевым словом out:

interface IMother<out TChild>
        where TChild : Person
{
        TChild GiveBirth();
}

Тогда следующий код будет абсолютно рабочим:

IMother<Person> justMother =  new MollyWeasley();
Person someChild = justMother.GiveBirth(); // только мы знаем, что он будет рыжим

Кажется с этим разобрались. Почему ковариантность обозначается словом out?
Чтобы ответить на этот вопрос, ответим на другой: а какое качество позволяет нам её рассматривать как просто матерь? Можем ли мы сделать так, чтобы такое преобразование было невозможным?

Ответ прост: чтобы можно было рассматривать мать рыжих как обобщённую мать, мы должны быть уверены, что она не полагается на особые свойства этих самых рыжих.
Например, она их только рожает.

Мы должны быть уверены, что, если мы её приведём к IMother<Person>, не возникнет никаких ошибок из-за её предрасположенности к рыжим.

Это может звучать непонятно, и поэтому я даю такой пример:

interface IMother<out TChild>
       where TChild : Person
{
        TChild GiveBirth();
        void Punish(TChild child);
}

Мы предоставляем маме дополнительную возможность — наказывать ребенка. Возможно, ближайшие семнадцать лет ей так будет проще бороться с Фредом или Джорджем.

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

class MollyWeasley : IMother<Ginger>
{
        public Ginger GiveBirth()
        {
                // код закрыт природой
        }

        public void Punish(Ginger ginger)
        {
                ginger.LongEars.Pat();
        }
}

Рыжий является человеком.
Наказание рыжего является наказанием человека. Правда?

Нет.
Это легко доказать:

IMother<Person> justMother = new MollyWeasley();
justMother.Punish(new DracoMalfoy()); // OOPS! 

Мать полагается на длинные уши рыжего ребёнка, и попытка наказать Драко Малфоя с треском провалится.
Так мы смогли нарушить правило, разрешающее ковариантность интерфейсов.

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

Пометив TChild модификатором out вы обещаете, что во всех методах TChild будет только возвращаться (либо не будет задействован):

interface IMother<out TChild>;
       where TChild : Person
{
        TChild GiveBirth();
        void Punish(TChild child); // BANG! ошибка компиляции
}

А что нам с того? Бóльшая свобода в выражении своих мыслей.
В следующей серии — пример с контравариантностью, ключевым словом in и киллером.

Рассказать друзьям:

Метки: , , , ,

Leave a Reply