Covariância e Contravariância em C#, Parte Onze: Ao infinito, mas não além

by Fujiy 2. julho 2008 21:41

Como discutido ao longo desse espaço, estavamos considerando adicionar Covariância e Contravariância em tipos de parâmetros de delegates e interfaces para uma futura versão do C#.

Variância é extremamente útil em muitos casos, mas expõe um problema irritante em certos casos raros e bizarros. Este é apenas um exemplo:

Considere uma interface "normal" contravariante em seu único parâmetro de tipo genérico, e uma interface "louca" invariante que herda a interface normal em uma forma estranha:

public interface IN<in U> {}
public interface IC<X> : IN<IN<IC<IC<X>>>> {}


Isto é um pouco estranho, mas com certeza válido.

Antes de continuarmos, vamos entender porque isto é válido. A maioria das pessoas quando ve algo como isso imediatamente diz "mas uma interface não pode herdar de si mesma, isso é inválido, é uma cadeia circular na herança!"

Primeiro, não, isso não é correto. Em nenhum lugar a especificação do C# faz este tipo de herança inválido, e de fato, uma forma mais simples disso deve ser válida. Você deve ser capaz de dizer:

interface INumber<X> : IComparable<INumber<X>> { ... }


ou seja, você deve ser capaz de expressar que uma das garantias do contrato de INumber<X> é que você possa sempre comparar um número com outro. Portanto, deve ser válido usar um nome de tipo no argumento de tipo do tipo generic do pai.

No entanto, nem tudo é perfeito. Este tipo particular bruto de herança que dei como exemplo é de fato inválido no CLR, mesmo que não seja inválido no C#. Isso significa que é possível ter um tipo de interface compilada em C# que não pode ser carregada pelo CLR. Isto infelizmente irá gerar problemas, e espero que em uma versão futura do C# faça as definição das regras do C# mais rígidas ou até mais rígidas que as do CLR. Até lá, evite fazer isso.

Segundo, infelizmente, o compilador de C# atualmente tem vários bugs no seu detector de ciclo, como algumas vezes em que coisas se parecem com ciclos mas não são realmente, e são marcados com erros de ciclos. Isto só torna ainda mais difícil a compreensão do que é um ciclo válido e o que não é. Por exemplo, hoje o compilador irá incorretamente dizer que isto é um ciclo de classe base inválido, mesmo que claramente não é:


    public class November<T> { }
    public class Romeo : November<Romeo.Sierra.Tango>
    {
        public class Sierra
        {
           public class Tango { }
        }
    }


De qualquer forma, voltando ao assunto: variância louca. Nós temos as interfaces definidas acima, e então damos ao compilador um pequeno enigma pra resolver:

IC<double> bar = qualquer;
IN<IC<string>> foo = bar;  // Esta atribuição é válida?


Estamos prestes a entrar em um nome generic praticamente impossível de ler, então pra ficar mais fácil, a partir de agora vamos abreviar IN<IC<string>> como NCS. IC<double> será abreviado como CD.

Do mesmo modo, vou abreviar "é convertível de forma implícita" por uma seta. Então a questão é: CD→NCS é true ou false?

Vamos ver. Claramente CD não se converte em NCS diretamente. Mas (por motivos do compilador) talvez o tipo base de CD’s consiga.

O tipo base de CD’s é NNCCD. Será que NNCCD→NCS? Bem, N é contravariante em seus parâmetros assim isto se resume a questão, CS→NCCD?

Claramente não diretamente. Mas talvez CS tenha um tipo base que converta em NCCD. O tipo base de CS é NNCCS. Agora nós temos a questão NNCCS→NCCD ?

N é contravariante em seus parâmetros, agora caímos em CCD→NCCS ?

O compilador vai "reduzindo" o problema se CD→NCS é verdadeiro à CCD→NCCS! Se mantermos uma "redução" como esta então teremos CCCD→NCCCS, CCCCD→NCCCCS, e assim por diante.

Isto é apenas uma fração de algumas das formas em que o sistema de tipos pode ficar estranho. Para se aprofundar mais nesse assunto, você pode ler o Microsoft Research paper.

Parte Anterior: Covariância e Contravariância em C#, Parte Dez: Lidando Com Ambiguidades

Tags:

Artigos | Covariância e Contravariância

Covariância e Contravariância em C#, Parte Dez: Lidando Com Ambiguidades

by Fujiy 24. junho 2008 10:01

Suponha que fizemos IEnumerable covariante em T. O que este código deve fazer?

class C : IEnumerable<Girafa>, IEnumerable<Tartaruga>
{
    IEnumerator<Girafa> IEnumerable<Girafa>.GetEnumerator()
    {
        yield return new Girafa();
    }
    IEnumerator<Tartaruga> IEnumerable<Tartaruga>.GetEnumerator()
    {
        yield return new Tartaruga();
    }
    // [etc.]
}
 
class Program
{
    static void Main()
    {
        IEnumerable<Animal> animais = new C();
        Console.WriteLine(animais.First().GetType().ToString());
    }
}


Opções:

1) Erro em tempo de compilação.
2) Erro em tempo de execução.
3) Sempre enumera Girafas.
4) Sempre enumera Tartarugas.
5) Escolha entre Girafas e Tartarugas em tempo de execução.
6) Nenhuma das opções acima.

Se você escolheu outra opção além da 1, deveríamos ter um warning em tempo de compilação?

Continua na parte onze.

Parte Anterior: Covariância e Contravariância em C#, Parte Nove: Criando Incompatibilidades

Tags:

Artigos | Covariância e Contravariância

Covariância e Contravariância em C#, Parte Nove: Criando Incompatibilidades

by Fujiy 24. junho 2008 09:02

Nesta parte vamos discutir quais incompatibilidades teremos ao adicionar este recurso.

Simplesmente adicionando variância às regras de conversão nunca deveria causar nenhuma incompatibilidade. Entretanto, a combinação de se adicionar variância às regras de conversão e fazer alguns tipos terem parâmetros variantes causa uma potencial quebra de compatibilidade.

Geralmente as pessoas sabem que não se deve fazer isso:

if (x is Animal) 
  DoSomething();
else if (x is Girafa) 
  DoSomethingElse(); // nunca executa


porque a segunda condição é totalmente englobada pela primeira. Mas hoje no C# 3.0 é perfeitamente normal escrever:

if (x is IEnumerable<Animal>) 
  DoSomething();
else if (x is IEnumerable<Girafa>) 
  DoSomethingElse();


porque não há qualquer conversão sendo usada entre IEnumerable<Animal> e IEnumerable<Girafa>. Se nós adicionarmos covariância no IEnumerable<T> e o programa compilado contendo o fragmento acima usar a nova biblioteca então o comportamento quando dado um IEnumerable<Girafa> irá mudar. O objeto passará a ser atribuível à IEnumerable<Animal>, e por isso o "is" irá retornar "true", mudando a lógica do programa.

Há também a questão de mudar a semântica dos códigos fonte existentes ou tornar programas compiláveis em programas com erros de compilação. Por exemplo, a resolução da sobrecarga pode falhar onde deveria ser usada com sucesso. Se nós temos:

interface IBar<T>{} // Vindo de outro assembly
...
void M(IBar<Tigre> x){}
void M(IBar<Girafa> x){}
void M(object x) {}
...
IBar<Animal> y = qualquer;
M(y);


Então a resolução da sobrecarga pega a versão que recebe um object porque é a única escolha aplicável. Se nós mudarmos a definição de IBar para

interface IBar<-T>{}


e recompilar então teremos um erro de ambiguidade porque agora todos os três são aplicáveis e não há uma única melhor escolha.

Sempre queremos evitar incompatibilidades se possível, mas as vezes novos recursos são suficientemente atraentes e as incompatibilidades são tão raras que vale a pena. Acho que criando a variância em interfaces e delegates vamos permitir muito mais cenários interessantes do que incompatibilidades.

Será que este recurso irá trazer tantos benefícios que vale a pena gastar tempo desenvolvendo-o pra uma futura versão do C#?

Continua na parte dez: Covariância e Contravariância em C#, Parte Dez: Lidando Com Ambiguidades

Parte Anterior: Covariância e Contravariância em C#, Parte Oito: Opções de Sintaxe

Tags:

Artigos | Covariância e Contravariância

Covariância e Contravariância em C#, Parte Oito: Opções de Sintaxe

by Fujiy 23. junho 2008 14:13

Como discutimos anteriormente, nós introduzimos variância de interface e delegate em uma hipotética futura versão do C#, então precisamos de uma sintaxe para ela. Aqui estão algumas possibilidades.

Opção 1:

interface IFoo<+T, -U> { T Foo(U u); }


A CLR usa a convenção que estamos usando em toda a série de "+ sendo covariante e - contravariante”. Embora isto tenha algum valor mnemónico (porque + quer dizer "é compatível com um tipo maior"), muitas pessoas (incluindo membros da comissão de design do C#!) têm dificuldades em se lembrar o que é exatamente.

Esta convenção também é utilizada pela linguagem de programação Scala.

Opção 2:


interface IFoo<T:*, *:U> { …


Este indica mais graficamente "alguma coisa que é extendida por T" e "alguma coisa a qual extende U". É similiar as keywords Java, onde elas dizem "extends U" ou "super T".

Embora isto não seja terrível, penso que isto funde um pouco as noções de extensão e compatibilidade de atribuição. Eu não quero implicar que IEnumerable<Animal> é a base de IEnumerable<Girafa>, mesmo que Animal seja a base de Girafa. Em vez disso, quero dizer que IEnumerable<Girafa> é convertível para IEnumerable<Animal>, ou a atribuição é compatível. Eu não quero exceder os conceitos o mecanismo de herança. Isto é ruim, nós estamos unindo classes base com interfaces base.

Opção 3:

interface IFoo<T, U> where T: covariant, U: contravariant { …


Novamente, nada mal. O perigo aqui é similar ao dos sinais mais e menos: que ninguém se lembra o que "contravariante" e "covariante" significa. Este tem a vantagem de que pelo menos você pode buscar pelas palavras e achar alguma explicação.

Opção 4:

interface IFoo<[Covariant] T, [Contravariant] U>  { …


Similar à opção 3.

Opção 5:

interface IFoo<out T, in U> { …


Estamos tomando um rumo diferente com essa sintaxe. Nas opções anteriores nós estávamos descrevendo como o usuário da interface pode trata-lá, respeitando as regras do sistema de conversão de tipos para conversão implícita – que é, quais são as variâncias válidas nos tipos dos parâmetros. Em vez disso, aqui nós estamos descrevendo como o implementador da interface tem a intenção de usar os tipos dos parâmetros.

A desvantagem é que, como discutido em artigos anteriores, você acaba em situações como essa:

delegate void Meta<out T>(Action<T> action);


onde o "out" T seja claramente utilizada em uma posição de entrada.

Continua na parte nove: Covariância e Contravariância em C#, Parte Nove: Criando Incompatibilidades

Parte Anterior: Covariância e Contravariância em C#, Parte Sete: Por que precisamos de uma sintaxe para tudo?

Tags:

Artigos | Covariância e Contravariância

Covariância e Contravariância em C#, Parte Sete: Por que precisamos de uma sintaxe para tudo?

by Fujiy 16. maio 2008 09:12

Suponha que estamos implementando interface genérica e variância delegada em um futura versão hipotética do C#. Como, hipoteticamente, deveria ser a sintaxe? Há muitas opções que poderíamos considerar.

Antes de pensarmos nas opções, nos perguntamos, "E se não tivermos uma sintaxe para tudo?" Por que não inferimos a variância para o desenvolvedor, de modo que tudo apenas magicamente funcione?

Infelizmente isso não existe, por várias razões.

Primeira, assim parece que a variância devia ser algo que você deliberadamente implementa em sua interface ou delegate. Fazendo isso você começa a perder o controle do objetivo que o usuário busca.

Fazer isso "automagicamente" também significa que, assim como o processo de desenvolvimento e métodos forem adicionados às interfaces, a variância da interface pode mudar inesperadamente. Isto pode introduzir inesperadas e profundas mudanças em outros pontos do programa.

Segundo, tentar fazer isso introduz um novo tipo de ciclo à análise da linguagem. Nós já temos que detectar coisas como ciclos nas classes base, ciclos na interfaces base e ciclos nas regras de tipos genéricos, então em teoria não tem nenhuma novidade. Mas na prática, há alguns problemas.

No artigo anterior não discutimos sobre restrições adicionais que nós precisamos para criar interfaces variantes. Uma restrição importante é que a interface variante que herda de outra interface variante deve fazer de uma maneira que não introduza problemas no sistema de tipos. Basicamente, todas as regras para quando um tipo de parâmetro pode ser covariante ou contravariante precisam "fluir" para a interface base.

Por exemplo, suponha que o compilador esteja tentando deduzir a variância neste programa:

interface IFrob<T> : IBlah<T> { }
interface IBlah<U>
{
    IFrob<U> Frob();
}



E nos perguntamos "é válido para T ser variante em IFrob<T>?" Para responder esta pergunta, nós precisamos determinar se é válido para U ser variante em IBlah. E para responder esta pergunta nós precisamos saber se válido para U ser variante no tipo de saída IFrob<U>, e...voltamos do ponto que começamos!

Não queremos que o compilador entre em um loop infinito quando compilar este programa. Mas esta claro que este programa é perfeitamente válido. Quando detectarmos um ciclo nas classes base, podemos parar e dizer "Seu programa é inválido". Mas não podemos fazer isso aqui. É uma questão complicada.

Terceiro, mesmo se pudéssemos descobrir uma maneira de resolver o problema do ciclo, nós ainda teríamos um problema com o caso acima. Ou seja, há três possíveis respostas logicamente consistentes: "ambas invariantes", "+T, +U" e "-T, -U" todas produzem programas que seriam typesafe. Como podemos escolher?

Podíamos começar em situações ainda piores:

interface IRezrov<V, W>
{
    IRezrov<V, W> Rezrov(IRezrov<W, V> x);
}



Nesta interface louca podemos deduzir que "ambas invariantes", <+V, -W> e <-V, +W> são todas as possibilidades. Novamente, como escolher?

E quarto, mesmo se pudéssemos resolver todos esses problemas, suspeito que o desempenho de tal algoritmo seria potencialmente muito ruim. Isto tem tudo para ter um "crescimento exponencial". Temos outros algoritmos exponenciais no compilador, mas prefiro não adicionar mais, se podemos evitar.

Assim, se adicionarmos interface e variância delegada em uma hipotética futura versão do C#, precisamos dar uma sintaxe para ela.

Continua na parte oito: Covariância e Contravariância em C#, Parte Oito: Opções de Sintaxe

Parte Anterior: Covariância e Contravariância em C#, Parte Seis:Variância de Interface

Tags:

Artigos | Covariância e Contravariância

Covariância e Contravariância em C#, Parte Seis:Variância de Interface

by Fujiy 14. maio 2008 16:12

Nos posts passados nós discutimos como seria possível tratar um delegate como contravariante nos seus argumentos e covariante no seu tipo de retorno. Um delegate é basicamente um objeto que representa uma chamada de função. Nós podemos fazer estes mesmos tipos de coisas para outros que representam chamadas de funções. Interfaces, por exemplo, são contratos que especificam qual o conjunto de chamadas de função que estão disponíveis em um objeto em particular.

Isto quer dizer que nós devemos extender a noção de variância para definições de interface também, usando as mesmas regras que nós temos para delegates. Por exemplo, considere:

public interface IEnumerator<T> : IDisposable, IEnumerator

{

    new T Current { get; }

}



Aqui nós temos uma interface genérica onde o único uso de parâmetro está em uma posição de saída. Poderíamos, assim, considerar o parâmetro covariante. Isso significaria que seria válido atribuir um objeto que implementa IEnumerator<Girafa> para uma variável do tipo IEnumerator<Animal>. Desde que o usuário daquela variável irá sempre esperar um Animal, e a nossa implementação irá sempre produzir uma Girafa, fica tudo ok.

Então temos IEnumerator<+T>, então nós podemos perceber que IEnumerable<T> é definido como:

public interface IEnumerable<T> : IEnumerable

{

    new IEnumerator<T> GetEnumerator();

}



Novamente, o parâmetro aparece somente em uma posição de saída, então nós poderiamos ter IEnumerable<+T> covariante também.

Isto então abre uma janela de assassinatos de bons cenários. Hoje, este código deve falhar na compilação:

void AlimentarAnimais(IEnumerable<Animal> animals)

{

    foreach (Animal animal in animals)

        if (animal.Faminto)

            Alimentar(animal);

}

...

IEnumerable<Girafa> GirafasAdultas = from g in girafas where g.Age > 5 select g;

AlimentarAnimais(GirafasAdultas);



Porque GirafasAdultas implementa IEnumerable<Girafa>, não IEnumerable<Animal>. No C# 3.0 você tem que fazer um idiota e dispendiosa operação de conversão para fazer isto compilar, algo como:

AlimentarAnimais(GirafasAdultas.Cast<Animal>());

ou

AlimentarAnimais(from g in GirafasAdultas select (Animal)g);

Ou seja o que for. Esta declaração explícita não deveria ser necessária. Diferente de arrays (que são leitura-escrita) é perfeitamente seguro tratar uma lista de Girafas read-only como uma lista de Animais.

Do mesmo modo, poderíamos tornar:

public interface IComparer<-T>

{

    int Compare(T x, T y);

}



uma interface contravariante, desde que o tipo fosse usado somente em posições de entrada. Você poderia, então, implementar um objeto que compara dois Animais e usá-lo em um contexto onde você precisa de um objeto que compare duas Girafas sem se preocupar com problemas no sistema de tipos.

Continua na parte sete: Covariância e Contravariância em C#, Parte Sete: Por que precisamos de uma sintaxe para tudo?

Parte Anterior: Covariância e Contravariância em C#, Parte Cinco: Contravariância Dupla

Tags:

Artigos | Covariância e Contravariância

Covariância e Contravariância em C#, Parte Cinco: Contravariância Dupla

by Fujiy 25. abril 2008 08:14

Na parte quatro da série, falamos sobre como poderiamos ter um tipo de delegate que aceitaria ter valores covariantes no seu tipo de retorno e contravariantes no tipo recebido por argumento. Por exemplo, nós podemos ter um delegate action contravariante:

delegate void Action< -A > (A a);


e então temos

Action<Animal> action1 = (Animal a)=>{ Console.WriteLine(a.NomeLatin); };

Action<Girafa> action2 = action1;



Porque o invocador do action2 irá sempre passar algo que o action1 possa manipular.

Baseado no que vimos até agora no que diz respeito à variância concluimos que "o que está entrando deve ser contravariante, o que sai deve ser variante". Embora pareça que este seria um uso comum de variância que deveria ser implementada numa futura versão do C#, a realidade é um pouco mais complicada. Há uma situação onde é válido usar um argumento covariante como parâmetro de um delegate.

Suponha que você queira criar uma programação funcional de "ordem maior". Por exemplo, talvez você queira definir uma meta-ação – um delegate que recebe actions e faz alguma coisa com eles:

delegate void Meta<A>(Action<A> action);


Por exemplo:

Meta<Mamifero> meta1 = (Action<Mamifero> action)=>{action(new Girafa());};

 

// A próxima linha é válida porque Action<Animal> é menor que Action<Mamifero>;

// lembrando que Action é contravariante

meta1(action1);

Então este Meta recebe um Action de Mamiferos, ou o action1 acima, o qual exibe o nome em Latin de qualquer Animal, o que quer dizer que pode ser usado para os Mamifero – e então invoca aquela action usando o new Girafa.

É evidente que o parâmetro de tipo A é usado em uma posição de entrada na definição de Meta<A>, então nós devemos ser capazes de usar contravariancia, certo? Suponha que sim. Isso significa que essa atribuição seria válida:

Meta<Tigre> meta2 = meta1; // deve ser válida se Meta é contraviante no parâmetro


Mas isso significa que isso seria válido:

Action<Tigre> action3 = Tigre=>{ Tigre.Rosnar(); };

meta2(action3);



Seguindo a lógica você verá que no final acabamos chamando (new Girafa()).Rosnar(), o qual claramente viola ambas as regras: do sistema e da natureza. Pode ser um pouco complicado até entender toda a lógica, mas escrevendo o código pode ajudar:

static void Main(string[] args)

        {

            Action<int> a;

 

            Action<Animal> action1 = metodo;

            Action<Girafa> action2 = action1;

 

            Meta<Mamifero> meta1 = metodo2;

 

            meta1(action1);

 

 

            Meta<Tigre> meta2 = meta1;

            Action<Tigre> action3 = metodo3;

            meta2(action3);

 

            Meta<Animal> meta3 = meta1;

 

 

        }

 

        static void metodo3(Tigre Tigre)

        {

            Tigre.Rosnar();

        }

 

        static void metodo2(Action<Mamifero> action)

        {

            action(new Girafa());

        }

 

        static void metodo(Animal a)

        {

            Console.WriteLine(a.NomeLatin);

        }

 

        public delegate void Meta<A>(Action<A> action);



No final do Main, você pode ver que o meta2 é igual ao meta1. O meta1 chama o método 2. O método 2 vai chamar o action passado(no caso o action3 que chama o metodo3) passando um new Girafa() como parâmetro. Neste momento que acontece a inconsistência. Pois estamos chamando o método3 passando uma Girafa, e o método3 iria chamar o método new Girafa()).Rosnar().

Então Meta<A> não pode ser contravariante em A. No entanto ele pode ser covariante:

Meta<Animal> meta3 = meta1; // válido se Meta for covariante


Agora tudo funciona. meta3 recebe um Action sobre Animals e então passa um Girafa para a Action.

Contravariância é complicado. O fato de se inverter o maior/menor relacionamento entre tipos diz que um tipo de parâmetro usado em uma posição de "contravariância dupla" (sendo uma entrada de Action, que é em si uma entrada de Meta) se torna covariante. O segundo desfaz a primeira inversão.

No próximo artigo deixaremos os delegates pra trás e falaremos sobre variância nas interfaces.

Continua na parte seis: Covariância e Contravariância em C#, Parte Seis:Variância de Interface

Parte Anterior: Covariância e Contravariância em C#, Parte Quatro: Variância de Delegate Real

Tags:

Artigos | Covariância e Contravariância

Covariância e Contravariância em C#, Parte Quatro: Variância de Delegate Real

by Fujiy 1. abril 2008 10:49
Nos dois últimos artigos da série falei sobre dois tipos de variância que o C# suporta - covariância de array e covariância (nos tipos de retorno) e contravariância (nos tipos dos argumentos) na conversão de grupo de membros para delegate.

Hoje vamos generalizar este último tipo de variância.

Hoje no C# 3.0, embora seja válido atribuir um grupo de membros sem tipo para uma função que retorne uma Girafa para uma variável do tipo Func<Animal>, não é válido atribuir uma expressão tipada do tipo Func<Giraffe> para uma Func<Animal>. Tipos Generic do delegate são são sempre invariantes no C# 3.0. Isso parece fraco.

Suponha que nós temos a possibilidade de declarar os tipos dos parâmetros dos tipos generic do delegate como sendo covariante ou contravariante. Para simplificar (e manter a consistência com a notação existente nas especificações da CLR) iremos escrever os parâmetros de tipo covariante com um + e os parâmetros de tipo contravariantes com um -.

Esta não é uma notação muito atraente. Mas por enquanto vamos usá-la. A forma de se lembrar o que o + significa é "este tipo aceita tipos maiores na atribuição", e menores para o -.

Considere, por exemplo a nossa função padrão:

delegate R Func<A, R>(A a);



Desde que R apareça somente no retorno e A apareça somente na lista de parâmetros, podemos fazer do R covariante e o A contravariante:

delegate R Func< -A, +R >(A a);



Então novamente, você pode pensar nisto como "você pode fazer o A menor ou o R maior" (ou, é claro, ambos). Por exemplo:

        Func<Animal, Giraffe> f1 = qualquer;


        Func<Mammal, Mammal> f2 = f1;



Normalmente no C# esta atribuição será inválida porque os delegates são parametrizados por tipos diferentes. Mas desde que nós temos Func variante em ambos tipos de parâmetros, esta atribuição deveria se tornar válida para adicionar este tipo de variância à uma futura versão do C#.

Será que isto faz sentido até agora?

Esta regra nem sempre é correta! Algumas vezes o parâmetros de entrada precisa ser de um tipo de parâmetro covariante (no nosso exemplo A é contravariante). Iremos discutir isto somente no próximo artigo.

Continua na parte cinco: Covariância e Contravariância em C#, Parte Cinco: Contravariância Dupla

Parte Anterior: Covariância e Contravariância em C#, Parte Três: Variância em Grupo de Métodos

Tags:

Artigos | Covariância e Contravariância

Covariância e Contravariância em C#, Parte Três: Variância em Grupo de Métodos

by Fujiy 13. fevereiro 2008 07:45

Anteriormente discutimos como a covariância numa array não funciona corretamente no C# (e Java, assim como uma série de outras linguagens). Agora, um tipo de variância válida suportada no C# 2.0: conversões de grupo de métodos em delegates. Este é um tipo mais complicado de variância, por isso vou explicar com mais detalhes.

Suponha que você tem um método que retorna um objeto Girafa.

static Girafa CriarGirafa() { }

E que você tem um delegate representando uma função que não recebe argumentos e retorna um Animal. Isto é, Func<animal>. Deveria esta conversão ser válida?

Func<animal> func = CriarGirafa;

Ao invocar Func é esperado que um Animal seja retornado. A função atual chamada pelo delegate sempre retorna uma Girafa, a qual é um Animal, então o invocador da função nunca recebe nada que ele não seja capaz de lidar. Não há problemas no sistema de tipo aqui. Portanto nós podemos criar métodos para delegate usando conversões covariantes nos seus tipos de retorno.


Agora vamos supor que você tem dois métodos, um que recebe Girafa e um que recebe um Animal:

        void Foo(Girafa g) { }

        void Bar(Animal a) { }


e um delegate que retorna vazio e recebe um argumento do tipo Mamifero:

        Action<Mamifero> action1 = Foo; // inválido

        Action<Mamifero> action2 = Bar; // válido


Por quê a primeira atribuição é inválida? Porque quem invocar o action1 pode passar por exemplo um tipo Tigre(já que seria derivado de Mamifero), mas Foo não pode receber um Tigre, somente uma Girafa(e seus derivados)! A segunda atribuição é válida porque Bar pode receber qualquer Animal.

No exemplo anterior preservamos a direção de atribuição: Girafa é menor que Animal, então o método que retorna Girafa é menor que o delegate que retorna um Animal. Neste exemplo, nós revertemos a direção de atribuição: Mamifero é menor que Animal, então o método que recebe Animal é menor que o delegate que recebe um Mamifero. Porque a direção está invertida, conversões de grupo de métodos para delegate são contravariantes nos tipos de seus argumentos.

Percebe que tudo falado acima aplica-se apenas em tipos por referência. Nunca diga algo como "Bem, todo int cabe em um long, então um método que retorna um int pode ser atribuído para uma variável do tipo Func<long>".

Continua na parte quatro: Covariância e Contravariância em C#, Parte Quatro: Variância de Delegate Real

Parte Anterior: Covariância e Contravariância em C#, Parte Dois: Covariância de Array

Tags:

Artigos | Covariância e Contravariância

Covariância e Contravariância em C#, Parte Dois: Covariância de Array

by Fujiy 3. janeiro 2008 08:52

C# implementa variância de duas maneiras. Hoje apresentarei a maneira incorreta.

Desde o C# 1.0, arrays onde o tipo do elemento é um tipo por referência são covariantes. Isto é perfeitamente correto:

Animal[] animais = new Girafa[10];

Desde que Girafa seja menor que Animal(isto é, Girafa herda de Animal), fazer um array dela é uma operação sobre tipos covariantes. Girafa[] é menor que Animal[], então sua instância se enquadra na variável.

Infelizmente, este tipo particular de covariância não é totalmente correto. Foi acrescentada à CLR porque o Java a implementa e os designers da CLR queriam uma linguagem parecida com o Java. Então foi adicionado ao C#, porque o recurso estava disponível na CLR. Esta decisão foi muito controversa no desenvolvimento, mas não há nada que possamos fazer em relação a isso agora.

Porque está incorreto? Porque deve ser sempre permitido colocar Tartaruga em um array de Animal. Com a covariância da array na linguagem e na runtime você não pode garantir que uma array de Animal pode aceitar uma Tartaruga porque por trás dela pode ser um array de Girafa.

Isto significa que tornamos um erro que podia ser pego pelo compilador em um que só pode ser pego no momento da execução. Isso também significa que toda vez que você colocar um objeto em um array, temos de fazer uma verificação em tempo de execução para garantir que o tipo funciona ou se gera uma exceção. Isso é potencialmente caro se você está colocando muitas coisas no array.

Um exemplo que você pode testar e verificar que realmente passa pelo compilador, mas gera uma exceção em tempo de execução:

class Animal { }
class Girafa : Animal { }
class Tartaruga : Animal { }

Animal
[] animais = new Girafa[10];
animais[0] = new Tartaruga();

Na próxima parte vamos discutir uma espécie de variância que foi adicionado ao C# 2.0, que não tem problemas como esse.

Continua na parte três: Covariância e Contravariância em C#, Parte Três: Variância em Grupo de Métodos

Parte Anterior: Covariância e Contravariância em C#, Parte Um

Tags:

Artigos | Covariância e Contravariância