Warum unterstützen 'ref' und 'out' den Polymorphismus nicht?


124

Nehmen Sie Folgendes:

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

Warum tritt der oben genannte Fehler bei der Kompilierung auf? Dies geschieht mit beiden refund outArgumenten.

Antworten:


169

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

UPDATE: Ich habe diese Antwort als Grundlage für diesen Blogeintrag verwendet:

Warum erlauben ref- und out-Parameter keine Typvariation?

Weitere Kommentare zu diesem Thema finden Sie auf der Blog-Seite. Danke für die tolle Frage.

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

Nehmen wir an , Sie haben Klassen Animal, Mammal, Reptile, Giraffe, Turtleund Tiger, mit den offensichtlichen Subklassifizieren Beziehungen.

Angenommen, Sie haben eine Methode void M(ref Mammal m). Mkann sowohl lesen als auch schreiben m.


Können Sie eine Variable vom Typ passieren Animalzu M?

Nein. Diese Variable könnte a enthalten Turtle, geht jedoch Mdavon aus, dass sie nur Säugetiere enthält. A Turtleist nicht a Mammal.

Schlussfolgerung 1 : refParameter können nicht "größer" gemacht werden. (Es gibt mehr Tiere als Säugetiere, daher wird die Variable "größer", weil sie mehr Dinge enthalten kann.)


Können Sie eine Variable vom Typ passieren Giraffezu M?

Nein Mkann schreiben m, und Mvielleicht einen schreiben wollen Tigerin m. Jetzt haben Sie ein Tigerin eine Variable eingefügt, die tatsächlich vom Typ ist Giraffe.

Schlussfolgerung 2 : refParameter können nicht "kleiner" gemacht werden.


Nun überlegen Sie N(out Mammal n).

Können Sie eine Variable vom Typ passieren Giraffezu N?

Nein, Nkann schreiben nund Nmöchte vielleicht eine schreiben Tiger.

Schlussfolgerung 3 : outParameter können nicht "kleiner" gemacht werden.


Können Sie eine Variable vom Typ passieren Animalzu N?

Hmm.

Gut, warum nicht? Nkann nicht lesen n, es kann nur darauf schreiben, oder? Du schreibst eine Tigerin eine Variable vom Typ Animalund bist fertig, oder?

Falsch. Die Regel lautet nicht " Nkann nur schreiben n".

Die Regeln sind kurz:

1) Nmuss schreiben, nbevor Nnormal zurückgegeben wird. (Bei NWürfen sind alle Wetten ungültig.)

2) Nmuss etwas schreiben, nbevor es etwas liest n.

Das erlaubt diese Abfolge von Ereignissen:

  • Deklarieren Sie ein Feld xvom Typ Animal.
  • Übergeben Sie xals outParameter N.
  • Nschreibt ein Tigerin n, was ein Alias ​​für ist x.
  • In einem anderen Thread schreibt jemand ein Turtlein x.
  • Nversucht, den Inhalt von zu lesen n, und entdeckt Turtleeine Variable, die seiner Meinung nach vom Typ ist Mammal.

Natürlich wollen wir das illegal machen.

Schlussfolgerung 4 : outParameter können nicht "größer" gemacht werden.


Endgültige Schlussfolgerung : Weder refnoch outParameter dürfen ihre Typen variieren. Andernfalls wird die überprüfbare Typensicherheit verletzt.

Wenn Sie diese Probleme in der grundlegenden Typentheorie interessieren, lesen Sie meine Reihe darüber, wie Kovarianz und Kontravarianz in C # 4.0 funktionieren .


6
+1. Hervorragende Erklärung anhand von Beispielen aus der Praxis, die die Probleme klar veranschaulichen (dh - das Erklären mit A, B und C macht es schwieriger zu demonstrieren, warum es nicht funktioniert).
Grant Wagner

4
Ich fühle mich gedemütigt, diesen Gedankenprozess zu lesen. Ich denke, ich komme besser zu den Büchern zurück!
Scott McKenzie

In diesem Fall können wir die abstrakte Klassenvariable wirklich nicht als Argumente verwenden und das abgeleitete Klassenobjekt weitergeben !!
Prashant Cholachagudda

Warum können outParameter nicht "größer" gemacht werden? Die von Ihnen beschriebene Sequenz kann auf jede Variable angewendet werden, nicht nur auf outParametervariablen. Und auch der Leser muss den Argumentwert auf setzen, Mammalbevor er versucht, darauf zuzugreifen, da er Mammalnatürlich fehlschlagen kann, wenn er nicht
rücksichtsvoll

29

Denn in beiden Fällen müssen Sie dem ref / out-Parameter einen Wert zuweisen können.

Wenn Sie versuchen, b als Referenz an die Foo2-Methode zu übergeben, und in Foo2 versuchen, a = new A () zuzuweisen, ist dies ungültig.
Aus demselben Grund können Sie nicht schreiben:

B b = new A();

+1 Auf den Punkt gebracht und erklärt den Grund perfekt.
Rui Craveiro

10

Sie haben mit dem klassischen OOP-Problem der Kovarianz (und Kontravarianz) zu kämpfen , siehe Wikipedia : So sehr diese Tatsache den intuitiven Erwartungen widerspricht, ist es mathematisch unmöglich, die Ersetzung abgeleiteter Klassen anstelle von Basisklassen durch veränderbare (zuweisbare) Argumente (und) zuzulassen auch Container, deren Gegenstände aus dem gleichen Grund zuweisbar sind), wobei das Liskov-Prinzip weiterhin eingehalten wird . Warum das so ist, wird in den vorhandenen Antworten skizziert und in diesen Wiki-Artikeln und deren Links eingehender untersucht.

OOP-Sprachen, die dies zu tun scheinen, während sie traditionell statisch typsicher bleiben, sind "betrügerisch" (Einfügen versteckter dynamischer Typprüfungen oder Überprüfung aller Quellen zur Überprüfung während der Kompilierung); Die grundlegende Wahl lautet: Geben Sie entweder diese Kovarianz auf und akzeptieren Sie die Verwirrung der Praktizierenden (wie hier C #), oder gehen Sie zu einem dynamischen Typisierungsansatz über (wie es die allererste OOP-Sprache, Smalltalk, getan hat) oder gehen Sie zu unveränderlich über (Single-) Zuweisungsdaten wie funktionale Sprachen (bei Unveränderlichkeit können Sie die Kovarianz unterstützen und auch andere verwandte Rätsel vermeiden, z. B. die Tatsache, dass Sie in einer Welt mit veränderlichen Daten kein Rechteck der quadratischen Unterklasse haben können).


4

Erwägen:

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

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

B b = null;
Foo2(ref b);

Dies würde die Typensicherheit verletzen


Es ist eher der unklare abgeleitete Typ von "b" aufgrund der Var, die dort das Problem ist.

Ich denke in Zeile 6 meinten Sie => B b = null;
Alejandro Miralles

@amiralles - ja, das varwar total falsch. Fest.
Henk Holterman

4

Während die anderen Antworten die Gründe für dieses Verhalten kurz und bündig erklärt haben, ist es meiner Meinung nach erwähnenswert, dass Sie, wenn Sie wirklich etwas in dieser Art tun müssen, ähnliche Funktionen erreichen können, indem Sie Foo2 zu einer generischen Methode machen:

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

Weil das Geben Foo2eines ref Bzu einem fehlerhaften Objekt führen würde, weil Foo2nur ein ATeil davon gefüllt werden kann B.


0

Sagt Ihnen der Compiler nicht, dass Sie das Objekt explizit umwandeln sollen, damit Sie sicher sind, dass Sie Ihre Absichten kennen?

Foo2(ref (A)b)

Kann das nicht tun, "Ein ref oder out Argument muss eine zuweisbare Variable sein"

0

Aus Sicherheitsgründen sinnvoll, aber ich hätte es vorgezogen, wenn der Compiler eine Warnung anstelle eines Fehlers ausgegeben hätte, da es legitime Verwendungen von polymoprhischen Objekten gibt, die als Referenz übergeben werden. z.B

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

Dies wird nicht kompiliert, aber würde es funktionieren?


0

Wenn Sie praktische Beispiele für Ihre Typen verwenden, werden Sie Folgendes sehen:

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

Und jetzt haben Sie Ihre Funktion, die den Vorfahren übernimmt ( dh Object ):

void Foo2(ref Object connection) { }

Was kann daran möglicherweise falsch sein?

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

Sie haben es gerade geschafft, eine zuzuweisen Bitmap , IhremSqlConnection .

Das ist nicht gut.


Versuchen Sie es erneut mit anderen:

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

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

Du hast ein OracleConnectionOver-Top von dir gestopft SqlConnection.


0

In meinem Fall hat meine Funktion ein Objekt akzeptiert und ich konnte nichts einsenden, also habe ich es einfach getan

object bla = myVar;
Foo(ref bla);

Und das funktioniert

Mein Foo ist in VB.NET und es prüft auf Typ im Inneren und macht viel Logik

Ich entschuldige mich, wenn meine Antwort doppelt ist, andere aber zu lang waren

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.