Почему компилятор С# жалуется, что "типы могут объединяться", когда они происходят из разных базовых классов?

Мой текущий некомпилирующий код похож на этот:

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

Компилятор С# отказывается компилировать его, ссылаясь на следующее правило/ошибку:

'MyProject.MyFoo < ТА > ' не может реализовать оба параметра "MyProject.IFoo <TA> "; и 'MyProject.IFoo < MyProject.B > ' потому что они могут объединяться для некоторых подстановок параметров типа

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

Но TA не может быть чем угодно. На основе иерархии типов TA не может быть B - по крайней мере, я не думаю, что это возможно. TA должен выводиться из A, который не выводится из B и, очевидно, нет наследования множественного класса в С#/. NET.

Если я удалю общий параметр и заменим TA на C или даже A, он скомпилируется.

Так почему я получаю эту ошибку? Это ошибка или вообще неинтеллектуальность компилятора, или есть что-то еще, что мне не хватает?

Есть ли какое-либо обходное решение или мне просто придется повторно реализовать общий класс MyFoo как отдельный не-общий класс для каждого возможного производного типа TA?

Ответ 1

Это следствие раздела 13.4.2 спецификации С# 4, в котором говорится:

Если какой-либо возможный построенный тип, созданный из C, после того, как аргументы типа будут заменены на L, вызовет идентичность двух интерфейсов в L, тогда объявление C недействительно. Объявления Constraint не учитываются при определении всех возможных построенных типов.

Обратите внимание, что второе предложение там.

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

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

К сожалению, это иногда приводит к ситуациям, когда язык является излишне строгим, как вы обнаружили.


В общем случае плохой запах кода реализует "тот же" интерфейс дважды, в некотором смысле различающийся только аргументами общего типа. Причудливо, например, иметь 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

Ответ 2

По-видимому, это было по дизайну, как обсуждалось в 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) { }
}

Теперь он компилируется отлично, моно.

Ответ 3

Смотрите мой ответ в основном на тот же вопрос: fooobar.com/questions/89934/...

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

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

См. мой пост здесь, с полностью работающим примером в другом контексте. fooobar.com/questions/89934/...

В основном, то, что вы делаете, это добавить другой тип параметра в IIndexer, чтобы он стал IIndexer <TKey, TValue, TDifferentiator>.

Затем, когда вы используете его дважды, вы передаете "Первый" для первого использования, а "Второй" для второго использования

Итак, класс Test становится: class Test<TKey, TValue> : IIndexer<TKey, TValue, First>, IIndexer<TValue, TKey, Second>

Таким образом, вы можете сделать new Test<int,int>()

где First и Second тривиальны:

interface First { }

interface Second { }

Ответ 4

Я знаю, что прошло некоторое время с тех пор, как был опубликован поток, но для тех, кто пришел к этому потоку через поисковую систему для получения помощи. Обратите внимание, что "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 }
    }

}

Ответ 5

Теперь угадаем...

Невозможно ли объявить 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) { }
}

Ответ 6

Хмм, как насчет этого:

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