Problem z rozumieniem kontrawariancji kowariancji za pomocą typów ogólnych w języku C #

115

Nie mogę zrozumieć, dlaczego nie można kompilować następującego kodu C #.

Jak widać, mam statyczną metodę ogólną Coś z IEnumerable<T>parametrem (i Tjest ograniczone jako IAinterfejs), a tego parametru nie można niejawnie przekonwertować na IEnumerable<IA>.

Jakie jest wyjaśnienie? (Nie szukam obejścia, tylko po to, aby zrozumieć, dlaczego to nie działa).

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

Błąd w Something2(bar)kolejce:

Argument 1: nie można konwertować z „System.Collections.Generic.List” na „System.Collections.Generic.IEnumerable”

BenLaz
źródło
12
Nie ograniczyłeś się Tdo typów referencyjnych. Jeśli użyjesz tego warunku where T: class, IA, powinno działać. Połączona odpowiedź zawiera więcej szczegółów.
Dirk
2
@Dirk Nie sądzę, aby to oznaczało jako duplikat. Chociaż prawdą jest, że problem z koncepcją jest tutaj problemem kowariancji / kontrawariancji w obliczu typów wartości, konkretnym przypadkiem jest tutaj „co oznacza ten komunikat o błędzie”, a autor, który nie zdaje sobie sprawy, że samo włączenie „klasy” rozwiązuje ten problem. Wierzę, że przyszli użytkownicy wyszukają ten komunikat o błędzie, znajdą ten post i wyjdą szczęśliwi. (Jak często to robię.)
Reginald Blue
Możesz również odtworzyć sytuację, mówiąc Something2(foo);wprost. Chodzenie po okolicy w .ToList()celu uzyskania a List<T>( Tjest to parametr typu zadeklarowany przez metodę ogólną) nie jest konieczne, aby to zrozumieć (a List<T>jest IEnumerable<T>).
Jeppe Stig Nielsen
@ReginaldBlue 100%, zamierzał opublikować to samo. Podobne odpowiedzi nie dublują pytania.
UuDdLrLrSs

Odpowiedzi:

218

Komunikat o błędzie nie zawiera wystarczających informacji i to moja wina. Przepraszam za to.

Problem, którego doświadczasz, jest konsekwencją faktu, że kowariancja działa tylko na typach referencyjnych.

Prawdopodobnie mówisz teraz „ale IAjest typem referencyjnym”. Tak to jest. Ale nie powiedziałeś, że T to równa się IA . Powiedziałeś, że Tjest to typ, który implementuje IA , a typ wartości może implementować interfejs . Dlatego nie wiemy, czy kowariancja zadziała i nie zezwalamy na to.

Jeśli chcesz, aby kowariancja działała, musisz powiedzieć kompilatorowi, że parametr typu jest typem referencyjnym z classograniczeniem, a także z IAograniczeniem interfejsu.

Komunikat o błędzie powinien naprawdę mówić, że konwersja nie jest możliwa, ponieważ kowariancja wymaga gwarancji typu odniesienia, ponieważ jest to podstawowy problem.

Eric Lippert
źródło
3
Dlaczego powiedziałeś, że to twoja wina?
user4951
77
@ user4951: Ponieważ zaimplementowałem całą logikę sprawdzania konwersji, w tym komunikaty o błędach.
Eric Lippert
@BurnsBA To jest tylko „błąd” w sensie przyczynowym - technicznie implementacja oraz komunikat o błędzie są całkowicie poprawne. (Tyle tylko, że stwierdzenie błędu o nieodwracalności mogłoby rozwinąć rzeczywiste przyczyny. Ale tworzenie dobrych błędów za pomocą typów generycznych jest trudne - w porównaniu do komunikatów o błędach szablonów C ++ kilka lat temu jest to przejrzyste i zwięzłe).
Piotr - Przywróć Monikę
3
@ PeterA.Schneider: Doceniam to. Jednak jednym z moich głównych celów przy projektowaniu logiki raportowania błędów w Roslyn było w szczególności wychwycenie nie tylko tego, która reguła została naruszona, ale ponadto zidentyfikowanie „głównej przyczyny” tam, gdzie to możliwe. Na przykład, do czego ma być przeznaczony komunikat o błędzie customers.Select(c=>c.FristName)? Specyfikacja języka C # jest bardzo jasna, że ​​jest to błąd rozpoznawania przeciążenia : zestaw odpowiednich metod o nazwie Select, które mogą przyjąć, że lambda jest pusty. Ale główną przyczyną jest FirstNameliterówka.
Eric Lippert
3
@ PeterA.Schneider: Wykonałem dużo pracy, aby upewnić się, że scenariusze obejmujące wnioskowanie o typie ogólnym i wyrażenia lambda wykorzystywały odpowiednią heurystykę, aby wywnioskować, który komunikat o błędzie może najbardziej pomóc deweloperowi. Ale wykonałem znacznie mniej dobrą robotę z komunikatami o błędach konwersji, szczególnie jeśli chodzi o wariancję. Zawsze tego żałowałem.
Eric Lippert
26

Chciałem tylko uzupełnić doskonałą odpowiedź Erica, podając przykład kodu dla tych, którzy mogą nie być zaznajomieni z ogólnymi ograniczeniami.

Zmień Something„s podpis tak: classograniczenie musi przyjść pierwszy .

public static IList<T> Something<T>(IEnumerable<T> foo) where T : class, IA
Marcell Toth
źródło
2
Jestem ciekawy ... jaki dokładnie jest powód znaczenia tego zamówienia?
Tom Wright
5
@TomWright - specyfikacja oczywiście nie zawiera odpowiedzi na wiele pytań „Dlaczego?” pytania, ale w tym przypadku wyraźnie widać, że istnieją trzy różne rodzaje ograniczeń, a kiedy wszystkie trzy są używane, muszą być konkretnieprimary_constraint ',' secondary_constraints ',' constructor_constraint
Damien_The_Unbeliever
2
@TomWright: Damien ma rację; nie ma szczególnego powodu, dla którego jestem świadomy, poza wygodą autora parsera. Gdybym miał wybór, składnia ograniczeń typu byłaby znacznie bardziej szczegółowa. classjest zły, ponieważ oznacza „typ odniesienia”, a nie „klasę”. Byłbym szczęśliwszy z czymś gadatliwy jakwhere T is not struct
Eric Lippert