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


115

Я не можу зрозуміти, чому наступний код C # не компілюється.

Як бачите, у мене є статичний загальний метод Щось із 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"



12
Ви не обмежилися Tтипами посилань. Якщо ви використовуєте умову, where T: class, IAто вона повинна працювати. Відповідна відповідь має більше деталей.
Дірк

2
@Dirk Я не думаю, що це слід позначити як дублікат. Хоча це правда, що проблема поняття тут є проблемою коваріації / протирівномірності за типом значень, конкретний випадок тут - "що означає це повідомлення про помилку", а також автор, не розуміючи просто включення "класу", виправляє свою проблему. Я вірю, що майбутні користувачі знайдуть це повідомлення про помилку, знайдуть цю публікацію та залишать задоволеними. (Як я часто це роблю.)
Reginald Blue

Ви також можете відтворити ситуацію, просто сказавши Something2(foo);прямо. Ходити , .ToList()щоб отримати List<T>( Tваш параметр типу оголошений загальний метод) не потрібно , щоб це зрозуміти (а List<T>це IEnumerable<T>).
Джеппе Стіг Нільсен

@ReginaldBlue 100%, збирався розмістити те саме. Подібні відповіді не викликають повторного запитання.
UuDdLrLrSs

Відповіді:


218

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

Проблема, яку ви відчуваєте, є наслідком того, що коваріація працює лише на еталонних типах.

Ви, мабуть, говорите "але IAце тип посилання" прямо зараз. Так. Але ти не сказав, що T це дорівнює IA . Ви сказали, що Tце тип, який реалізує IA , а тип значення може реалізувати інтерфейс . Тому ми не знаємо, чи працюватиме коваріація, і ми її забороняємо.

Якщо ви хочете, щоб коваріація працювала, ви повинні сказати компілятору, що параметр type є еталонним типом з classобмеженням, а також IAобмеженням інтерфейсу.

Повідомлення про помилку дійсно повинно говорити про те, що перетворення неможливе, оскільки коваріація вимагає гарантії опорного типу, оскільки це є основною проблемою.


3
Чому ви сказали, що ви винні?
user4951

77
@ user4951: Тому що я реалізував всю логіку перевірки конверсій, включаючи повідомлення про помилки.
Ерік Ліпперт

@BurnsBA Це лише "помилка" в причинному сенсі - технічна реалізація, а також повідомлення про помилку є абсолютно правильними. (Це просто те, що заява про помилку щодо непереконливості може пояснити фактичні причини. Але створювати хороші помилки в генеріках важко - порівняно з повідомленнями про помилки шаблону C ++, кілька років тому це є чіткими та стислими.)
Пітер - Відновлення Моніки

3
@ PeterA.Schneider: Я ціную це. Але однією з моїх головних цілей для розробки логіки повідомлення про помилки в Росліні було, зокрема, виявити не лише те, що було порушено правило, але, крім того, визначити «першопричину», де це можливо. Наприклад, яким має бути повідомлення про помилку 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
Ерік Ліпперт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.