用多个泛型类型的C#泛型导致允许和不允许的歧义。

6

我最近写了下面的代码,出乎意料地它竟然能够通过编译:

public class MyGeneric<U, V> {
  MyGeneric(U u) { ... }
  MyGeneric(V v) { ... }
  public void Add(U u, V v) { ... }
  public void Add(V v, U u) { ... }
}

如果我按照以下方式使用这个类,那么如果我调用Add方法,就会收到"模棱两可的构造函数引用"和"模棱两可的调用"的错误提示。
var myVar = new MyGeneric<int, int>(new MyIntComparer());

显然,当我使用 int 和 double 作为泛型类型时,没有任何歧义,除非我同时使用两个 int 类型,这会分别分配给两个 double 类型。
var myVar = new MyGeneric<int, double>(new MyIntComparer());
myVar.Add(3, 5);

于是我认为下面的语句也是可以的,但令人惊讶的是我遇到了错误。为什么下面的语句不能编译?

public interface IMyInterface<T, S> {
  void Add(T t, S s);
}

public class MyGeneric<U, V> : IMyInterface<U, V>, IMyInterface<V, U> {
  public MyGeneric(U u) { }
  public MyGeneric(V v) { }
  void IMyInterface<U, V>.Add(U u, V v) { ... }
  void IMyInterface<V, U>.Add(V v, U u) { ... }
}

无论我使用隐式还是显式接口实现,编译器都会指出:“‘MyGeneric’无法同时实现‘IMyInterface’和‘IMyInterface’,因为它们可能在某些类型参数替代中统一”。而为什么第一个写法是被允许的呢?

2
虽然这两篇博客文章讨论了一个泛型方法和一个非泛型方法在某些类型参数下可能会具有相同签名的情况,但它们也可能适用于你的两种类型参数的情况。(关于为什么允许这样做的答案基本上是“我们在C# 2.0中允许了这样做,现在要改变已经太晚了”。) - Rawling
谢谢提供这些链接,这正是我在寻找的编译器实现解释。 - AlexH
@Rawling,感谢您分享这个有趣的链接。 - Andreas
2个回答

4

1- 为什么以下内容无法编译?

问题的一部分在该帖子中:为什么C#编译器在它们从不同的基类派生时抱怨“类型可能合并”?

C# 4规范的第13.4.2节规定:

泛型类型声明实现的接口必须对所有可能的构造类型保持唯一性。如果没有这个规则,将无法确定对于某些构造类型要调用哪个正确的方法。

2- 为什么第一个内容可以通过编译?

编译器在编译时执行泛型类型检查,C# 4规范的第7.4.3.5节规定:

虽然声明的签名必须是唯一的,但是可能会出现类型参数替换导致相同签名的情况。在这种情况下,上面的重载解析的决策规则将选择最具体的成员。以下示例显示根据此规则有效和无效的重载:

interface I1<T> {...}
interface I2<T> {...}
class G1<U>
{
    int F1(U u);                    // Overload resulotion for G<int>.F1
    int F1(int i);                  // will pick non-generic
    void F2(I1<U> a);               // Valid overload
    void F2(I2<U> a);
}
class G2<U,V>
{
    void F3(U u, V v);          // Valid, but overload resolution for
    void F3(V v, U u);          // G2<int,int>.F3 will fail
    void F4(U u, I1<V> v);      // Valid, but overload resolution for   
   void F4(I1<V> v, U u);       // G2<I1<int>,int>.F4 will fail
    void F5(U u1, I1<V> v2);    // Valid overload
    void F5(V v1, U u2);
    void F6(ref U u);               // valid overload
    void F6(out V v);
}

感谢详细的解释。创建MyGeneric<int, int>确实会基本上实现相同的接口两次,这就是为什么它会抱怨参数替换的原因。 - Andreas

1

这是语言规范的一部分,正如在这里接受的答案中所解释的:

为什么C#编译器会在它们从不同的基类派生而来时抱怨“类型可能合并”?

C# 4 规范的第13.4.2节规定:

如果从 C 创建的任何可能的构造类型,在将类型参数替换到 L 后,导致 L 中的两个接口相同,则 C 的声明无效。约束声明在确定所有可能的构造类型时不被考虑。

我猜你两个示例之间的区别在于第二个使用接口(根据语言规范检查重复项),但第一个使用类型(尽管可能会引起歧义,正如你所看到的,但不检查重复项)。


谢谢,是的,这基本上会从同一个接口派生两次。 - Andreas

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