Чому не "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припустимо, що вона містить лише ссавців. А 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, він може писати лише це, правда? Ви пишете a Tigerдо змінної типу Animalі все готово, правда?

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

Короткі правила:

1) Nповинен написати nдо Nповернення в звичайному режимі. (Якщо Nкидає, всі ставки відключені.)

2) Nмає щось написати до того, nяк воно щось прочитає n.

Це дозволяє цю послідовність подій:

  • Оголосити поле xтипу Animal.
  • Передати xяк outпараметр до N.
  • Nпише a Tigerinto n, що є псевдонімом для x.
  • На іншій темі хтось пише Turtleвx .
  • Nнамагається прочитати вміст nта виявляє a Turtleу тому, що, на його думку, є змінною типу Mammal.

Зрозуміло, ми хочемо зробити це незаконним.

Висновок 4 : outпараметри не можна робити "більшими".


Остаточний висновок : Ні, refніout параметри, параметри не можуть змінюватися залежно від типу. Інакше - порушити безпеку типу, що перевіряється.

Якщо ці проблеми в теорії базових типів вас цікавлять, розгляньте, прочитавши мою серію про те, як працюють коваріація та протиріччя в C # 4.0 .


6
+1. Прекрасне пояснення з використанням прикладів реального класу, які наочно демонструють проблеми (тобто пояснення за допомогою A, B і C ускладнює демонстрацію, чому це не працює).
Грант Вагнер

4
Я відчуваю себе приниженим, читаючи цей мислительний процес. Думаю, я краще повернусь до книг!
Скотт Маккензі

У цьому випадку ми дійсно не можемо використовувати змінну класу Abstract як аргументи та передавати його похідному об'єкту класу !!
Прашант Чолачагудда,

І все-таки, чому outпараметри не можна зробити "більшими"? Ви описана послідовність може бути застосована до будь-якої змінної, не тільки outзмінної параметра. А також читачеві потрібно надати значення аргументу до того, Mammalяк він спробує отримати доступ до нього, Mammalі, звичайно, він може зазнати невдачі, якщо він не буде уважним
astef

29

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

Якщо ви спробуєте передати b в метод Foo2 в якості посилання, а в Foo2 ви спробуєте надати a = new A (), це буде недійсним.
З тієї ж причини ви не можете писати:

B b = new A();

+1 Прямо до суті і прекрасно пояснює причину.
Rui Craveiro

10

Ви боретеся з класичною проблемою коваріації (і протиріччя) OOP , див. Wikipedia : наскільки цей факт може спростовувати інтуїтивні очікування, математично неможливо дозволити підміняти похідні класи замість базових на змінні (присвоювані) аргументи (і також контейнери, предмети яких призначаються з тієї самої причини), дотримуючись принципу Ліскова . Чому це так, накреслено в існуючих відповідях і більш глибоко досліджено в цих статтях вікі та посиланнях з них.

Мови OOP, які, здається, роблять це, залишаючись традиційно статично типовими, є "обманом" (вставлення прихованих перевірок динамічного типу або вимагає перевірки часу компіляції ВСІХ джерел для перевірки); Основний вибір: або відмовитися від цієї коваріації та прийняти загадку практикуючих (як це робить C #), або перейти до динамічного набору тексту (як найперша мова OOP, Smalltalk), або перейти до незмінного ( присвоєння) даних, як це роблять функціональні мови (за незмінності ви можете підтримувати коваріантність, а також уникати інших пов'язаних головоломок, таких як, наприклад, відсутність у прямому прямокутнику квадратного підкласу у світі змінних даних).


4

Поміркуйте:

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

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

B b = null;
Foo2(ref b);

Це порушило б безпеку типу


Це більше неясний висновок типу "b" через вар, в чому проблема.

Я здогадуюсь у рядку 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 {}  
}

2

Оскільки надання призведе до спотвореного об'єкту , тому що знає , як заповнювати частину .Foo2ref BFoo2AB


0

Хіба це не той компілятор, який каже вам, що ви хотіли б, щоб ви явно передали об'єкт, щоб ви могли бути впевнені, що знаєте, які ваші наміри?

Foo2(ref (A)b)

Не можна цього зробити, "Аргумент відмови чи виходу повинен бути змінною",

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

Це не компілюється, але чи буде це працювати?


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, і він перевіряє тип усередині і робить багато логіки

Прошу вибачення, якщо моя відповідь повторюється, але інші були занадто довгими

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.