Dlaczego „ref” i „out” nie obsługują polimorfizmu?

124

Wykonaj następujące czynności:

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

Dlaczego występuje powyższy błąd kompilacji? Dzieje się tak zarówno refi outargumentów.

Andreas Grech
źródło

Odpowiedzi:

169

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

AKTUALIZACJA: Użyłem tej odpowiedzi jako podstawy dla tego wpisu na blogu:

Dlaczego parametry ref i out nie pozwalają na zmianę typu?

Więcej komentarzy na ten temat znajdziesz na blogu. Dzięki za świetne pytanie.

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

Załóżmy, że masz zajęcia Animal, Mammal, Reptile, Giraffe, Turtlei Tiger, z oczywistych relacji podklasy.

Teraz przypuśćmy, że masz metodę void M(ref Mammal m). Mpotrafi zarówno czytać, jak i pisać m.


Czy możesz przekazać zmienną typu Animaldo M?

Nie. Ta zmienna może zawierać a Turtle, ale Mzakłada, że ​​zawiera tylko ssaki. A Turtlenie jest Mammal.

Wniosek 1 : refparametrów nie można „zwiększyć”. (Jest więcej zwierząt niż ssaków, więc zmienna staje się „większa”, ponieważ może zawierać więcej rzeczy).


Czy możesz przekazać zmienną typu Giraffedo M?

Nie. MMoże pisać do mi Mmoże chcieć napisać Tigerdo m. Teraz wstawiłeś a Tigerdo zmiennej, która jest faktycznie typu Giraffe.

Wniosek 2 : refparametrów nie można uczynić „mniejszymi”.


A teraz zastanów się N(out Mammal n).

Czy możesz przekazać zmienną typu Giraffedo N?

Nie. NMoże pisać do ni Nmoże chcieć napisać Tiger.

Wniosek 3 : outparametrów nie można uczynić „mniejszymi”.


Czy możesz przekazać zmienną typu Animaldo N?

Hmm.

Czemu nie? Nnie może czytać n, może tylko pisać, prawda? Piszesz a Tigerdo zmiennej typu Animali wszystko gotowe, prawda?

Źle. Zasada nie brzmi: „ Nmożna tylko pisać n”.

Zasady są w skrócie:

1) Nmusi napisać nprzed Npowrotem normalnie. (Jeśli Nrzuca, wszystkie zakłady są anulowane.)

2) Nmusi coś napisać, nzanim coś przeczyta n.

To pozwala na taką sekwencję wydarzeń:

  • Zadeklaruj pole xtypu Animal.
  • Przekaż xjako outparametr do N.
  • Nzapisuje Tigerdo n, który jest aliasem dla x.
  • W innym wątku ktoś pisze Turtledo x.
  • Npróbuje odczytać zawartość ni Turtleznajduje zmienną typu, jak uważa Mammal.

Oczywiście chcemy uczynić to nielegalnym.

Wniosek 4 : outparametrów nie można uczynić „większymi”.


Wniosek końcowy : Ani parametry, refani outparametry nie mogą różnić się typami. W przeciwnym razie należy złamać weryfikowalne bezpieczeństwo typów.

Jeśli te kwestie w podstawowej teorii typów Cię interesują, rozważ przeczytanie mojej serii o tym, jak działają kowariancja i kontrawariancja w C # 4.0 .

Eric Lippert
źródło
6
+1. Świetne wyjaśnienie za pomocą przykładów z prawdziwego świata, które jasno pokazują problemy (np. - wyjaśnienie za pomocą A, B i C utrudnia wykazanie, dlaczego to nie działa).
Grant Wagner
4
Czuję pokorę czytając ten proces myślowy. Myślę, że lepiej wrócę do książek!
Scott McKenzie,
W tym przypadku naprawdę nie możemy użyć zmiennej klasy abstrakcyjnej jako argumentów i przekazać jej pochodnego obiektu klasy !!
Prashant Cholachagudda
Jednak dlaczego outnie można zwiększyć parametrów? Opisaną sekwencję można zastosować do dowolnej zmiennej, nie tylko outzmiennej parametrycznej. A także czytelnik musi rzucić wartość argumentu, Mammalzanim spróbuje uzyskać do niego dostęp, Mammali oczywiście może się nie powieść, jeśli nie jest rozważny
astef
29

Ponieważ w obu przypadkach musisz mieć możliwość przypisania wartości do parametru ref / out.

Jeśli spróbujesz przekazać b do metody Foo2 jako odniesienie, aw Foo2 spróbujesz przypisać a = new A (), byłoby to nieprawidłowe.
Z tego samego powodu, dla którego nie możesz pisać:

B b = new A();
maciejkow
źródło
+1 Prosto do rzeczy i doskonale wyjaśnia powód.
Rui Craveiro
10

Zmagasz się z klasycznym problemem OOP kowariancji (i kontrawariancji), patrz wikipedia : chociaż ten fakt może przeciwstawiać się intuicyjnym oczekiwaniom, matematycznie niemożliwe jest zezwolenie na zastępowanie klas pochodnych zamiast podstawowych dla zmiennych (przypisywalnych) argumentów (i także pojemniki, których przedmioty można przypisać z tego samego powodu), przy jednoczesnym poszanowaniu zasady Liskova . Dlaczego tak jest, zostało naszkicowane w istniejących odpowiedziach i dokładniej zbadane w tych artykułach wiki i linkach do nich.

Języki OOP, które wydają się to robić, pozostając tradycyjnie statycznie bezpiecznymi typami, to „oszukiwanie” (wstawianie ukrytych dynamicznych kontroli typów lub wymaganie sprawdzenia WSZYSTKICH źródeł w czasie kompilacji); Podstawowym wyborem jest: albo zrezygnuj z tej kowariancji i zaakceptuj zdziwienie praktyków (tak jak robi to C # tutaj), albo przejdź do dynamicznego podejścia do pisania (jak zrobił to pierwszy język OOP, Smalltalk), albo przejdź do niezmienności (jedno- przypisanie), podobnie jak języki funkcjonalne (w niezmienności można wspierać kowariancję, a także unikać innych powiązanych zagadek, takich jak fakt, że nie można mieć prostokątnej podklasy Square w świecie zmiennych danych).

Alex Martelli
źródło
4

Rozważać:

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

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

B b = null;
Foo2(ref b);

Naruszyłoby to bezpieczeństwo typów

Henk Holterman
źródło
Jest to raczej niejasny wywnioskowany typ „b” ze względu na zmienną, która jest tam problemem.
Myślę, że w linii 6 miałeś na myśli => B b = null;
Alejandro Miralles
@amiralles - tak, to varbyło całkowicie błędne. Naprawiony.
Henk Holterman
4

Podczas gdy inne odpowiedzi zwięźle wyjaśniły powody tego zachowania, myślę, że warto wspomnieć, że jeśli naprawdę potrzebujesz zrobić coś takiego, możesz osiągnąć podobną funkcjonalność, przekształcając Foo2 w metodę ogólną, jako taką:

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
źródło
2

Ponieważ podanie Foo2a ref Bspowodowałoby zniekształcenie obiektu, ponieważ Foo2wie tylko, jak wypełnić Aczęść B.

CannibalSmith
źródło
0

Czy nie jest to, że kompilator mówi ci, że chciałby, abyś jawnie rzutował obiekt, aby mieć pewność, że wiesz, jakie są twoje zamiary?

Foo2(ref (A)b)
dlamblin
źródło
Nie mogę tego zrobić, „Argument ref lub out musi być zmienną
0

Ma to sens z punktu widzenia bezpieczeństwa, ale wolałbym, aby kompilator dał ostrzeżenie zamiast błędu, ponieważ istnieją uzasadnione zastosowania obiektów polimoficznych przekazywanych przez odniesienie. na przykład

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

To się nie skompiluje, ale czy zadziała?

Oofpez
źródło
0

Jeśli użyjesz praktycznych przykładów dla swoich typów, zobaczysz to:

SqlConnection connection = new SqlConnection();
Foo(ref connection);

A teraz masz swoją funkcję, która przyjmuje przodka ( tj. Object ):

void Foo2(ref Object connection) { }

Co może być w tym złego?

void Foo2(ref Object connection)
{
   connection = new Bitmap();
}

Właśnie udało Ci się przypisać Bitmapdo swojego SqlConnection.

To nie dobrze.


Spróbuj ponownie z innymi:

SqlConnection conn = new SqlConnection();
Foo2(ref conn);

void Foo2(ref DbConnection connection)
{
    conn = new OracleConnection();
}

Wypchałeś OracleConnectiongórną część swojego SqlConnection.

Ian Boyd
źródło
0

W moim przypadku moja funkcja przyjęła obiekt i nie mogłem nic wysłać, więc po prostu to zrobiłem

object bla = myVar;
Foo(ref bla);

I to działa

Mój Foo jest w VB.NET i sprawdza typ wewnątrz i wykonuje dużo logiki

Przepraszam, jeśli moja odpowiedź jest podwójna, ale inne były zbyt długie

Shereef Marzouk
źródło