接口实现了两次“类型可能统一”; 为什么这个解决方法有效?

21

我在尝试为同一个类实现两次接口时遇到了编译器错误,就像这样:

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 ,以表示此问题可能出现的更真实的场景。


如果在MyClass中没有实现接口的方法,它是否能编译通过?如果是,那么你就得到了答案。 - Dhawalk
3个回答

22
考虑这个实现:

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

MyClass<int, int>实现了什么?它实现了IMyInterface<int, int>两次,因为当T1T2相等时,IMyInterface<T1, T2>IMyInterface<T2, T1>会合并。这就是为什么在同一个类上同时实现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>。如果 T1T2 相等,并且你有一个 MyClass<T1, T2> 实例,则会选择 IMyInterface<T1, T2> 实现。如果你有一个 MyBaseClass<T1, T2> 实例,则会选择 IMyInterface<T2, T1> 实现。
以下是一个示例程序,展示了这些行为。特别注意 a_as_i.M(0, 1)a_as_b.M(0, 1) 的行为。如果你要在 B<T1, T2> 上显式实现 I<T2, T1>(通过在方法名前加上 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();
    }
}

如果您添加了示例方法以显示第二个示例创建的优先级,我会认为答案更好。 - Euphoric
我的直觉告诉我,任何实现Foo<T1,T2>和Foo<T2,T1>的东西,在T1 = T2时应该对方法进行对称处理(因此方法的选择是无关紧要的)。但是我猜一般来说没有必须这样做的理由。所以这个错误只是为了避免任何“未定义行为”的可能性? - Alexis Beingessner
@AlexisBeingessner 我建议避免使用这种特定的模式,除非你能解释为什么使用它是有意义的。 :) - Timothy Shields
@TimothyShields 我完全同意。我无法想象这种模式的好用例(因此我特别困惑,为什么这被认为是一个问题)。也许如果你被其他人编码到一个角落里,不得不进行一些严重的黑客攻击。据我所知,这是彻头彻尾的疯狂。 - Alexis Beingessner
@AlexisBeingessner和TimothyShields,我已经更新了我的问题中的类和接口名称,这些是我实际使用时遇到问题时的名称。Mapper类的设计意图是在“域”对象和“数据库”对象之间建立一个双向映射(用于读取和写入操作)。 - fooser
@TimothyShields 我没有看到任何疯狂的地方。一个非常常见的情况是尝试为多个类型实现标准接口,例如 IEnumerable<T>。随机举例:class Family : IEnumerable<Person>, IEnumerable<Pet>class Room : IEnumerable<Door>, IEnumerable<Window>。还有其他类似意义的接口。 - AnorZaken

1
你并没有欺骗编译器,而是使得不会有竞争的函数定义。假设你的接口有一个函数string Convert(T1 t1, T2 t2)。在你的第一段(非法)代码中,如果你使用MyClass<string, string>,你将会有两个相同函数的实例。通过使用基类,这两个实例将会在MyClassBaseMyClass中,所以在MyClass中的那个会隐藏另外一个,而不是与之冲突。是否有效取决于你自己。

0

我认为问题是由于编译器无法确定在T1T2类型之一是另一个的后代(或是相同类型)时,应该调用哪个实现方法。


想象一下,如果你实例化了类MyClass<int, int>,它应该做什么。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接