Compreendendo as interfaces covariante e contravariante em C #

86

Encontrei isso em um livro que estou lendo em C #, mas estou tendo dificuldade em entendê-los, provavelmente devido à falta de contexto.

Existe uma boa explicação concisa do que eles são e para que são úteis lá fora?

Editar para esclarecimento:

Interface covariante:

interface IBibble<out T>
.
.

Interface contravariante:

interface IBibble<in T>
.
.
NibblyPig
fonte
3
Esta é uma breve e boa explicação IMHO: blogs.msdn.com/csharpfaq/archive/2010/02/16/…
digEmAll
Pode ser útil: Postagem no blog
Krunal
Hmm, é bom, mas não explica o porquê, o que é o que realmente está me confundindo.
NibblyPig

Respostas:

144

Com <out T>, você pode tratar a referência de interface como uma para cima na hierarquia.

Com <in T>, você pode tratar a referência da interface como uma para baixo na hierarquia.

Deixe-me tentar explicar em termos mais ingleses.

Digamos que você esteja recuperando uma lista de animais de seu zoológico e pretenda processá-los. Todos os animais (em seu zoológico) têm um nome e uma identificação exclusiva. Alguns animais são mamíferos, alguns são répteis, alguns são anfíbios, alguns são peixes, etc., mas são todos animais.

Então, com sua lista de animais (que contém animais de diferentes tipos), você pode dizer que todos os animais têm um nome, então obviamente seria seguro obter o nome de todos os animais.

No entanto, e se você tiver apenas uma lista de peixes, mas precisar tratá-los como animais, isso funciona? Intuitivamente, deve funcionar, mas no C # 3.0 e anteriores, este trecho de código não compilará:

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

A razão para isso é que o compilador não "sabe" o que você pretende ou pode fazer com a coleção de animais depois de recuperá-la. Pelo que se sabe, pode haver uma maneira IEnumerable<T>de colocar um objeto de volta na lista, e isso potencialmente permitiria que você coloque um animal que não seja um peixe em uma coleção que supostamente contém apenas peixes.

Em outras palavras, o compilador não pode garantir que isso não seja permitido:

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

Portanto, o compilador simplesmente se recusa a compilar seu código. Isso é covariância.

Vejamos a contravariância.

Como nosso zoológico pode lidar com todos os animais, certamente pode lidar com peixes, então vamos tentar adicionar alguns peixes ao nosso zoológico.

No C # 3.0 e anteriores, isso não compila:

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

Aqui, o compilador pode permitir esse trecho de código, embora o método retorne List<Animal>simplesmente porque todos os peixes são animais, então, se apenas alterarmos os tipos para este:

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

Então funcionaria, mas o compilador não pode determinar que você não está tentando fazer isso:

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

Como a lista é na verdade uma lista de animais, isso não é permitido.

Portanto, contra e covariância é como você trata as referências de objetos e o que você pode fazer com elas.

As palavras in- outchave e em C # 4.0 marcam especificamente a interface como uma ou outra. Com in, você pode colocar o tipo genérico (geralmente T) em posições de entrada , o que significa argumentos de método e propriedades somente de gravação.

Com out, você pode colocar o tipo genérico em posições de saída , que são valores de retorno de método, propriedades somente leitura e parâmetros de método de saída.

Isso permitirá que você faça o que pretendia fazer com o código:

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> tem direções de entrada e saída em T, portanto, não é nem covariante nem contra-variante, mas uma interface que permite adicionar objetos, como este:

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

permitiria que você fizesse isso:

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

Aqui estão alguns vídeos que mostram os conceitos:

Aqui está um exemplo:

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

Sem essas marcas, o seguinte poderia compilar:

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

ou isto:

public List<Base> GetBases() ...
List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases
                                               as Descendants
Lasse V. Karlsen
fonte
Hmm, você seria capaz de explicar a meta de covariância e contravariância? Pode me ajudar a entender melhor.
NibblyPig
1
Veja a última parte, que é o que o compilador preveniu antes, o propósito de in e out é dizer o que você pode fazer com as interfaces (ou tipos) que são seguras, para que o compilador não o impeça de fazer coisas seguras .
Lasse V. Karlsen
Excelente resposta, assisti aos vídeos que foram muito úteis e, combinado com o seu exemplo, resolvi agora. Resta apenas uma pergunta, e é por isso que 'fora' e 'dentro' são necessários, por que o visual studio não sabe automaticamente o que você está tentando fazer (ou qual é a razão por trás disso)?
NibblyPig
Automagic "Eu vejo o que você está tentando fazer aí" é geralmente desaprovado quando se trata de declarar coisas como classes, é melhor fazer com que o programador marque os tipos explicitamente. Você pode tentar adicionar "in" a uma classe que possui métodos que retornam T e o compilador reclamará. Imagine o que aconteceria se ele silenciosamente apenas removesse o "in" que havia adicionado automaticamente para você.
Lasse V. Karlsen
1
Se uma palavra-chave precisar dessa longa explicação, algo está claramente errado. Na minha opinião, C # está tentando ser muito inteligente neste caso específico. No entanto, obrigado pela explicação legal.
rr-
7

Este post é o melhor que li sobre o assunto

Em suma, covariância / contravariância / invariância lida com a conversão automática de tipos (da base para a derivada e vice-versa). Esses tipos de conversão são possíveis apenas se algumas garantias forem respeitadas em termos de ações de leitura / gravação realizadas nos objetos moldados. Leia a postagem para mais detalhes.

Ben G
fonte
5
Link parece morto. Aqui está uma versão arquivada: web.archive.org/web/20140626123445/http://adamnathan.co.uk/…
si618
1
Gosto
volkit