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

124

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

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аргументами.

Андреас Греч
источник

Ответы:

169

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

ОБНОВЛЕНИЕ: я использовал этот ответ как основу для этой записи в блоге:

Почему параметры ref и out не допускают изменение типа?

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

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

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

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


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

Нет. Эта переменная может содержать a Turtle, но Mпредполагается, что она содержит только Mammals. А Turtleне а Mammal.

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


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

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

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


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

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

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

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


Вы можете передать переменную типа Animalв 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 : outпараметры нельзя делать «больше».


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

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

Эрик Липперт
источник
6
+1. Отличное объяснение с использованием реальных примеров, которые четко демонстрируют проблемы (например, объяснение с помощью A, B и C затрудняет демонстрацию, почему это не работает).
Грант Вагнер
4
Я испытываю смирение, читая этот мыслительный процесс. Думаю, мне лучше вернуться к книгам!
Скотт Маккензи,
В этом случае мы действительно не можем использовать переменную абстрактного класса в качестве аргументов и передавать объект производного класса !!
Прашант Чолачагудда
Все-таки почему outпараметры нельзя сделать «больше»? Описанная вами последовательность может применяться к любой переменной, а не только к outпеременной параметра. А также читателю необходимо преобразовать значение аргумента в значение, Mammalпрежде чем он попытается получить к нему доступ, Mammalи, конечно же, он может потерпеть неудачу, если он не внимателен
astef
29

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

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

B b = new A();
maciejkow
источник
+1 Сразу по делу и прекрасно объясняет причину.
Rui Craveiro
10

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

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

Алекс Мартелли
источник
4

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

class C : A {}
class B : A {}

void Foo2(ref A a) { a = new C(); } 

B b = null;
Foo2(ref b);

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

Хенк Холтерман
источник
Это скорее неясный предполагаемый тип «b» из-за проблемы с var.
Думаю, в строке 6 вы имели в виду => B b = null;
Алехандро Мираллес
@amiralles - да, это varбыло совершенно неправильно. Исправлена.
Хенк Холтерман,
4

В то время как в других ответах лаконично объясняется причина такого поведения, я думаю, стоит упомянуть, что если вам действительно нужно сделать что-то в этом роде, вы можете выполнить аналогичную функцию, превратив 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<AType> (ref AType a) where AType: A {}  
}
BrendanLoBuglio
источник
2

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

CannibalSmith
источник
0

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

Foo2(ref (A)b)
dlamblin
источник
Не могу этого сделать: «Аргумент ref или out должен быть присваиваемой переменной»
0

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

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);
}

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

Oofpez
источник
0

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

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.

Ян Бойд
источник
0

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

object bla = myVar;
Foo(ref bla);

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

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

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

Шериф Марзук
источник