Почему «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
.
- Ошибка компилятора: ссылка на вызов неоднозначную
- Почему вы не можете уменьшить видимость метода в подclassе Java?
- Изучение C ++: polymorphism и нарезка
- Сериализация JSON массива с полиморфными объектами
- Что такое polymorphism, для чего он используется и как он используется?
- Полиморфизм в c ++
- В чем преимущество polymorphismа с использованием интерфейса Collection для создания объекта ArrayList?
- Использует ли polymorphism атрибуты classа в Java?
- Когда использовать enums, а когда заменить их classом статическими членами?
- Что такое Тень?
- Как скопировать / создать экземпляр производного classа из указателя в полиморфный базовый class?
- Scala: Как определить «общие» параметры функции?
- Как вызвать base.base.method ()?
=============
UPDATE: я использовал этот ответ в качестве основы для этой записи в блоге:
Почему параметры ref и out не позволяют изменять тип?
См. Страницу блога для получения дополнительных комментариев по этой проблеме. Спасибо за большой вопрос.
=============
Предположим, у вас есть classы Animal
, Mammal
, Reptile
, Giraffe
, Turtle
и Tiger
с очевидными отношениями подclassа.
Теперь предположим, что у вас есть метод void M(ref Mammal m)
. M
может читать и писать m
.
Можете ли вы передать переменную типа
Animal
toM
?
Нет. Эта переменная может содержать 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
toN
?
Хм.
А почему бы не? 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 и проверяет тип внутри и делает много логики
Прошу прощения, если мой ответ повторяется, но другие слишком длинны