为什么c#编译器抱怨“类型可能统一”?当它们从不同的基类派生时

本文关键字:派生 基类 编译器 类型 为什么 | 更新日期: 2023-09-27 18:10:26

我当前的非编译代码类似于:

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可以是任何东西,那么它在技术上也可以是B,这将在两种不同的Handle实现中引入歧义。

但是TA 不可能是任何东西。基于类型层次结构,TA 不能B -至少,我不认为可以。TA必须从A派生,而不能从B派生,显然c#/. net中没有多类继承。

如果我删除泛型参数并将TA替换为C,甚至A,它将编译。

为什么会出现这个错误?它是编译器中的错误或一般的不智能,还是我遗漏了其他东西?

是否有任何解决方法,或者我只是要重新实现MyFoo泛型类作为一个单独的非泛型类为每一个可能的TA派生类型?

为什么c#编译器抱怨“类型可能统一”?当它们从不同的基类派生时

这是c# 4规范第13.4.2节的结果,其中规定:

如果从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);

Handle的哪个实现被调用??

关于这个问题的更多想法,请参阅本文和评论:

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) { }
}

现在通过mono编译得很好。

如果你把一个接口放在一个基类上,你可以偷偷地把它隐藏起来。

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

我怀疑这是有效的,因为如果类型"统一";很明显,派生类的实现获胜。

看我对基本相同问题的回答:https://stackoverflow.com/a/12361409/471129

在某种程度上,这是可以做到的!我使用了一种区分方法,而不是限定符来限制类型。

它没有统一,事实上,它可能比统一更好,因为你可以把单独的接口分开。

在这里看到我的帖子,在另一个上下文中有一个完整的工作示例。https://stackoverflow.com/a/12361409/471129

基本上,你要做的就是给IIndexer添加另一个类型参数,这样它就变成了IIndexer <TKey, TValue, TDifferentiator>

那么当你使用它两次时,你将"First"传递给第一次使用,"Second"传递给第二次使用

所以,类Test变成:class Test<TKey, TValue> : IIndexer<TKey, TValue, First>, IIndexer<TValue, TKey, Second>

因此,您可以执行new Test<int,int>()

其中First和Second是平凡的:

interface First { }
interface Second { }

现在猜一猜…

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) { }
}

我知道自从这个帖子发布以来已经有一段时间了,但是对于那些通过搜索引擎寻求帮助的人来说。请注意,'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 }
    }
}