Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Interface implemented twice "types may unify"; why does this workaround work?

Tags:

I've run into a compiler error when attempting to implement an interface twice for the same class like so:

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

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

The error:

'Mapper' cannot implement both 'IMapper' and 'IMapper' because they may unify for some type parameter substitutions.

Why does this workaround work? I'm wondering if I've solved the problem or just tricked the compiler.

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. */
}

EDIT: I've updated MyClass, MyClassBase, and IMyInterface to Mapper, MapperBase, and IMapper to represent a more real-world scenario where this issue may present itself.

like image 378
fooser Avatar asked Mar 28 '14 19:03

fooser


1 Answers

Consider this implementation:

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

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

What does MyClass<int, int> implement? It implements IMyInterface<int, int> twice, because IMyInterface<T1, T2> and IMyInterface<T2, T1> unify when T1 and T2 are equal. That's why implementing both IMyInterface<T1, T2> and IMyInterface<T2, T1> on the same class is disallowed. The same reasoning would apply if you tried to implement, for example, IMyInterface<int, T1> and IMyInterface<T2, double>: the type expressions unify for T1 = double, T2 = int.

Consider this implementation:

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. */
}

What you've done is place a priority on IMyInterface<T1, T2> over IMyInterface<T2, T1>. In the event that T1 and T2 are equal and you have an instance of MyClass<T1, T2>, the IMyInterface<T1, T2> implementation will be selected. If you have an instance of MyBaseClass<T1, T2>, the IMyInterface<T2, T1> implementation will be selected.

Here's a toy program that shows you the behaviors. In particular notice the behavior of a_as_i.M(0, 1) and a_as_b.M(0, 1). If you were to implement I<T2, T1> explicitly on B<T1, T2> (by prefixing the method name with I<T2, T1>.), it would be impossible to call it using compile-time syntax. Reflection would be necessary.

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();
    }
}
like image 199
Timothy Shields Avatar answered Nov 11 '22 22:11

Timothy Shields