Четкие правила корректности вариантности


Думаю, всем вам будет интересно узнать точное описание того, как именно мы определяем допустимость ключевых слов ”in” и “out” при объявлении параметров типов в C# 4. Я хочу привести здесь это описание по следующим причинам: (1) ради общего интереса и (2) поскольку в результате попытки создать более читабельную версию этого алгоритма в черновике спецификации C# 4.0 мы допустили некоторые коварные ошибки. Мы работаем над исправлением этих ошибок для финальной версии спецификации, а пока, рассмотрим те определения, на основе которых я работал над реализацией, поэтому они являются точными.

Эти определения практически полностью «позаимствованы» из раздела спецификации CLI о вариантности. Я нахожусь в долгу у авторов этой спецификации за их тщательные и точные определения.

Самое первое, что нам нужно определить, так это три разновидности «корректности» типов. Я хочу определить такие понятия как «ковариантно корректный», «контрвариантно корректный» и «инваринтно корректный», но только применительно к типам. Мы поговорим о том, что делает объявление интерфейсов корректными немного позже. В первую очередь нам нужны только эти определения.

Прежде чем мы перейдем к точному определению, я хочу поговорить о том, что же логически означает фраза «ковариантно корректный». Основная мысль этого понятия сводится к следующему: «рассматриваемый тип не является контрвариантным». И нам, на самом деле, не важно, является он ковариантным или нет. Все что нас интересует, так это то, что если этот тип ковариантно корректен, значит он не контрвариантен. Аналогично, под «контрвариантно корректным» типом, мы подразумеваем тип, который «не является ковариантным».

Это приводит нас к первому «зубодробительному» определению: Тип является «инвариантно корректным» если он одновременно является ковариантно и контрвариантно корректным. Звучит немного глупо: как тип может быть инвариантно корректным только если он является ковариантным и контрвариантным? Но «ковариантно корректен» не значит «ковариантен», это всего лишь значит «точно не контрвариантен». Поэтому, если тип корректен ковариантно и контрвариантно, значит, он точно не является ковариантным и контрвариантным, и поэтому должен быть инвариантным.

Как бы то ни было, мы хотим взять любой конкретный тип и классифицировать его, как ковариантно корректный, контрвариантно корректный или корректный и в том и в другом смысле, т.е. инвариантно корректный. (Нам не нужно беспокоиться о термине для случая «или не один из них», поскольку на практике этот случай не представляет интереса.)

Тип является ковариантно корректным если он является:

1) Классом, структурой, nullable-типом, перечислением, указателем, необобщенным (non-generic) делегатом или необобщенным интерфейсным типом.

Звучит разумно. Помните, что мы под этим понятием подразумеваем «не является контрвариантным». Классы и структуры, не обобщенные (в этом случае они явно не контрвариантны) или обобщенные (в этом случае, опять же, они не являются контрвариантными, поскольку мы не поддерживаем вариантность для классов или структур). Nullable-типы – это всего лишь синонимы для типов Nullable<T>, которые являются структурами, поэтому не являются вариантными. Перечисления и указатели являются необобщенными, поэтому также не являются вариантными. Необобщенные делегаты и интерфейсы по определению не являются обобщенными, поэтому не являются вариантными. Это простые случаи.

2) Типом массива T[], где T является корректно ковариантным типом.

Поскольку массивы в C# ковариантны, но не контрвариантны, мы можем говорить о ковариантной корректности массивов. Не забывайте, что «ковариантно корректный» означает «не контрвариантный».

3) Обобщенным типом параметра типа, если он не объявлен контрвариантным.

Обобщенные типы параметров типа естественно являются типами с точки зрения компилятора. Если вы находитесь внутри объявления обобщенного интерфейса, значит, вы можете использовать обобщенные типы параметров в качестве типов. В этом контексте такие типы являются ковариантно корректными, если они не объявлены контрвариантными (с помощью ключевого слова “in”).

4) Интерфейс или делегат вида X<T1, …, Tk> может быть ковариантно корректным. Для определения этого мы проверяем каждый тип аргумента независимо, является ли он ковариантным (с помощью ключевого слова “out”), контрвариантным (с помощью ключевого слова “in”) или интвариантным (без обоих этих ключевых слов) в зависимости от объявления соответствующего типа параметра. Если i-й обобщенный параметр объявлен ковариантным, тогда тип Ti должен быть ковариантно корректным. Если он объявлен контрвариантным, тогда тип Ti должен быть контрвариантно корректным. Если он объявлен инвариантным, тогда Ti должен быть инвариантно корректным.

Можно сказать, что ковариантная корректность сохраняет направление корректности: ковариантные параметры должны быть ковариантно корректными, контрвариантные параметры должны быть контрвариантно корректными.

Хорошо. Я надеюсь, это было относительно просто. Правила контрвариантной корректности аналогичны. Можно сказать, из «реверсивной» природы контрвариантности следует, что направление изменяется на противоположное особенным образом:

Тип является контрвариантно корректным, если он является:

1) Классом, структурой, Nullable-типом, перечислением, указателем, необобщенным делегатом или необобщенным интерфейсным типом.

2) Массивом типа T[], где T является контрвариантно корректным типом. Массивы являются ковариантными по типу их элементов. Ковариантность сохраняет направление вариантности. Поэтому, для контрвариантной корректности массива он (ковариантный по типу его элементов) должен быть контрвариантно корректным по типу его элементов.

3) Обобщенным типом параметра типа, если он не объявлен ковариантным.

Помните, «контрвариантно корректный» означает «не ковариантный». Но это не означает «контрвариантный».

4) Интерфейс или делегат вида X<T1, …, Tk> может быть контрвариантно корректным. Если i-й обобщенный параметр объявлен контрвариантным, тогда тип Ti должен быть ковариантно корректным. Если этот тип объявлен ковариантным, тогда Ti должен быть контрвариантно корректным. Если он объявлен инвариантным, тогда Ti должен быть инвариантно корректным.

Можно сказать, что, контрвариантная корректность изменяет направление корректности на противоположное.

Теперь, я надеюсь, вам понятно, почему мы захотели переписать эти определения для спецификации в более читабельном виде. И, наверное, теперь вам понятно, почему мы случайно допустили ошибку при описании: с такой логикой мозги свернуть пара пустяков. Хорошо. Теперь, после всего этого, мы можем дать определение корректности интерфейса. Интерфейс должен выполнять следующие условия:

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

* Каждый формальный тип обобщенного параметра всех методов интерфейса должен быть контрвариантно корректным. (Инвариантным, для out- и ref-параметров.)

* Для всех обобщенных методов интерфейса, каждое ограничение типа параметра обобщенного метода должно быть контрвариантно корректным.

* Все базовые интерфейсы должны быть ковариантно корректными.

* Тип свойства или индексатора должен быть ковариантно корректным при наличии get-метода и контрвариантно корректным при наличии set-метода.

* Любые типы формальных параметров индексатора должны быть контрвариантно корректными.

* Типы делегатов всех событий должны быть контрвариантно корректными.

Первые два пункта достаточно просты: возвращаемые типы «выходят» (go out), поэтому они должны быть ковариантно корректными, а формальные типы параметров «входят» (go in), поэтому должны быть контрвариантно корректными. А что насчет третьего пункта? Как ограничения типов параметров обобщенных методов должны взаимодействовать с корректностью интерфейса? Давайте, отбросим третье правило и посмотрим, что пойдет не так.

interface I<out T>

{

void M<U>() where U : T;

// некорректно. Тип T должен быть контрвариантно корректным, но это 

// противоречит ограничению параметра типа на ковариантность

// Давайте пока пропустим это и продемонстрируем ошибку

}

class C<T> : I<T> { public void M<U>() {} }

// ограничение типа параметра обобщенного метода неявно 

// наследовано от интерфейса и не указано заново

I<Giraffe> igiraffe = new C<Giraffe>(); // C<T> реализует интерфейс I<T>

I<Animal> ianimal = igiraffe; // интерфейс ковариантен по типу T

ianimal.M<Turtle>(); // удовлетворяет ограничение, что U должен относится к типу Animal.

Итак, ianimal, на самом деле является экземпляром класса C<Giraffe>. Метод M<U> класса C<Giraffe> наследует требование о том, что тип U должен быть производным от Giraffe. Класс Turtle (черепаха) не наследует классу Giraffe (жираф), что нарушает ограничение метода M<U>. Единственное место, где мы можем отловить это ограничение – это объявление, поскольку все остальные шаги являются совершенно корректными. Поэтому, ограничения не могут быть ковариантными. Но если их сделать контрвариантными (или инвариантными), тогда все будет прекрасно работать. Например, давайте зададим контрвариантное ограничение параметра типа:

interface I2<in T> // в этот раз тип T контрвариантный
{
void M<U>() where U : T; 
} 

class C2<T> : I2<T> { public void M<U>() {} }
I2<Animal> i2animal = new C2<Animal>(); // C2<T> реализует I2<T>
I2<Mammal> i2mammal = i2animal; // интерфейс контрвариантный по T
i2mammal.M<Giraffe>(); // удовлетворяет ограничению, что U должен быть типом Animal. 

Теперь все в порядке. Механизм проверки ограничений времени компиляции проверяет, что класс Giraffe является классом Mammal; во время выполнение этот тип должен быть типом Animal, а это условие выполняется автоматически, поскольку тип является классом Mammal.

Правила объявления делегатов являются упрощенным подмножеством правил объявления интерфейсов. Для того, чтобы объявления делегата было корректным, тип возвращаемого значения должен быть ковариантно корректным (или void), формальные типы обобщенных параметров должны быть контрвариантно корректными (или инвариантными для ref- или out-параметров) и любые ограничения параметров типа должны быть контрвариантно корректными.

Оригинал статьи

Skip to main content