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

Я столкнулся с ошибкой компилятора при попытке дважды реализовать интерфейс для одного и того же класса, например:

public class Mapper<T1, T2> : IMapper<T1, T2>, IMapper<T2, T1>
{
   /* implementation for IMapper<T1, T2> here.  */

   /* implementation for IMapper<T2, T1> here.  */
}

Ошибка:

«Mapper» не может реализовать одновременно «IMapper» и «IMapper», потому что они могут объединяться для подстановок некоторых параметров типа.

Почему это обходное решение работает? Мне интересно, решил ли я проблему или просто обманул компилятор.

public class Mapper<T1, T2> : MapperBase<T1, T2>, IMapper<T1, T2>
{
    /* implementation for IMapper<T1, T2> here. */
}

public class MapperBase<T1, T2> : IMapper<T2, T1>
{
    /* implementation for IMapper<T2, T1> here. */
}

РЕДАКТИРОВАТЬ: я обновил MyClass, MyClassBase и IMyInterface до Mapper, MapperBase и IMapper, чтобы представить более реальный сценарий, при котором эта проблема может возникнуть.


person fooser    schedule 28.03.2014    source источник
comment
компилируется ли он без реализации методов интерфейса в MyClass? если да, то у тебя есть ответ   -  person Dhawalk    schedule 28.03.2014


Ответы (3)


Рассмотрим эту реализацию:

public class MyClass<T1, T2> : IMyInterface<T1, T2>, IMyInterface<T2, T1>
{
   /* implementation for IMyInterface<T1, T2> here.  */

   /* implementation for IMyInterface<T2, T1> here.  */
}

Что MyClass<int, int> реализует? Он реализует IMyInterface<int, int> дважды, потому что IMyInterface<T1, T2> и IMyInterface<T2, T1> объединяются, когда T1 и T2 равны. Вот почему реализация IMyInterface<T1, T2> и IMyInterface<T2, T1> в одном классе запрещена. То же самое можно было бы применить, если бы вы попытались реализовать, например, IMyInterface<int, T1> и IMyInterface<T2, double>: выражения типа унифицируют для T1 = double, T2 = int.

Рассмотрим эту реализацию:

public class MyClass<T1, T2> : MyClassBase<T1, T2>, IMyInterface<T1, T2>
{
    /* implementation for IMyInterface<T1, T2> here. */
}

public class MyClassBase<T1, T2> : IMyInterface<T2, T1>
{
    /* implementation for IMyInterface<T2, T1> here. */
}

Что вы сделали, так это поставили IMyInterface<T1, T2> приоритет перед IMyInterface<T2, T1>. Если T1 и T2 равны и у вас есть экземпляр MyClass<T1, T2>, будет выбрана реализация IMyInterface<T1, T2>. Если у вас есть экземпляр MyBaseClass<T1, T2>, будет выбрана реализация IMyInterface<T2, T1>.

Вот игрушечная программа, которая показывает вам поведение. В частности, обратите внимание на поведение a_as_i.M(0, 1) и a_as_b.M(0, 1). Если бы вы явно реализовали I<T2, T1> на B<T1, T2> (добавив к имени метода префикса I<T2, T1>.), было бы невозможно вызвать его, используя синтаксис времени компиляции. Размышление было бы необходимо.

interface I<T1, T2>
{
    void M(T1 x, T2 y);
}

class A<T1, T2> : B<T1, T2>, I<T1, T2>
{
    public void M(T1 x, T2 y)
    {
        Console.WriteLine("A: M({0}, {1})", x, y);
    }
}

class B<T1, T2> : I<T2, T1>
{
    public void M(T2 x, T1 y)
    {
        Console.WriteLine("B: M({0}, {1})", x, y);
    }
}

class Program
{
    static void Main(string[] args)
    {
        //Outputs "A: M(0, 1)"
        var a = new A<int, int>();
        a.M(0, 1);

        //Outputs "B: M(0, 1)"
        var b = new B<int, int>();
        b.M(0, 1);

        //Outputs "A: M(0, 1)" because I<T1, T2>
        //takes precedence over I<T2, T1>
        var a_as_i = a as I<int, int>;
        a_as_i.M(0, 1);

        //Outputs "B: M(0, 1)" despite being called on an instance of A
        var a_as_b = a as B<int, int>;
        a_as_b.M(0, 1);

        Console.ReadLine();
    }
}
person Timothy Shields    schedule 28.03.2014
comment
Я бы рассмотрел ответ лучше, если бы вы добавили примеры методов, которые показывают приоритет, созданный вторым примером. - person Euphoric; 29.03.2014
comment
Моя интуиция подсказывает мне, что все, что реализует Foo ‹T1, T2› и Foo ‹T2, T1›, должно иметь методы симметричного поведения, когда T1 = T2 (и, следовательно, выбор метода не имеет значения). Но я думаю, что в целом нет причин, почему это должно быть. Итак, эта ошибка предназначена для того, чтобы избежать любой возможности неопределенного поведения? - person Alexis Beingessner; 29.03.2014
comment
@AlexisBeingessner Я бы посоветовал избегать этого конкретного шаблона, если вы не можете объяснить, почему имеет смысл его использовать. :) - person Timothy Shields; 29.03.2014
comment
@TimothyShields Я полностью согласен. Я не могу представить себе хороший вариант использования этого шаблона (поэтому меня особенно смущает, что это даже идентифицировано как проблема). Может быть, если вас загнали в угол, и вам нужно заняться серьезным взломом. Насколько я могу судить, это безумие. - person Alexis Beingessner; 29.03.2014
comment
@AlexisBeingessner и TimothyShields, я обновил имена классов и интерфейсов в моем вопросе до имен, которые я фактически использовал, когда столкнулся с проблемой. Замысел, лежащий в основе дизайна класса Mapper, состоял в том, чтобы иметь двустороннее сопоставление (для использования как в операциях чтения, так и в операциях записи) между объектами моего домена и объектами базы данных. - person fooser; 29.03.2014
comment
@TimothyShields Я не вижу безумия. Очень распространенный сценарий - попытка реализовать стандартные интерфейсы, такие как IEnumerable<T>, для более чем одного типа. Случайные примеры: class Family : IEnumerable<Person>, IEnumerable<Pet>, class Room : IEnumerable<Door>, IEnumerable<Window>. Есть и другие интерфейсы, для которых это имеет аналогичный смысл. - person AnorZaken; 04.06.2015

Вы не обманули компилятор, вы сделали его так, чтобы у вас не было конкурирующих определений функций. Предположим, в вашем интерфейсе есть функция string Convert(T1 t1, T2 t2). С вашим первым (незаконным) кодом, если вы выполнили MyClass<string, string>, у вас будет 2 экземпляра одной и той же функции. С вашим базовым классом эти 2 экземпляра будут в MyClassBase и MyClass, поэтому один в MyClass будет СКРЫТЬ другой, а не конфликтовать с ним. Полагаю, сработает это или нет, решать вам.

person Kyle W    schedule 28.03.2014

Я считаю, что проблема вызвана неспособностью компилятора определить, какой из реализованных методов должен быть вызван, если один из типов T1 или T2 является потомком другого (или имеет тот же тип).
Представьте, что он должен делать, если вы создаете экземпляр класса MyClass<int, int>.

person Dmitry    schedule 28.03.2014