Compréhension du problème de la contravariance des covariances avec les génériques en C #

115

Je ne comprends pas pourquoi le code C # suivant ne se compile pas.

Comme vous pouvez le voir, j'ai une méthode générique statique Something avec un IEnumerable<T>paramètre (et Test contraint d'être une IAinterface), et ce paramètre ne peut pas être converti implicitement en IEnumerable<IA>.

Quelle est l'explication? (Je ne recherche pas de solution de contournement, juste pour comprendre pourquoi cela ne fonctionne pas).

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)
    {
    }
}

Erreur que je reçois en Something2(bar)ligne:

Argument 1: impossible de convertir de «System.Collections.Generic.List» en «System.Collections.Generic.IEnumerable»

BenLaz
la source
12
Vous n'êtes pas limité Taux types de référence. Si vous utilisez la condition, where T: class, IAcela devrait fonctionner. La réponse liée a plus de détails.
Dirk
2
@Dirk Je ne pense pas que cela devrait être signalé comme un doublon. S'il est vrai que le problème de concept ici est un problème de covariance / contravariance face aux types de valeur, le cas spécifique ici est "que signifie ce message d'erreur" ainsi que l'auteur ne se rendant pas compte simplement d'inclure "classe" corrige son problème. Je pense que les futurs utilisateurs rechercheront ce message d'erreur, trouveront ce message et partiront heureux. (Comme je le fais souvent.)
Reginald Blue
Vous pouvez également reproduire la situation en disant simplement Something2(foo);directement. Il n'est pas nécessaire de faire le tour .ToList()pour obtenir un List<T>( Tvotre paramètre de type est-il déclaré par la méthode générique) pour comprendre cela (a List<T>est un IEnumerable<T>).
Jeppe Stig Nielsen
@ReginaldBlue 100%, allait publier la même chose. Des réponses similaires ne font pas une question en double.
UuDdLrLrSs

Réponses:

218

Le message d'erreur n'est pas suffisamment informatif, et c'est ma faute. Désolé pour ça.

Le problème que vous rencontrez est une conséquence du fait que la covariance ne fonctionne que sur les types de référence.

Vous dites probablement "mais IAest un type de référence" en ce moment. Oui, ça l'est. Mais vous n'avez pas dit que T c'était égal à IA . Vous avez dit que Tc'est un type qui implémente IA , et qu'un type valeur peut implémenter une interface . Par conséquent, nous ne savons pas si la covariance fonctionnera et nous la rejetons.

Si vous voulez que la covariance fonctionne, vous devez indiquer au compilateur que le paramètre de type est un type de référence avec la classcontrainte ainsi que la IAcontrainte d'interface.

Le message d'erreur devrait vraiment dire que la conversion n'est pas possible car la covariance nécessite une garantie de type de référence, puisque c'est le problème fondamental.

Eric Lippert
la source
3
Pourquoi as-tu dit que c'était de ta faute?
user4951
77
@ user4951: Parce que j'ai implémenté toute la logique de vérification de conversion, y compris les messages d'erreur.
Eric Lippert
@BurnsBA Ce n'est qu'un "défaut" au sens causal - techniquement, la mise en œuvre ainsi que le message d'erreur sont parfaitement corrects. (C'est juste que l'énoncé d'erreur d'inconvertabilité pourrait expliquer les raisons réelles. Mais produire de bonnes erreurs avec des génériques est difficile - comparé aux messages d'erreur du modèle C ++ il y a quelques années, c'est lucide et concis.)
Peter - Réintégrer Monica
3
@ PeterA.Schneider: J'apprécie cela. Mais l'un de mes principaux objectifs pour la conception de la logique de rapport d'erreurs dans Roslyn était en particulier de capturer non seulement quelle règle a été violée, mais en outre, d'identifier la «cause fondamentale» lorsque cela était possible. Par exemple, à quoi doit servir le message d'erreur customers.Select(c=>c.FristName)? La spécification C # indique très clairement qu'il s'agit d'une erreur de résolution de surcharge : l'ensemble des méthodes applicables nommées Select qui peuvent accepter ce lambda est vide. Mais la cause première est qu'il y FirstNamea une faute de frappe.
Eric Lippert
3
@ PeterA.Schneider: J'ai fait beaucoup de travail pour m'assurer que les scénarios impliquant l'inférence de type générique et les lambdas utilisent l'heuristique appropriée pour déduire le message d'erreur susceptible d'aider le mieux le développeur. Mais j'ai fait un bien moins bon travail sur les messages d'erreur de conversion, particulièrement en ce qui concerne la variance. J'ai toujours regretté cela.
Eric Lippert
26

Je voulais juste compléter l'excellente réponse d'initié d'Eric avec un exemple de code pour ceux qui ne sont peut-être pas familiers avec les contraintes génériques.

Changez Somethingla signature de la manière suivante: la classcontrainte doit venir en premier .

public static IList<T> Something<T>(IEnumerable<T> foo) where T : class, IA
Marcell Toth
la source
2
Je suis curieux ... quelle est exactement la raison derrière la signification de la commande?
Tom Wright
5
@TomWright - la spécification n'inclut bien sûr pas la réponse à de nombreux "Pourquoi?" questions, mais dans ce cas, il est clair qu'il existe trois types distincts de contraintes, et lorsque les trois sont utilisées, elles doivent être spécifiquementprimary_constraint ',' secondary_constraints ',' constructor_constraint
Damien_The_Unbeliever
2
@TomWright: Damien a raison; il n'y a aucune raison particulière que je connaisse autre que la commodité de l'auteur de l'analyseur. Si j'avais mes druthers, la syntaxe des contraintes de type serait considérablement plus verbeuse. classest mauvais car cela signifie «type de référence», pas «classe». J'aurais été plus heureux avec quelque chose de verbeux commewhere T is not struct
Eric Lippert