Comprensione delle interfacce covariante e controvariante in C #


87

Li ho trovati in un libro di testo che sto leggendo in C #, ma ho difficoltà a capirli, probabilmente a causa della mancanza di contesto.

C'è una buona spiegazione concisa di cosa sono e per cosa sono utili là fuori?

Modifica per chiarimenti:

Interfaccia covariante:

interface IBibble<out T>
.
.

Interfaccia controvariante:

interface IBibble<in T>
.
.

3
Questa è una breve e buona spiegazione IMHO: blogs.msdn.com/csharpfaq/archive/2010/02/16/…
digEmAll

Può essere utile: post sul blog
Krunal

Hmm è buono ma non spiega perché, che è ciò che mi sconcerta davvero.
NibblyPig

Risposte:


144

Con <out T>è possibile trattare il riferimento all'interfaccia come uno verso l'alto nella gerarchia.

Con <in T>, puoi trattare il riferimento dell'interfaccia come uno verso il basso nella ricerca.

Vorrei provare a spiegarlo in termini più inglesi.

Supponiamo che tu stia recuperando un elenco di animali dal tuo zoo e intendi elaborarli. Tutti gli animali (nel tuo zoo) hanno un nome e un ID univoco. Alcuni animali sono mammiferi, alcuni sono rettili, alcuni sono anfibi, altri sono pesci, ecc. Ma sono tutti animali.

Quindi, con il tuo elenco di animali (che contiene animali di diversi tipi), puoi dire che tutti gli animali hanno un nome, quindi ovviamente sarebbe sicuro ottenere il nome di tutti gli animali.

Tuttavia, cosa succede se hai solo un elenco di pesci, ma devi trattarli come animali, funziona? Intuitivamente, dovrebbe funzionare, ma in C # 3.0 e prima, questo pezzo di codice non verrà compilato:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>

Il motivo è che il compilatore non "sa" cosa intendi o puoi fare con la raccolta di animali dopo che l'hai recuperata. Per quanto ne sa, potrebbe esserci un modo IEnumerable<T>per rimettere un oggetto nell'elenco, e questo potenzialmente consentirebbe di inserire un animale che non è un pesce, in una raccolta che dovrebbe contenere solo pesce.

In altre parole, il compilatore non può garantire che ciò non sia consentito:

animals.Add(new Mammal("Zebra"));

Quindi il compilatore si rifiuta semplicemente di compilare il codice. Questa è la covarianza.

Diamo un'occhiata alla controvarianza.

Dal momento che il nostro zoo può gestire tutti gli animali, può sicuramente gestire i pesci, quindi proviamo ad aggiungere alcuni pesci al nostro zoo.

In C # 3.0 e versioni precedenti, questo non viene compilato:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
fishes.Add(new Fish("Guppy"));

Qui, il compilatore potrebbe consentire questo pezzo di codice, anche se il metodo ritorna List<Animal>semplicemente perché tutti i pesci sono animali, quindi se cambiassimo i tipi in questo:

List<Animal> fishes = GetAccessToFishes();
fishes.Add(new Fish("Guppy"));

Quindi funzionerebbe, ma il compilatore non può determinare che non stai tentando di farlo:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
Fish firstFist = fishes[0];

Poiché l'elenco è in realtà un elenco di animali, ciò non è consentito.

Quindi la controvarianza e la covarianza è il modo in cui tratti i riferimenti agli oggetti e cosa puoi farne.

Le parole chiave ine outin C # 4.0 contrassegnano specificamente l'interfaccia come l'una o l'altra. Con in, puoi inserire il tipo generico (di solito T) nelle posizioni di input , che significa argomenti del metodo e proprietà di sola scrittura.

Con out, è possibile inserire il tipo generico nelle posizioni di output , ovvero valori restituiti dal metodo, proprietà di sola lettura e parametri del metodo out.

Ciò ti consentirà di fare ciò che intendi fare con il codice:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>
// since we can only get animals *out* of the collection, every fish is an animal
// so this is safe

List<T> ha entrambe le direzioni di entrata e di uscita su T, quindi non è né co-variante né contro-variante, ma un'interfaccia che ti ha permesso di aggiungere oggetti, come questo:

interface IWriteOnlyList<in T>
{
    void Add(T value);
}

ti permetterebbe di fare questo:

IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns
                                                            IWriteOnlyList<Animal>
fishes.Add(new Fish("Guppy")); <-- this is now safe

Ecco alcuni video che mostrano i concetti:

Ecco un esempio:

namespace SO2719954
{
    class Base { }
    class Descendant : Base { }

    interface IBibbleOut<out T> { }
    interface IBibbleIn<in T> { }

    class Program
    {
        static void Main(string[] args)
        {
            // We can do this since every Descendant is also a Base
            // and there is no chance we can put Base objects into
            // the returned object, since T is "out"
            // We can not, however, put Base objects into b, since all
            // Base objects might not be Descendant.
            IBibbleOut<Base> b = GetOutDescendant();

            // We can do this since every Descendant is also a Base
            // and we can now put Descendant objects into Base
            // We can not, however, retrieve Descendant objects out
            // of d, since all Base objects might not be Descendant
            IBibbleIn<Descendant> d = GetInBase();
        }

        static IBibbleOut<Descendant> GetOutDescendant()
        {
            return null;
        }

        static IBibbleIn<Base> GetInBase()
        {
            return null;
        }
    }
}

Senza questi segni, potrebbe essere compilato quanto segue:

public List<Descendant> GetDescendants() ...
List<Base> bases = GetDescendants();
bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant

o questo:

public List<Base> GetBases() ...
List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases
                                               as Descendants

Hmm, saresti in grado di spiegare l'obiettivo della covarianza e della controvarianza? Potrebbe aiutarmi a capirlo di più.
NibblyPig

1
Guarda l'ultima parte, che è ciò che il compilatore ha impedito in precedenza, lo scopo di in e out è di dire cosa puoi fare con le interfacce (o tipi) che è sicuro, in modo che il compilatore non ti impedisca di fare cose sicure .
Lasse V. Karlsen,

Risposta superba, ho visto i video che sono stati molto utili e combinato con il tuo esempio ora l'ho risolto. Rimane solo una domanda, ed è per questo che sono necessari "out" e "in", perché visual studio non sa automaticamente cosa stai cercando di fare (o qual è la ragione dietro)?
NibblyPig

Automagic "Vedo cosa stai cercando di fare lì" di solito è disapprovato quando si tratta di dichiarare cose come le classi, è meglio che il programmatore contrassegni esplicitamente i tipi. Puoi provare ad aggiungere "in" a una classe che ha metodi che restituiscono T e il compilatore si lamenterà. Immagina cosa succederebbe se rimuovesse silenziosamente la "in" che aveva precedentemente aggiunto automaticamente per te.
Lasse V. Karlsen,

1
Se una parola chiave necessita di questa lunga spiegazione, qualcosa è chiaramente sbagliato. A mio parere, C # sta cercando di essere troppo intelligente in questo caso particolare. Tuttavia, grazie per la bella spiegazione.
rr-

8

Questo post è il migliore che abbia letto sull'argomento

In breve, covarianza / controvarianza / invarianza si occupa del casting automatico del tipo (da base a derivato e viceversa). Tali cast di tipo sono possibili solo se vengono rispettate alcune garanzie in termini di azioni di lettura / scrittura eseguite sugli oggetti cast. Leggi il post per maggiori dettagli.


5
Link sembra morto. Ecco una versione archiviata: web.archive.org/web/20140626123445/http://adamnathan.co.uk/…
si618

1
mi piace ancora di più questa spiegazione: codepureandsimple.com/…
volkit
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.