Чому компілятор C # скаржиться на те, що "типи можуть об'єднуватися", коли вони походять від різних базових класів?


77

Мій поточний некомпілюючий код подібний до цього:

public abstract class A { }

public class B { }

public class C : A { }

public interface IFoo<T>
{
    void Handle(T item);
}

public class MyFoo<TA> : IFoo<TA>, IFoo<B>
    where TA : A
{
    public void Handle(TA a) { }
    public void Handle(B b) { }
}

Компілятор C # відмовляється компілювати це, посилаючись на таке правило / помилку:

'MyProject.MyFoo <TA>' не може реалізувати як 'MyProject.IFoo <TA>', так і "MyProject.IFoo <MyProject.B>", оскільки вони можуть уніфікувати для деяких підстановок параметрів типу

Я розумію, що означає ця помилка; якщо TAвзагалі може бути що-небудь, то технічно це також може бути таким, Bщо внесе неоднозначність щодо двох різних Handleреалізацій.

Але ТА не може бути нічим. Виходячи з ієрархії типів, TA це не може бути B- принаймні, я не думаю, що може. TAповинен походити з A, що не походить з B, і, очевидно, у C # /. NET немає успадкування декількох класів.

Якщо я видалю загальний параметр і замінюю TAна C, або навіть A, він компілюється.

То чому я отримую цю помилку? Це помилка або загальна неінтелектуальна інформація компілятора, чи щось ще мені не вистачає?

Чи є якесь обхідне рішення, або мені просто доведеться повторно реалізувати MyFooзагальний клас як окремий не-загальний клас для кожного можливого TAпохідного типу?


2
Я думаю, що TItem повинен читати TA, ні?
Джефф,

Навряд чи це буде помилка компілятора. Чесно кажучи, повідомлення про помилку використовує слова "може об'єднати", я припускаю, що це тому, що ви використовуєте обидва інтерфейси.
Гонча охорони

Редагувати: Неважливо, я прочитав це як B як параметр типу. <strike> Що заважає вам передавати одне і те ж параметр типу, Bяк і ви TA? </strike>
Джош,

1
@JoshEinstein: Bне є параметром типу, це фактичний тип. Єдиним параметром типу є TA.
Aaronaught

1
@Ramhound Я не розумію, чому це "малоймовірно" бути помилкою компілятора. Насправді я не бачу іншого пояснення. Це здається легкою помилкою і ситуацією, яка трапляється не так часто.
kmkemp

Відповіді:


50

Це наслідок розділу 13.4.2 специфікації C # 4, який говорить:

Якщо будь-який можливий побудований тип, створений з C, після заміни аргументів типу на L призведе до того, що два інтерфейси в L будуть ідентичними, тоді оголошення C є недійсним. Декларації обмежень не враховуються при визначенні всіх можливих побудованих типів.

Зверніть увагу, що там є друге речення.

Отже, це не помилка компілятора; правильний компілятор. Можна стверджувати, що це недолік мовної специфікації.

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

На жаль, це іноді призводить до ситуацій, коли мова є надмірно суворою, як ви виявили.


Взагалі, поганий запах коду - реалізовувати "однаковий" інтерфейс двічі, певним чином відрізняючись лише загальними аргументами типу. Це дивно, наприклад, мати class C : IEnumerable<Turtle>, IEnumerable<Giraffe>- що таке C , що і послідовність черепах, і послідовність жирафів, в той же час ? Чи можете ви описати те, що ви тут намагаєтесь зробити? Може бути кращий шаблон для вирішення справжньої проблеми.


Якщо насправді ваш інтерфейс точно такий, як ви описуєте:

interface IFoo<T>
{
    void Handle(T t);
}

Тоді багаторазове успадкування інтерфейсу представляє ще одну проблему. Ви можете обґрунтовано вирішити зробити цей інтерфейс противаріантним:

interface IFoo<in T>
{
    void Handle(T t);
}

Тепер припустимо, що маєте

interface IABC {}
interface IDEF {}
interface IABCDEF : IABC, IDEF {}

І

class Danger : IFoo<IABC>, IFoo<IDEF>
{
    void IFoo<IABC>.Handle(IABC x) {}
    void IFoo<IDEF>.Handle(IDEF x) {}
}

А тепер все стає по-справжньому божевільним ...

IFoo<IABCDEF> crazy = new Danger();
crazy.Handle(null);

Яку реалізацію Handle викликають ???

Дивіться цю статтю та коментарі, щоб отримати більше думок з цього питання:

http://blogs.msdn.com/b/ericlippert/archive/2007/11/09/covariance-and-contravariance-in-c-part-ten-dealing-with-ambiguity.aspx


1
@asawyer: Так. Визначення реалізації визначає, яке перетворення CLR виконує в цьому випадку. І справді, ви можете побудувати більш складні ситуації , в яких він є , певні CLI специфікації , яка одна реалізація повинна прийняти, і CLR приймає «невірну». Я не впевнений, що команда CLR та власники специфікації CLI ще вирішили цю суперечку; моя особиста думка полягає в тому, що правила CLI насправді дають менш хороші результати, ніж правила CLR в найбільш імовірних випадках. (Не те, щоб будь-який з цих випадків був загальним; всі вони досить дивні кутові справи.)
Ерік Ліпперт,

3
Безумовно, немає сенсу, коли інтерфейси є типовими параметрами; Я неправильно припускав, що використання конкретних класів допоможе вирішити проблему. Що стосується сценарію, клас - це Saga, який повинен реалізувати кілька обробників повідомлень (по одному для кожного повідомлення, що є частиною саги). Є близько 10 майже однакових саг, єдиною відмінністю яких є точний конкретний тип одного з повідомлень, але я не можу мати абстрактний обробник повідомлень, тому я думав, що спробую використати загальний базовий клас і просто використати купа класів заглушок; єдина альтернатива - багато копіювання та вставлення.
Aaronaught

22
@Eric: Ситуація реалізації такої ж загальний інтерфейс з більш ніж один раз, більш імовірно , якщо врахувати , як інтерфейс IComparable<T>або , IEquatable<T>швидше за , ніж IEnumerable<T>. Цілком правдоподібно мати об’єкт, який можна порівняти з кількома типами типів ... насправді, я кілька разів стикався з проблемами об’єднання типів саме для цього випадку.
Л.Бушкін,

4
@ Ерік, Л.Бушкін правильний. Те, що деякі види використання є нечутливими, не означає, що всі способи використання є нечутливими. Ця проблема просто вкусила і мене, оскільки я реалізовував абстрактний інтерфейс синтаксичного аналізу IFoo <T0, T1, ..>, який реалізує поштучно IParseable <T0>, IParseable <T1>, ... із набором методів розширення, визначеним на IParseable , але зараз це здається неможливим. У C # / CLR є багато таких неприємних випадків, як цей.
naasking

1
@EricLippert Чи правильно я вважаю, що були додані функції, що дозволяють неоднозначність у "певних" умовах? Здається, вам зараз (.NET 4.5) дозволено висловити наведений приклад, тоді як загальна версія все ще заборонена, тобто class Danger : IFoo<IABC>, IFoo<DEF> {...}дозволена, тоді як class Danger<T1,T2> : IFoo<T1>, IFoo<T2>все ще заборонена. Здається, неоднозначність здійснюється детермінованим арбітражем, де використовується перша визначена найбільш конкретна реалізація (там, де застосовується кілька)?
micdah

8

Очевидно, це було за задумом, як обговорювалося в Microsoft Connect:

І обхідний шлях полягає у визначенні іншого інтерфейсу як:

public interface IIFoo<T> : IFoo<T>
{
}

Потім застосуйте це замість цього:

public class MyFoo<TA> : IIFoo<TA>, IFoo<B>
    where TA : A
{
    public void Handle(TA a) { }
    public void Handle(B b) { }
}

Тепер він компілюється чудово, моно .


Проголосував за посилання Connect; на жаль, обхідний шлях не компілюється з компілятором Microsoft.
Aaronaught

@Aaronaught: Спробуйте зробити тоді. IIFooabstract class
Nawaz

Це справді компілюється, хоча це, очевидно, заважає мені походити з іншого базового класу. Я думаю, що я справді зможу зробити цю роботу, хоча це вимагатиме ще одного проміжного (хакерського / марного) рівня в ієрархії класів.
Aaronaught

Це насправді порушує специфікацію, якщо я не помиляюся. У розділі, який цитував Ерік, зазначено, що це не повинно бути можливим, чи не так?
конфігуратор

2
@Aaronaught: Легально для типу впроваджувати один і той же інтерфейс двічі через ланцюжок успадкування (для приховування та пом'якшення крихкого базового класу). Але не законно, щоб один і той самий тип реалізовував один і той же інтерфейс двічі в одному і тому ж місці - адже немає «кращого».
конфігуратор

4

Ви можете сховати його під радаром, якщо покладете один інтерфейс на базовий клас.

public interface IFoo<T> {
}

public class Foo<T> : IFoo<T>
{
}

public class Foo<T1, T2> : Foo<T1>, IFoo<T2>
{
}

Я підозрюю, що це працює, тому що якщо типи "об'єднують", зрозуміло, що реалізація похідного класу виграє.


2

Дивіться мою відповідь на те саме питання тут: https://stackoverflow.com/a/12361409/471129

Певною мірою це можна зробити! Я використовую метод диференціації, замість того, щоб класифікатор (и) обмежував типи.

Це не уніфікує, насправді це може бути краще, ніж якби це сталося, тому що ви можете дратувати окремі інтерфейси.

Дивіться мій пост тут, із повністю робочим прикладом в іншому контексті. https://stackoverflow.com/a/12361409/471129

В основному, що ви робите, це додаєте інший параметр типу до IIndexer , щоб він став IIndexer <TKey, TValue, TDifferentiator>.

Тоді ви, використовуючи його двічі, передаєте "Перший" першому використанню, а "Другий" - другому

Отже, клас Test стає: class Test<TKey, TValue> : IIndexer<TKey, TValue, First>, IIndexer<TValue, TKey, Second>

Таким чином, ви можете зробити new Test<int,int>()

де Перший та Другий є тривіальними:

interface First { }

interface Second { }

1

Я знаю, що з моменту публікації потоку минув деякий час, але для тих, хто за допомогою цієї пошукової системи переходить до цієї теми. Зверніть увагу, що нижче "Base" означає базовий клас для TA та B.

public class MyFoo<TA> : IFoo<Base> where TA : Base where B : Base
{
    public void Handle(Base obj) 
    { 
       if(obj is TA) { // TA specific codes or calls }
       else if(obj is B) { // B specific codes or calls }
    }

}

0

Припускаючи зараз ...

Чи не можуть бути оголошені A, B і C у зовнішніх збірках, де ієрархія типів може змінитися після компіляції MyFoo <T>, що спричинить хаос у світ?

Просте обхідне рішення - просто застосувати Handle (A) замість Handle (TA) (і використовувати IFoo <A> замість IFoo <TA>). За допомогою Handle (TA) і так неможливо зробити набагато більше, ніж методи доступу з A (через обмеження A: TA).

public class MyFoo : IFoo<A>, IFoo<B> {
    public void Handle(A a) { }
    public void Handle(B b) { }
}

Ні цьому >> Couldn't A, B and C be declared in outside assemblies, where the type hierarchy may change after the compilation of MyFoo<T>, bringing havoc into the world?.
Nawaz

Зміна ієрархії типів призведе до втрати практично будь-якої програми; для мене немає абсолютно сенсу в тому, що компілятор базуватиме свої рішення на чому-небудь, крім того, що є ієрархією типів зараз (хоча, мабуть, для мене це має не менший сенс, ніж сама помилка на даний момент. ..) Що стосується обхідного шляху, ну, це буде компіляція, але це вже не загальний характер, тому насправді це не дуже допомагає.
Aaronaught

0

Хм, а що з цим:

public class MyFoo<TA> : IFoo<TA>, IFoo<B>
    where TA : A
{
    void IFoo<TA>.Handle(TA a) { }
    void IFoo<B>.Handle(B b) { }
}

2
Ні, помилка в самому класі; це не залежить від способу реалізації інтерфейсу.
Кверті
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.