Почему «ref» и «out» не поддерживают polymorphism?

Возьмите следующее:

class A {} class B : A {} class C { C() { var b = new B(); Foo(b); Foo2(ref b); // <= compile-time error: // "The 'ref' argument doesn't match the parameter type" } void Foo(A a) {} void Foo2(ref A a) {} } 

Почему возникает ошибка времени компиляции? Это происходит с аргументами ref и out .

=============

UPDATE: я использовал этот ответ в качестве основы для этой записи в блоге:

Почему параметры ref и out не позволяют изменять тип?

См. Страницу блога для получения дополнительных комментариев по этой проблеме. Спасибо за большой вопрос.

=============

Предположим, у вас есть classы Animal , Mammal , Reptile , Giraffe , Turtle и Tiger с очевидными отношениями подclassа.

Теперь предположим, что у вас есть метод void M(ref Mammal m) . M может читать и писать m .


Можете ли вы передать переменную типа Animal to M ?

Нет. Эта переменная может содержать Turtle , но M предположит, что она содержит только млекопитающих. Turtle – не Mammal .

Заключение 1 : параметры ref не могут быть сделаны «большими». (Есть больше животных, чем млекопитающих, поэтому переменная становится «больше», потому что она может содержать больше вещей.)


Можете ли вы передать переменную типа Giraffe на M ?

Нет. M может написать m , и M может захотеть написать Tiger в m . Теперь вы поставили Tiger в переменную, которая на самом деле относится к типу Giraffe .

Вывод 2 : параметры ref не могут быть сделаны «меньше».


Теперь рассмотрим N(out Mammal n) .

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

N может писать n , и N может захотеть написать Tiger .

Вывод 3 : параметры out не могут быть «меньше».


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

Хм.

А почему бы не? N не может читать из n , он может писать только ему, не так ли? Вы пишете Tiger переменной переменной Animal и все готово, не так ли?

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

Правила вкратце:

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

2) N должен написать что-то в n прежде чем он что-то прочитает из n .

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

  • Объявите поле x типа Animal .
  • Передайте x в качестве out параметра в N
  • N пишет Tiger в n , что является псевдонимом для x .
  • В другой теме кто-то пишет Turtle в x .
  • N пытается прочитать содержимое n и обнаруживает Turtle в том, что, по ее мнению, является переменной типа Mammal .

Очевидно, мы хотим сделать это незаконным.

Вывод 4 : параметры не могут быть сделаны «большими».


Заключительный вывод : ни параметры, ни параметры, ни параметры могут не меняться. Иначе обстоит дело с разрывом поддающейся проверке типа безопасности.

Если вам интересны эти вопросы в теории базового типа, подумайте о том, чтобы прочитать мою серию о том, как ковариация и контравариантность работают в C # 4.0 .

Потому что в обоих случаях вы должны иметь возможность присвоить значение параметру ref / out.

Если вы попытаетесь передать b в метод Foo2 в качестве ссылки, а в Foo2 вы попытаетесь определить a = new A (), это будет недействительным.
По той же причине вы не можете написать:

 B b = new A(); 

Вы боретесь с classической проблемой ковариации (и контравариантности) ООП, см. Wikipedia : поскольку этот факт может игнорировать интуитивные ожидания, математически невозможно разрешить замену производных classов вместо базовых для изменяемых (назначаемых) аргументов (и также контейнеры, чьи позиции можно назначить по той же причине), при этом все еще соблюдая принцип Лискова . Почему это так нарисовано в существующих ответах и ​​более глубоко изучено в этих статьях и ссылках на них.

Языки ООП, которые, как представляется, делают это, оставаясь традиционно статически типичными, «обманывают» (вставляют скрытые проверки динамического типа или требуют проверки во время компиляции всех источников для проверки); фундаментальный выбор: либо отказаться от этой ковариации и принять недоумение практикующих (как это делает C # здесь), либо перейти к подходу с динамической типизацией (как к самому первому языку ООП, Smalltalk), или перейти к неизменяемому (однострочному) присваивание), как и функциональные языки (при неизменности вы можете поддерживать ковариацию, а также избегать других связанных головоломок, таких как тот факт, что вы не можете иметь прямоугольный подclass Rectangle в мире изменяемых данных).

Рассматривать:

 class C : A {} class B : A {} void Foo2(ref A a) { a = new C(); } B b = null; Foo2(ref b); 

Это будет нарушать безопасность типа

Поскольку предоставление Foo2 результате ref B приведет к искаженному объекту, потому что Foo2 знает только, как заполнить часть B

В то время как другие ответы кратко объясняли причины такого поведения, я думаю, стоит упомянуть, что если вам действительно нужно что-то сделать из этого, вы можете выполнить аналогичную функциональность, сделав Foo2 в общий метод, как таковой:

 class A {} class B : A {} class C { C() { var b = new B(); Foo(b); Foo2(ref b); // <= no compile error! } void Foo(A a) {} void Foo2 (ref AType a) where AType: A {} } 

Разве это не значит, что компилятор говорит вам, что вы хотели бы, чтобы я явно бросил объект, чтобы он мог быть уверен, что вы знаете, каковы ваши намерения?

 Foo2(ref (A)b) 

Имеет смысл с точки зрения безопасности, но я бы предпочел, чтобы компилятор выдал предупреждение вместо ошибки, поскольку существует законное использование полиморфных объектов, переданных по ссылке. например

 class Derp : interfaceX { int somevalue=0; //specified that this class contains somevalue by interfaceX public Derp(int val) { somevalue = val; } } void Foo(ref object obj){ int result = (interfaceX)obj.somevalue; //do stuff to result variable... in my case data access obj = Activator.CreateInstance(obj.GetType(), result); } main() { Derp x = new Derp(); Foo(ref Derp); } 

Это не скомпилируется, но будет ли это работать?

Если вы используете практические примеры для своих типов, вы увидите следующее:

 SqlConnection connection = new SqlConnection(); Foo(ref connection); 

И теперь у вас есть ваша функция, которая берет предка ( т.е. Object ):

 void Foo2(ref Object connection) { } 

Что может быть неправильно с этим?

 void Foo2(ref Object connection) { connection = new Bitmap(); } 

Вам просто удалось присвоить Bitmap вашему SqlConnection .

Это не хорошо.


Повторите попытку с другими:

 SqlConnection conn = new SqlConnection(); Foo2(ref conn); void Foo2(ref DbConnection connection) { conn = new OracleConnection(); } 

Вы наполнили OracleConnection поверх своего SqlConnection .

В моем случае моя функция приняла объект, и я не мог отправить ничего, поэтому я просто сделал

 object bla = myVar; Foo(ref bla); 

И это работает

Мой Foo находится в VB.NET и проверяет тип внутри и делает много логики

Прошу прощения, если мой ответ повторяется, но другие слишком длинны

  • Должны ли методы ASP.NET MVC Controller возвращать ActionResult?
  • Полиморфизм с gson
  • Хранить производные объекты classа в переменных базового classа
  • Без-программирование (в основном без условностей)
  • C #: Разница (ковариация / контравариантность) другое слово для polymorphismа?
  • Попробуйте описать polymorphism так просто, как вы можете
  • Вектор объектов, принадлежащих признаку
  • Избегание instanceof в Java
  • В Java, как мне вызвать метод базового classа из переопределяющего метода в производном classе?
  • Почему polymorphism не работает без указателей / ссылок?
  • Зачем использовать polymorphism?
  • Давайте будем гением компьютера.