Dlaczego słowo kluczowe „out” jest używane w dwóch pozornie odmiennych kontekstach?

11

W języku C # outsłowa kluczowego można używać na dwa różne sposoby.

  1. Jako modyfikator parametru, w którym argument jest przekazywany przez odwołanie

    class OutExample
    {
        static void Method(out int i)
        {
            i = 44;
        }
        static void Main()
        {
            int value;
            Method(out value);
            // value is now 44
        }
    }
  2. Jako modyfikator parametru typu do określania kowariancji .

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

Moje pytanie brzmi: dlaczego?

Dla początkującego połączenie między nimi nie wydaje się intuicyjne . Wydaje się, że użycie z rodzajami nie ma nic wspólnego z przekazywaniem przez referencję.

Najpierw nauczyłem się, co outjest związane z przekazywaniem argumentów przez odniesienie, i to utrudniło mi zrozumienie użycia definicji kowariancji z rodzajami.

Czy istnieje związek między tymi zastosowaniami, których mi brakuje?

Rowan Freeman
źródło
5
Połączenie jest nieco bardziej zrozumiałe, jeśli spojrzysz na użycie kowariancji i kontrawariancji w System.Func<in T, out TResult>delegacie .
rwong
4
Ponadto większość projektantów języków stara się zminimalizować liczbę słów kluczowych, a dodanie nowego słowa kluczowego w istniejącym języku z dużą bazą kodu jest bolesne (możliwy konflikt z istniejącym kodem używającym tego słowa jako nazwy)
Basile Starynkevitch

Odpowiedzi:

20

Istnieje połączenie, jednak jest nieco luźne. W języku C # słowa kluczowe „in” i „out”, jak ich nazwa sugeruje, oznaczają wejście i wyjście. Jest to bardzo jasne w przypadku parametrów wyjściowych, ale mniej czyste, co to ma wspólnego z parametrami szablonu.

Rzućmy okiem na zasadę substytucji Liskowa :

...

Zasada Liskowa nakłada pewne standardowe wymagania na podpisy, które zostały przyjęte w nowszych obiektowych językach programowania (zwykle na poziomie klas, a nie typów; zobacz rozróżnienie nominalne vs. strukturalne dla rozróżnienia):

  • Kontrawariancja argumentów metody w podtypie.
  • Kowariancja typów zwracanych w podtypie.

...

Zobacz, w jaki sposób kontrawariancja jest powiązana z danymi wejściowymi, a kowariancja jest powiązana z danymi wyjściowymi? W języku C # jeśli oflagujesz zmienną szablonu, outaby była kowariantna, ale pamiętaj, że możesz to zrobić tylko wtedy, gdy wspomniany parametr type pojawia się tylko jako wynik (typ zwracanej funkcji). Zatem następujące informacje są nieprawidłowe:

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.

}

Podobnie, jeśli oflagujesz parametr typu in, co oznacza, że ​​możesz go używać tylko jako danych wejściowych (parametr funkcji). Zatem następujące informacje są nieprawidłowe:

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

}

Podsumowując, połączenie ze outsłowem kluczowym polega na tym, że w przypadku parametrów funkcji oznacza to, że jest to parametr wyjściowy , a dla parametrów typu oznacza, że ​​typ jest używany tylko w kontekście wyjściowym .

System.Funcjest również dobrym przykładem tego, co rwong wspomniał w swoim komentarzu. We System.Funcwszystkich parametrach wejściowych są oznaczone in, a parametr wyjściowy jest oznaczone out. Powodem jest dokładnie to, co opisałem.

Gábor Angyal
źródło
2
Niezła odpowiedź! Zaoszczędził mi trochę… poczekaj na to… pisanie! Nawiasem mówiąc: część LSP, którą zacytowałeś, była znana na długo przed Liskovem. To tylko standardowe reguły podtypów dla typów funkcji. (Typy parametrów są sprzeczne, typy zwracane są kowariantne). Nowością w podejściu Liskowa było: a) sformułowanie reguł nie w kategoriach ko / kontrowariancji, ale w kategoriach zastępowalności behawioralnej (zgodnie z warunkami wstępnymi / post) oraz b) reguły historii , która umożliwia zastosowanie całego tego rozumowania na zmienne typy danych, co wcześniej nie było możliwe.
Jörg W Mittag,
10

@ Gábor wyjaśnił już związek (sprzeczność dla wszystkiego, co wchodzi „w”, kowariancja dla wszystkiego, co wychodzi „)”, ale po co w ogóle ponownie używać słów kluczowych?

Cóż, słowa kluczowe są bardzo drogie. Nie możesz ich używać jako identyfikatorów w swoich programach. Ale w języku angielskim jest tylko tyle słów. Czasami więc napotykasz konflikty i musisz niezręcznie zmieniać nazwy swoich zmiennych, metod, pól, właściwości, klas, interfejsów lub struktur, aby uniknąć kolizji ze słowem kluczowym. Na przykład, jeśli modelujesz szkołę, jak nazywasz klasę? Nie można tego nazwać klasą, ponieważ classjest słowem kluczowym!

Dodawanie słowa kluczowego do języka jest jeszcze bardziej kosztowne. Zasadniczo sprawia, że ​​cały kod, który używa tego słowa kluczowego jako identyfikatora, jest nielegalny, przerywając kompatybilność wsteczną w każdym miejscu.

inI outsłowa kluczowe już istnieje, więc może po prostu być ponownie wykorzystane.

Oni mogli dodałem słowo kontekstowych, które są tylko słowa kluczowe w kontekście listy parametrów typu, ale jakie słowa kluczowe by wybrali? covarianti contravariant? +i -(jak na przykład Scala)? superi extendsjak Java? Czy pamiętasz z góry głowy, które parametry są kowariantne i sprzeczne?

W obecnym rozwiązaniu istnieje ładny mnemonik: parametry typu wyjściowego uzyskują outsłowo kluczowe, parametry typu wejściowego uzyskują insłowo kluczowe. Zwróć uwagę na dobrą symetrię z parametrami metody: parametry wyjściowe otrzymują outsłowo kluczowe, parametry wejściowe otrzymują insłowo kluczowe (właściwie nie ma w ogóle słowa kluczowego, ponieważ wejście jest domyślne, ale masz pomysł).

[Uwaga: jeśli spojrzysz na historię edycji, zobaczysz, że tak naprawdę pierwotnie zamieniłem je w zdaniu wprowadzającym. W tym czasie dostałem nawet głos! To po prostu pokazuje, jak ważny jest ten mnemonik.]

Jörg W Mittag
źródło
Sposobem na zapamiętywanie kontrastu i kontrastowości jest rozważenie, co się stanie, jeśli funkcja w interfejsie przyjmuje parametr ogólnego typu interfejsu. Jeśli ktoś ma interface Accepter<in T> { void Accept(T it);};, to Accepter<Foo<T>>zaakceptuje Tjako parametr wejściowy, jeśli Foo<T>zaakceptuje go jako parametr wyjściowy i odwrotnie. Zatem przeciwwariancja . Natomiast będzie miał cokolwiek rodzaju wariancji ma - stąd współpracy -variance. interface ISupplier<out T> { T get();};Supplier<Foo<T>>Foo
supercat