Perché la parola chiave "out" viene utilizzata in due contesti apparentemente diversi?


11

In C #, la outparola chiave può essere utilizzata in due modi diversi.

  1. Come modificatore di parametri in cui un argomento viene passato per riferimento

    class OutExample
    {
        static void Method(out int i)
        {
            i = 44;
        }
        static void Main()
        {
            int value;
            Method(out value);
            // value is now 44
        }
    }
  2. Come modificatore di parametri di tipo per specificare la covarianza .

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

La mia domanda è: perché?

Per un principiante, la connessione tra i due non sembra intuitiva . L'uso con i generici non sembra avere nulla a che fare con il passaggio per riferimento.

Ho prima appreso cosa outc'era in relazione al passare degli argomenti per riferimento, e questo ha ostacolato la mia comprensione dell'uso della definizione della covarianza con i generici.

C'è una connessione tra questi usi che mi manca?


5
La connessione è leggermente più comprensibile se si osserva l'utilizzo della covarianza e della contraddizione in System.Func<in T, out TResult>delegato .
rwong

4
Inoltre, la maggior parte dei progettisti di lingue tenta di ridurre al minimo il numero di parole chiave e l'aggiunta di una nuova parola chiave in una lingua esistente con una base di codice di grandi dimensioni è dolorosa (possibile conflitto con un codice esistente che utilizza quella parola come nome)
Basile Starynkevitch,

Risposte:


20

C'è una connessione, tuttavia è un po 'allentata. In C # le parole chiave "in" e "out", come suggerisce il nome, indicano input e output. Questo è molto chiaro nel caso dei parametri di output, ma meno pulito di ciò che ha a che fare con i parametri del modello.

Diamo un'occhiata al principio di sostituzione di Liskov :

...

Il principio di Liskov impone alcuni requisiti standard per le firme che sono stati adottati in linguaggi di programmazione orientati agli oggetti più recenti (di solito a livello di classi piuttosto che di tipi; vedere il sottotipo nominale rispetto a quello strutturale per la distinzione):

  • La contraddizione degli argomenti del metodo nel sottotipo.
  • Covarianza dei tipi di ritorno nel sottotipo.

...

Vedi come la contraddizione è associata all'input e la covarianza è associata all'output? In C # se si contrassegna una variabile di modello con outper renderla covariante, ma si noti che è possibile farlo solo se il parametro di tipo menzionato appare solo come output (tipo di ritorno della funzione). Quindi quanto segue non è valido:

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.

}

Analogamente se si contrassegna un parametro di tipo con in, ciò significa che è possibile utilizzarlo solo come input (parametro della funzione). Quindi quanto segue non è valido:

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

}

Quindi, per riassumere, la connessione con la outparola chiave è che con parametri di funzione significa che si tratta di un parametro di output e per i parametri di tipo significa che il tipo viene utilizzato solo nel contesto di output .

System.Funcè anche un buon esempio di ciò che ha citato nel suo commento. In System.Functutti i parametri di input sono flaged con ine il parametro di output è flaged con out. Il motivo è esattamente quello che ho descritto.


2
Bella risposta! Mi ha salvato un po '... aspetta ... digitando! A proposito: la parte dell'LSP che hai citato era in realtà conosciuta molto prima di Liskov. Sono solo le regole di sottotipizzazione standard per i tipi di funzione. (I tipi di parametro sono contraddittori, i tipi restituiti sono covarianti). La novità dell'approccio di Liskov era a) formulare le regole non in termini di co- / contravarianza ma in termini di sostituibilità comportamentale (come definita da pre / postcondizioni) eb) la regola della storia , che rende possibile applicare tutto questo ragionamento a tipi di dati mutabili, che prima non era possibile.
Jörg W Mittag,

10

@ Gábor ha già spiegato la connessione (contraddizione per tutto ciò che va "in", covarianza per tutto ciò che "esce"), ma perché riutilizzare le parole chiave?

Bene, le parole chiave sono molto costose. Non puoi usarli come identificatori nei tuoi programmi. Ma ci sono solo tante parole in lingua inglese. Quindi, a volte ti imbatti in conflitti e devi rinominare goffamente variabili, metodi, campi, proprietà, classi, interfacce o strutture per evitare di scontrarti con una parola chiave. Ad esempio, se stai modellando una scuola, come chiami una classe? Non puoi chiamarlo una classe, perché classè una parola chiave!

L'aggiunta di una parola chiave a una lingua è ancora più costosa. Fondamentalmente rende tutto il codice che utilizza questa parola chiave come identificatore illegale, interrompendo la retrocompatibilità ovunque.

Le parole chiave ine outesistevano già, quindi potevano essere riutilizzate.

Avrebbero potuto aggiungere parole chiave contestuali che sono solo parole chiave nel contesto di un elenco di parametri di tipo, ma quali parole chiave avrebbero scelto? covariante contravariant? +e -(come ha fatto Scala, per esempio)? supere extendscome ha fatto Java? Potresti ricordare dalla parte superiore della tua testa quali parametri sono covarianti e contravanti?

Con la soluzione attuale, c'è un bel mnemonico: i parametri del tipo di output ottengono la outparola chiave, i parametri del tipo di input ottengono la inparola chiave. Nota la bella simmetria con i parametri del metodo: i parametri di output ottengono la outparola chiave, i parametri di input ottengono la inparola chiave (beh, in realtà, nessuna parola chiave, poiché l'input è l'impostazione predefinita, ma ottieni l'idea).

[Nota: se guardi la cronologia delle modifiche, vedrai che in realtà ho cambiato le due cose nella mia frase introduttiva. E ho anche ottenuto un voto durante quel periodo! Questo dimostra solo quanto sia importante quel mnemonico.]


Il modo di ricordare la contro-varianza è considerare cosa succede se una funzione in un'interfaccia accetta un parametro di un tipo di interfaccia generico. Se uno ha un interface Accepter<in T> { void Accept(T it);};, Accepter<Foo<T>>accetterà Tcome parametro di input se lo Foo<T>accetta come parametro di output e viceversa. Così, contra -variance. Al contrario, interface ISupplier<out T> { T get();};una Supplier<Foo<T>>volontà avrà qualunque tipo di varianza Foo- quindi la co- fusione.
supercat,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.