Почему «ref» и «out» не поддерживают полиморфизм? - PullRequest
122 голосов
/ 30 июля 2009

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

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.

Ответы [ 10 ]

166 голосов
/ 30 июля 2009

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

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

Почему параметры 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 : * параметры 1060 * нельзя сделать «меньшими».

<ч />

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

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

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

Заключение 3 : * Параметры 1080 * нельзя сделать «меньшими».

<ч />

Можете ли вы передать переменную типа 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 .

29 голосов
/ 30 июля 2009

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

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

B b = new A();
10 голосов
/ 30 июля 2009

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

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

4 голосов
/ 30 июля 2009

Рассмотрим:

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

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

B b = null;
Foo2(ref b);

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

3 голосов
/ 31 марта 2018

В то время как другие ответы кратко объяснили причину такого поведения, я думаю, что стоит упомянуть, что если вам действительно нужно сделать что-то подобное, вы можете выполнить аналогичную функциональность, превратив 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 {}  
}
2 голосов
/ 30 июля 2009

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

0 голосов
/ 20 июня 2018

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

object bla = myVar;
Foo(ref bla);

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

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

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

0 голосов
/ 02 августа 2017

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

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 голосов
/ 14 марта 2012

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

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

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

0 голосов
/ 30 июля 2009

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

Foo2(ref (A)b)
...