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

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

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.

Ответ 1

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

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

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

Подробнее см. на странице блога. Спасибо за большой вопрос.

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

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

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


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

Нет. Эта переменная может содержать Turtle, но M будет предполагать, что она содержит только млекопитающих. A 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 на N?

Хм.

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

Неправильно. Правило не "N может писать только N".

Правила кратко:

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

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

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

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

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

Заключение 4: out параметры не могут быть сделаны "большими".


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

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

Ответ 2

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

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

B b = new A();

Ответ 3

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

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

Ответ 4

Рассмотрим:

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

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

B b = null;
Foo2(ref b);

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

Ответ 5

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

Ответ 6

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

Foo2(ref (A)b)

Ответ 7

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

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

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

Ответ 8

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

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.