Почему в ref и out параметрах нет вариантности типов?

Вот хороший вопрос со StackOverflow:

Если у вас есть метод, принимающий «X», то вы должны передавать выражение типа X или что-то, приводимое к X. Скажем, выражение производного от X типа. Но если у вас есть метод, принимающий «ref X», то вы обязаны передавать ссылку на переменную типа X, точка. Почему это? Почему бы не разрешить типу варьироваться, как мы делаем для не-ref вызовов?

Предположим, что у вас есть классы Животное, Млекопитающее, Рептилия, Жираф, Черепаха и Тигр, с очевидными отношениями наследования.

Теперь предположим, что у вас есть метод void M(ref Млекопитающее m). M может как читать, так и писать m. Можно ли передать в M переменную типа Животное? Нет. Это было бы небезопасно. Такая переменная может ссылаться на Черепаху, но M предполагает, что там могут быть только Млекопитающие. Черепаха – не млекопитающее.

Вывод 1: Ref-параметры нельзя делать «больше». (Животных больше, чем млекопитающих, так что переменная становится «больше» потому, что в неё входит больше разных существ)

Можно ли передать в M переменную типа Жираф? Нет. M может записывать в m, и может захотеть записать туда экземпляр Тигра. Теперь вы засунули Тигра в переменную, которая имеет тип Жираф.

Вывод 2: Ref-параметры нельзя делать «меньше».

Теперь рассмотрим N(out Млекопитающее n).

Можно ли передать в N переменную типа Жираф? Нет. Как и в нашем предыдущем примере, N может записывать в n, и N может захотеть записать туда Тигра.

Вывод 3: Out-параметры нельзя делать «меньше».

Можно ли передать в N переменную типа Животное?

Хмм.

Ну, почему бы и нет? N не может читать из n, он может туда только писать, верно? Вы записываете Тигра в переменную типа Животное и всё в порядке, так?

Нет. Правило не в том, что «N может только записывать в n». Правила, вкратце, таковы:

1) N обязан записать в n перед тем, как выполнить нормальный возврат (если N бросает исключение, то с него взятки гладки)

2) N обязан записать что-то в n перед тем, как что-то оттуда прочитать.

Это позволяет такую последовательность событий:

  • Объявляем поле x типа Животное.
  • Передаём x как out-параметр в N
  • N пишет Тигра в n, который является псевдонимом для x.
  • В другом потоке, кто-то записывает в x Черепаху.
  • N пытается читать содержимое n, и обнаруживает Черепаху в том, что, по его мнению, является переменной типа Млекопитающее.

Этот сценарий – использование многопоточности для записи в переменную, у которой есть псевдоним, – ужасен и вам ни в коем случае не стоит его реализовывать, но он возможен.

UPDATE: Комментатор Павел Минаев верно подметил, что нет нужды в многопоточности для нанесения увечий. Мы можем заменить четвёртый шаг на

  • N делает вызов метода, который прямо или косвенно заставляет некоторый код записать в x Черепаху.

Независимо от того, каким образом может измениться содержимое переменной, мы явно хотим сделать нарушение системы типов незаконным.

Вывод 4: Out-параметры нельзя делать «больше».

Есть и другой аргумент в пользу этого вывода: «out» и «ref» на самом деле за кулисами совершенно одинаковы. CLR поддерживает только «ref»; «out» - это всего лишь «ref», для которого компилятор навязывает несколько другие правила насчёт того, когда рассматриваемая переменная подвергается определяющему присваиванию. Вот почему запрещено делать перегрузки метода, которые отличаются только out/ref-ностью параметров; CLR неспособен их различить! Так что правила типобезопасности для out вынуждены быть такими же, как и для ref.

Окончательный вывод: Ни ref ни out параметры не позволяют варьировать типы аргументов в местах вызова. Иное бы нарушило верифицируемую безопасность типов.