Почему в 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 параметры не позволяют варьировать типы аргументов в местах вызова. Иное бы нарушило верифицируемую безопасность типов.