Problem beim Verständnis der Kovarianz-Kontravarianz mit Generika in C #

115

Ich kann nicht verstehen, warum der folgende C # -Code nicht kompiliert wird.

Wie Sie sehen können, habe ich eine statische generische Methode Etwas mit einem IEnumerable<T>Parameter (und Tist darauf beschränkt, eine IASchnittstelle zu sein), und dieser Parameter kann nicht implizit in konvertiert werden IEnumerable<IA>.

Was ist die Erklärung? (Ich suche nicht nach einer Problemumgehung, nur um zu verstehen, warum es nicht funktioniert).

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

Fehler, den ich in der Something2(bar)Schlange bekomme :

Argument 1: Konvertierung von 'System.Collections.Generic.List' in 'System.Collections.Generic.IEnumerable' nicht möglich

BenLaz
quelle
12
Sie haben sich nicht Tauf Referenztypen beschränkt . Wenn Sie die Bedingung verwenden where T: class, IA, sollte es funktionieren. Die verknüpfte Antwort enthält weitere Details.
Dirk
2
@Dirk Ich denke nicht, dass dies als Duplikat gekennzeichnet werden sollte. Während es stimmt, dass das Konzeptproblem hier ein Kovarianz- / Kontravarianzproblem angesichts von Werttypen ist, ist der spezielle Fall hier "Was bedeutet diese Fehlermeldung", und der Autor, der nicht erkennt, dass nur "Klasse" enthalten ist, behebt sein Problem. Ich glaube, zukünftige Benutzer werden nach dieser Fehlermeldung suchen, diesen Beitrag finden und glücklich gehen. (Wie ich es oft tue.)
Reginald Blue
Sie können die Situation auch reproduzieren, indem Sie einfach Something2(foo);direkt sagen . Um dies zu verstehen, ist es nicht erforderlich .ToList(), a zu ermitteln List<T>( Tist Ihr Typparameter, der von der generischen Methode deklariert wurde) (a List<T>ist ein IEnumerable<T>).
Jeppe Stig Nielsen
@ReginaldBlue 100%, wollte das gleiche posten. Ähnliche Antworten machen keine doppelten Fragen.
UuDdLrLrSs

Antworten:

218

Die Fehlermeldung ist nicht ausreichend informativ, und das ist meine Schuld. Das tut mir leid.

Das Problem, das Sie haben, ist eine Folge der Tatsache, dass die Kovarianz nur bei Referenztypen funktioniert.

Sie sagen wahrscheinlich gerade "aber IAist ein Referenztyp". Ja, so ist es. Aber du hast nicht gesagt, dass T das gleich ist IA . Sie sagten, dies Tsei ein Typ, der implementiert wird IA , und ein Werttyp kann eine Schnittstelle implementieren . Daher wissen wir nicht, ob Kovarianz funktionieren wird, und wir verbieten es.

Wenn die Kovarianz funktionieren soll, müssen Sie dem Compiler mitteilen, dass der Typparameter ein Referenztyp mit der classEinschränkung sowie der IASchnittstellenbeschränkung ist.

Die Fehlermeldung sollte wirklich sagen, dass die Konvertierung nicht möglich ist, da für die Kovarianz eine Garantie für den Referenztyp erforderlich ist, da dies das grundlegende Problem ist.

Eric Lippert
quelle
3
Warum hast du gesagt, es ist deine Schuld?
user4951
77
@ user4951: Weil ich die gesamte Konvertierungsprüflogik einschließlich der Fehlermeldungen implementiert habe.
Eric Lippert
@BurnsBA Dies ist nur ein "Fehler" im kausalen Sinne - sowohl die technische Implementierung als auch die Fehlermeldung sind vollkommen korrekt. (Es ist nur so, dass die Fehlererklärung der Inkonvertierbarkeit die tatsächlichen Gründe erläutern könnte. Es ist jedoch schwierig, mit Generika gute Fehler zu erzeugen - im Vergleich zu C ++ - Vorlagenfehlermeldungen vor einigen Jahren ist dies klar und prägnant.)
Peter - Reinstate Monica
3
@ PeterA.Schneider: Das weiß ich zu schätzen. Eines meiner Hauptziele beim Entwerfen der Fehlerberichterstattungslogik in Roslyn war es insbesondere, nicht nur zu erfassen, gegen welche Regel verstoßen wurde, sondern darüber hinaus, wo immer möglich, die "Grundursache" zu identifizieren. Wozu soll die Fehlermeldung beispielsweise dienen customers.Select(c=>c.FristName)? Die C # -Spezifikation ist sehr klar, dass dies ein Überlastungsauflösungsfehler ist: Der Satz anwendbarer Methoden mit dem Namen Select, der annehmen kann, dass das Lambda leer ist, ist leer. Aber die Hauptursache ist, dass FirstNameein Tippfehler vorliegt.
Eric Lippert
3
@ PeterA.Schneider: Ich habe viel Arbeit geleistet, um sicherzustellen, dass Szenarien mit generischer Typinferenz und Lambdas die entsprechenden Heuristiken verwenden, um abzuleiten, welche Fehlermeldung dem Entwickler wahrscheinlich am besten hilft. Bei den Konvertierungsfehlermeldungen habe ich jedoch weitaus weniger gute Arbeit geleistet, insbesondere bei Abweichungen. Das habe ich immer bereut.
Eric Lippert
26

Ich wollte nur Erics exzellente Insider-Antwort mit einem Codebeispiel für diejenigen ergänzen, die mit allgemeinen Einschränkungen möglicherweise nicht so vertraut sind.

Ändern Sie Somethingdie Signatur wie folgt: Die classEinschränkung muss an erster Stelle stehen .

public static IList<T> Something<T>(IEnumerable<T> foo) where T : class, IA
Marcell Toth
quelle
2
Ich bin neugierig ... was genau ist der Grund für die Bedeutung der Bestellung?
Tom Wright
5
@ TomWright - die Spezifikation enthält natürlich nicht die Antwort auf viele "Warum?" Fragen, aber in diesem Fall macht es klar, dass es drei verschiedene Arten von Einschränkungen gibt, und wenn alle drei verwendet werden, müssen sie spezifisch seinprimary_constraint ',' secondary_constraints ',' constructor_constraint
Damien_The_Unbeliever
2
@ TomWright: Damien ist richtig; Es gibt keinen besonderen Grund, den ich kenne, außer die Bequemlichkeit des Autors des Parsers. Wenn ich meine druthers die Syntax für Typ Zwänge hätte erheblich sein mehr ausführlich. classist schlecht, weil es "Referenztyp" bedeutet, nicht "Klasse". Ich wäre glücklicher gewesen mit etwas Ausführlichem wiewhere T is not struct
Eric Lippert