Problema de compreensão da contravariância de covariância com genéricos em C #

115

Não consigo entender por que o seguinte código C # não compila.

Como você pode ver, tenho um método genérico estático Something com um IEnumerable<T>parâmetro (e Testá restrito a ser uma IAinterface) e esse parâmetro não pode ser convertido implicitamente para IEnumerable<IA>.

Qual é a explicação? (Não procuro uma solução alternativa, apenas para entender por que ela não funciona).

public interface IA { }
public interface IB : IA { }
public class CIA : IA { }
public class CIAD : CIA { }
public class CIB : IB { }
public class CIBD : CIB { }

public static class Test
{
    public static IList<T> Something<T>(IEnumerable<T> foo) where T : IA
    {
        var bar = foo.ToList();

        // All those calls are legal
        Something2(new List<IA>());
        Something2(new List<IB>());
        Something2(new List<CIA>());
        Something2(new List<CIAD>());
        Something2(new List<CIB>());
        Something2(new List<CIBD>());
        Something2(bar.Cast<IA>());

        // This call is illegal
        Something2(bar);

        return bar;
    }

    private static void Something2(IEnumerable<IA> foo)
    {
    }
}

Erro que recebo na Something2(bar)fila:

Argumento 1: não é possível converter de 'System.Collections.Generic.List' em 'System.Collections.Generic.IEnumerable'

BenLaz
fonte
12
Você não se restringiu Ta tipos de referência. Se você usar a condição where T: class, IA, ela deve funcionar. A resposta vinculada tem mais detalhes.
Dirk
2
@Dirk Não acho que isso deva ser sinalizado como uma duplicata. Embora seja verdade que o problema de conceito aqui é um problema de covariância / contravariância em face dos tipos de valor, o caso específico aqui é "o que essa mensagem de erro significa", bem como o autor não perceber que apenas incluir "classe" corrige seu problema. Acredito que futuros usuários irão pesquisar por esta mensagem de erro, encontrar esta postagem e sair feliz. (Como sempre faço.)
Reginald Blue
Você também pode reproduzir a situação simplesmente dizendo Something2(foo);diretamente. Ir ao redor .ToList()para obter um List<T>( Té o parâmetro de tipo declarado pelo método genérico) não é necessário para entender isso (a List<T>é um IEnumerable<T>).
Jeppe Stig Nielsen
@ReginaldBlue 100%, ia postar a mesma coisa. Respostas semelhantes não fazem uma pergunta duplicada.
UuDdLrLrSs

Respostas:

218

A mensagem de erro não é suficientemente informativa e a culpa é minha. Me desculpe por isso.

O problema que você está enfrentando é uma consequência do fato de que a covariância só funciona em tipos de referência.

Você provavelmente está dizendo "mas IAé um tipo de referência" agora. Sim, ele é. Mas você não disse que T é igual a IA . Você disse que Té um tipo que implementa IA , e um tipo de valor pode implementar uma interface . Portanto, não sabemos se a covariância funcionará e não a permitimos.

Se você deseja que a covariância funcione, você deve informar ao compilador que o parâmetro de tipo é um tipo de referência com a classrestrição, bem como a IArestrição de interface.

A mensagem de erro realmente deveria dizer que a conversão não é possível porque a covariância requer uma garantia de tipo de referência, uma vez que esse é o problema fundamental.

Eric Lippert
fonte
3
Por que você disse que é sua culpa?
user4951
77
@ user4951: Porque implementei toda a lógica de verificação de conversão, incluindo as mensagens de erro.
Eric Lippert
@BurnsBA Esta é apenas uma "falha" no sentido causal - tanto a implementação técnica quanto a mensagem de erro estão perfeitamente corretas. (Acontece que a declaração de erro de inconversibilidade poderia explicar as razões reais. Mas produzir bons erros com genéricos é difícil - em comparação com as mensagens de erro do modelo C ++ de alguns anos atrás, isso é lúcido e conciso.)
Peter - Reintegrar Monica
3
@ PeterA.Schneider: Agradeço isso. Mas um dos meus principais objetivos para projetar a lógica de relatório de erros em Roslyn foi, em particular, capturar não apenas qual regra foi violada, mas, além disso, identificar a "causa raiz" onde possível. Por exemplo, para que serve a mensagem de erro customers.Select(c=>c.FristName)? A especificação C # deixa bem claro que se trata de um erro de resolução de sobrecarga : o conjunto de métodos aplicáveis ​​denominado Select que pode aceitar esse lambda está vazio. Mas a causa raiz é que FirstNametem um erro de digitação.
Eric Lippert
3
@ PeterA.Schneider: Eu trabalhei muito para garantir que os cenários envolvendo inferência de tipo genérico e lambdas usassem as heurísticas apropriadas para deduzir qual mensagem de erro provavelmente ajudaria melhor o desenvolvedor. Mas eu fiz um trabalho muito menos bom nas mensagens de erro de conversão, particularmente no que se refere à variação. Sempre me arrependi disso.
Eric Lippert
26

Eu só queria complementar a excelente resposta interna de Eric com um exemplo de código para aqueles que podem não estar familiarizados com restrições genéricas.

Altere Somethinga assinatura da seguinte forma: A classrestrição tem que vir primeiro .

public static IList<T> Something<T>(IEnumerable<T> foo) where T : class, IA
Marcell Toth
fonte
2
Estou curioso ... qual é exatamente a razão por trás do significado do pedido?
Tom Wright
5
@TomWright - a especificação, é claro, não inclui a resposta a muitos "Por quê?" perguntas, mas neste caso deixa claro que existem três tipos distintos de restrições, e quando todos os três são usados, eles têm que ser especificamenteprimary_constraint ',' secondary_constraints ',' constructor_constraint
Damien_The_Unbeliever
2
@TomWright: Damien está correto; não há nenhum motivo específico que eu conheça além da conveniência do autor do analisador. Se eu pudesse, a sintaxe das restrições de tipo seria consideravelmente mais detalhada. classé ruim porque significa "tipo de referência", não "classe". Eu teria ficado mais feliz com algo prolixo comowhere T is not struct
Eric Lippert