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


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

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 'не может реализовать оба' MyProject.IFoo 'и' MyProject.IFoo ' потому что они могут объединяться для некоторых подстановок параметров типа

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

но та не могу быть что угодно. На основе иерархии типов,TAне могу быть

7 68

7 ответов:

это является следствием раздела 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);

какая реализация дескриптора получает называется???

см. эту статью и комментарии для получения дополнительной информации по этому вопросу:

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

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

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

смотрите мой ответ в основном один и тот же вопрос здесь: https://stackoverflow.com/a/12361409/471129

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

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

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

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

тогда вы, когда вы используете его дважды, вы передаете "первый "для 1-го использования, а" второй " для 2-го использования

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

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

где Первый и второй тривиальны:

interface First { }

interface Second { }

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

}

теперь предположим...

Не могли бы A, B и C быть объявлены во внешних сборках, где иерархия типов может измениться после компиляции MyFoo, принося хаос в мир?

простой обходной путь - это просто реализовать Handle(A) вместо Handle (TA) (и использовать IFoo вместо IFoo). Вы не можете сделать гораздо больше с Handle(TA), чем методы доступа из A (из-за ограничения A : TA) в любом случае.

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

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

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

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

public interface IFoo<T> {
}

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

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