Проблема понимания ковариационной контравариантности с дженериками в C #

115

Я не могу понять, почему следующий код C # не компилируется.

Как видите, у меня есть статический универсальный метод Something с IEnumerable<T>параметром (и Tон должен быть IAинтерфейсом), и этот параметр не может быть неявно преобразован в IEnumerable<IA>.

Какое объяснение? (Я не ищу обходной путь, просто чтобы понять, почему это не работает).

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

Ошибка, я попал в Something2(bar)очередь:

Аргумент 1: невозможно преобразовать из System.Collections.Generic.List в System.Collections.Generic.IEnumerable.

BenLaz
источник
12
Вы не ограничивались Tссылочными типами. Если вы используете это условие, where T: class, IAоно должно работать. Связанный ответ содержит более подробную информацию.
Дирк
2
@Dirk Я не думаю, что это следует отмечать как дубликат. Хотя это правда, что проблема концепции здесь - проблема ковариации / контравариантности перед лицом типов значений, конкретным случаем здесь является то, «что означает это сообщение об ошибке», а также то, что автор не понимает, что простое включение «класса» решает его проблему. Я верю, что будущие пользователи будут искать это сообщение об ошибке, найдут это сообщение и уйдут счастливыми. (Как я часто это делаю.)
Реджинальд Блю
Вы также можете воспроизвести ситуацию, просто сказав Something2(foo);прямо. Чтобы понять это, не нужно идти вокруг, .ToList()чтобы получить List<T>( Tэто ваш параметр типа, объявленный универсальным методом) (a List<T>- это IEnumerable<T>).
Jeppe Stig Nielsen
@ReginaldBlue 100%, собирался опубликовать то же самое. Подобные ответы не создают повторяющийся вопрос.
UuDdLrLrSs

Ответы:

218

Сообщение об ошибке недостаточно информативно, и это моя вина. Извини за это.

Проблема, с которой вы столкнулись, является следствием того факта, что ковариация работает только с ссылочными типами.

Вы, наверное, сейчас говорите «но IAэто ссылочный тип». Да, это так. Но вы не сказали, что T это равно IA . Вы сказали, что Tэто тип, который реализует IA , а тип значения может реализовывать интерфейс . Поэтому мы не знаем, сработает ли ковариация, и запрещаем ее.

Если вы хотите, чтобы ковариация работала, вы должны сообщить компилятору, что параметр типа является ссылочным типом с classограничением, а также с IAограничением интерфейса.

В сообщении об ошибке действительно должно быть сказано, что преобразование невозможно, потому что ковариация требует гарантии ссылочного типа, поскольку это фундаментальная проблема.

Эрик Липперт
источник
3
Почему ты сказал, что это твоя вина?
user4951 07
77
@ user4951: Потому что я реализовал всю логику проверки преобразования, включая сообщения об ошибках.
Эрик Липперт
@BurnsBA Это всего лишь «ошибка» в причинном смысле - технически реализация и сообщение об ошибке совершенно верны. (Просто в заявлении об ошибке о неконвертируемости могут быть подробно описаны реальные причины. Но создавать хорошие ошибки с помощью обобщений сложно - по сравнению с сообщениями об ошибках шаблона C ++ несколько лет назад это ясно и лаконично.)
Питер - Восстановить Монику,
3
@ PeterA.Schneider: Я ценю это. Но одна из моих основных целей при разработке логики сообщения об ошибках в Roslyn заключалась, в частности, в том, чтобы зафиксировать не только то, какое правило было нарушено, но, более того, выявить «основную причину» там, где это возможно. Например, для чего должно быть сообщение об ошибке customers.Select(c=>c.FristName)? В спецификации C # четко указано, что это ошибка разрешения перегрузки : набор применимых методов с именем Select, которые могут принимать эту лямбду, пуст. Но основная причина в том, что FirstNameэто опечатка.
Эрик Липперт
3
@ PeterA.Schneider: Я проделал большую работу, чтобы убедиться, что в сценариях, включающих вывод общего типа и лямбда-выражения, используются соответствующие эвристики, чтобы определить, какое сообщение об ошибке лучше всего поможет разработчику. Но я сделал гораздо менее хорошую работу с сообщениями об ошибках преобразования, особенно в том, что касается дисперсии. Я всегда сожалел об этом.
Эрик Липперт
26

Я просто хотел дополнить отличный инсайдерский ответ Эрика примером кода для тех, кто может быть не знаком с общими ограничениями.

Измените Somethingподпись следующим образом: classограничение должно быть первым .

public static IList<T> Something<T>(IEnumerable<T> foo) where T : class, IA
Марселл Тот
источник
2
Мне любопытно ... в чем именно причина важности заказа?
Том Райт
5
@TomWright - в спецификации, конечно же, нет ответа на многие вопросы "Почему?" вопросы, но в данном случае проясняет, что есть три различных типа ограничений, и когда все три используются, они должны быть конкретнымиprimary_constraint ',' secondary_constraints ',' constructor_constraint
Damien_The_Unbeliever
2
@TomWright: Дэмиен прав; мне известно только об удобстве автора синтаксического анализатора. Если бы у меня были мои помощники, синтаксис для ограничений типа был бы значительно более подробным. classплохо, потому что означает «ссылочный тип», а не «класс». Я был бы более доволен чем-нибудь подробным, например,where T is not struct
Эрик Липперт