Warum wird das Schlüsselwort "out" in zwei scheinbar unterschiedlichen Kontexten verwendet?

11

In C # kann das outSchlüsselwort auf zwei verschiedene Arten verwendet werden.

  1. Als Parametermodifikator, in dem ein Argument als Referenz übergeben wird

    class OutExample
    {
        static void Method(out int i)
        {
            i = 44;
        }
        static void Main()
        {
            int value;
            Method(out value);
            // value is now 44
        }
    }
    
  2. Als Typparameter-Modifikator zur Angabe der Kovarianz .

    // Covariant interface. 
    interface ICovariant<out R> { }
    
    // Extending covariant interface. 
    interface IExtCovariant<out R> : ICovariant<R> { }
    
    // Implementing covariant interface. 
    class Sample<R> : ICovariant<R> { }
    
    class Program
    {
        static void Test()
        {
            ICovariant<Object> iobj = new Sample<Object>();
            ICovariant<String> istr = new Sample<String>();
    
            // You can assign istr to iobj because 
            // the ICovariant interface is covariant.
            iobj = istr;
        }
    }
    

Meine Frage ist: warum?

Für einen Anfänger scheint die Verbindung zwischen beiden nicht intuitiv zu sein . Die Verwendung mit Generika scheint nichts mit der Übergabe von Referenzen zu tun zu haben.

Ich habe zuerst gelernt, was outmit der Weitergabe von Argumenten durch Bezugnahme zu tun hat, und dies hat mein Verständnis der Verwendung der Definition von Kovarianz mit Generika behindert.

Gibt es einen Zusammenhang zwischen diesen Verwendungen, den ich vermisse?

Rowan Freeman
quelle
5
Der Zusammenhang ist etwas verständlicher, wenn Sie sich die Verwendung von Kovarianz und Kontravarianz in System.Func<in T, out TResult>Delegaten ansehen .
Rwong
4
Außerdem versuchen die meisten Sprachdesigner, die Anzahl der Schlüsselwörter zu minimieren, und das Hinzufügen eines neuen Schlüsselworts in einer vorhandenen Sprache mit einer großen Codebasis ist schmerzhaft (möglicher Konflikt mit einem vorhandenen Code, der dieses Wort als Namen verwendet)
Basile Starynkevitch

Antworten:

20

Es besteht eine Verbindung, die jedoch etwas locker ist. In C # stehen die Schlüsselwörter ´in´ und ´out´, wie der Name schon sagt, für Eingabe und Ausgabe. Dies ist bei Ausgabeparametern sehr klar, aber weniger sauber, was es mit Vorlagenparametern zu tun hat.

Schauen wir uns das Liskov-Substitutionsprinzip an :

...

Das Liskov-Prinzip stellt einige Standardanforderungen an Signaturen, die in neueren objektorientierten Programmiersprachen übernommen wurden (normalerweise eher auf Klassen- als auf Typenebene; Unterscheidung zwischen nominaler und struktureller Subtypisierung):

  • Kontravarianz von Methodenargumenten im Subtyp.
  • Kovarianz der Rückgabetypen im Subtyp.

...

Sehen Sie, wie Kontravarianz mit Eingabe und Kovarianz mit Ausgabe verbunden ist? Wenn Sie in C # eine Vorlagenvariable mit outkennzeichnen, um sie kovariant zu machen, beachten Sie jedoch, dass Sie dies nur tun können, wenn der angegebene Typparameter nur als Ausgabe angezeigt wird (Funktionsrückgabetyp). Folgendes ist also ungültig:

interface I<out T>
{
  void func(T t); //Invalid variance: The type parameter 'T' must be
                  //contravariantly valid on 'I<T>.func(T)'.
                  //'T' is covariant.

}

Ähnlich, wenn Sie einen Typparameter mit kennzeichnen in, bedeutet dies, dass Sie ihn nur als Eingabe (Funktionsparameter) verwenden können. Folgendes ist also ungültig:

interface I<in T>
{
  T func(); //Invalid variance: The type parameter 'T' must
            //be covariantly valid on 'I<T>.func()'. 
            //'T' is contravariant.

}

Zusammenfassend bedeutet die Verbindung mit dem outSchlüsselwort, dass es sich bei Funktionsparametern um einen Ausgabeparameter handelt , und bei Typparametern bedeutet dies, dass der Typ nur im Ausgabekontext verwendet wird.

System.Funcist auch ein gutes Beispiel, was rwong in seinem Kommentar erwähnt hat. In System.Funcallen Eingabeparametern wird mit flagiert in, und der Ausgabeparameter wird mit flagiert out. Der Grund ist genau das, was ich beschrieben habe.

Gábor Angyal
quelle
2
Gute Antwort! Hat mir etwas gespart ... warte darauf ... tippe! Übrigens: Der Teil des LSP, den Sie zitiert haben, war tatsächlich lange vor Liskov bekannt. Dies sind nur die Standardregeln für die Subtypisierung von Funktionstypen. (Parametertypen sind kontravariant, Rückgabetypen sind kovariant). Die Neuheit von Liskovs Ansatz war a) die Formulierung der Regeln nicht in Bezug auf Ko- / Kontravarianz, sondern in Bezug auf die Substituierbarkeit des Verhaltens (wie durch Vor- / Nachbedingungen definiert) und b) die Geschichtsregel , die es ermöglicht, all diese Überlegungen anzuwenden zu veränderlichen Datentypen, was bisher nicht möglich war.
Jörg W Mittag
10

@ Gábor hat den Zusammenhang bereits erklärt (Kontravarianz für alles, was "rein" geht, Kovarianz für alles, was "raus" geht), aber warum Schlüsselwörter überhaupt wiederverwenden?

Schlüsselwörter sind sehr teuer. Sie können sie nicht als Bezeichner in Ihren Programmen verwenden. Aber es gibt nur so viele Wörter in der englischen Sprache. Manchmal treten Konflikte auf, und Sie müssen Ihre Variablen, Methoden, Felder, Eigenschaften, Klassen, Schnittstellen oder Strukturen umständlich umbenennen, um Konflikte mit einem Schlüsselwort zu vermeiden. Wenn Sie beispielsweise eine Schule modellieren, wie nennen Sie eine Klasse? Sie können es nicht als Klasse bezeichnen, da classes sich um ein Schlüsselwort handelt!

Hinzufügen ein Schlüsselwort in eine Sprache ist noch mehr teuer. Grundsätzlich macht jeder Code, der dieses Schlüsselwort als Bezeichner verwendet, illegal, wodurch die Abwärtskompatibilität überall beeinträchtigt wird.

Die Schlüsselwörter inund waren outbereits vorhanden, sodass sie einfach wiederverwendet werden konnten.

Sie konnten haben kontextuelle Schlüsselwörter hinzugefügt, die nur Schlüsselwörter im Rahmen einer Typparameterliste, aber welche Keywords würden sie gewählt haben? covariantund contravariant? +und -(wie Scala zum Beispiel)? superund extendswie Java? Können Sie sich auf Anhieb daran erinnern, welche Parameter kovariant und kontravariant sind?

Mit der aktuellen Lösung gibt es eine nette Mnemonik: Ausgabetypparameter erhalten das outSchlüsselwort, Eingabetypparameter erhalten das inSchlüsselwort. Beachten Sie die schöne Symmetrie mit den Methodenparametern: Ausgabeparameter erhalten das outSchlüsselwort, Eingabeparameter erhalten das inSchlüsselwort (naja, eigentlich überhaupt kein Schlüsselwort, da die Eingabe die Standardeinstellung ist, aber Sie haben die Idee).

[Hinweis: Wenn Sie sich den Bearbeitungsverlauf ansehen, werden Sie feststellen, dass ich die beiden ursprünglich in meinem Einleitungssatz vertauscht habe. Und ich habe in dieser Zeit sogar eine positive Bewertung bekommen! Dies zeigt nur, wie wichtig diese Mnemonik wirklich ist.]

Jörg W Mittag
quelle
Der Weg, sich an die Gegen- oder Gegenvarianz zu erinnern, besteht darin, zu überlegen, was passiert, wenn eine Funktion in einer Schnittstelle einen Parameter eines generischen Schnittstellentyps verwendet. Wenn man eine hat interface Accepter<in T> { void Accept(T it);};, eine Accepter<Foo<T>>akzeptiert Tals Eingangsparameter , wenn Foo<T>es als Ausgabeparameter übernimmt, und umgekehrt. So contra -variance. Im Gegensatz dazu hat interface ISupplier<out T> { T get();};ein Supplier<Foo<T>>Wille jede Art von Varianz Foo- also Co- Varianz.
Superkatze