当C#编译器派生自不同的基类时,为什么会抱怨“类型可能统一”?


76

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

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 <TA>'无法同时实现'MyProject.IFoo <TA>'和'MyProject.IFoo <MyProject.B>',因为它们可能会为某些类型参数替换统一

我了解此错误的含义;如果TA可以是任何东西,那么从技术上讲它也可能是一个B会引入两种不同Handle实现方式的歧义的东西。

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

如果删除通用参数并替换TAC或什至A,它将编译。

那么,为什么会出现此错误?是编译器中的错误或普遍的非智能性,还是我遗漏了其他东西?

是否有任何解决方法,或者我将不得不MyFoo针对每个可能的TA派生类型将泛型类重新实现为单独的非泛型类?


2
我认为TItem应该读TA,不是吗?
杰夫

它不太可能是编译器中的错误。公平地说,错误消息确实使用了“可能统一”一词,我猜这是因为您同时使用了两个接口。
安全猎犬

编辑:没关系,我读为B是类型参数。<strike>是什么让您无法传递与B传递给参数相同的参数TA?</ strike>
乔什

1
@JoshEinstein:B不是类型参数,它是实际类型。唯一的类型参数是TA
亚伦诺特,2011年

1
@Ramhound我不明白为什么它“不太可能”成为编译器错误。我看不到其他解释。这似乎是一个容易犯的错误,而且这种情况很少发生。
kmkemp

Answers:


49

这是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


void IFoo<IABC>.Handle(IABC x)我猜我们正在看到内部实施细节?
2011年

2
当然,当接口是类型参数时,这毫无意义。我错误地认为使用具体的类可以解决此问题。对于场景,该类是一个Saga,它必须实现多个消息处理程序(对于属于saga的每条消息,一个处理程序)。大约有10个几乎相同的sagas,它们的唯一区别是其中一条消息的确切具体类型,但是我没有抽象的消息处理程序,因此我认为我会尝试使用通用基类,而仅使用一堆存根类;唯一的选择是大量复制粘贴。
亚伦诺特,2011年

22
@Eric:如果您将接口视为IComparable<T>IEquatable<T>而不是,则多次执行同一泛型接口的可能性更大IEnumerable<T>。有一个对象可以与多种类型的对象进行比较是很合理的……实际上,对于这种确切的情况,我已经多次遇到类型统一问题。
LBushkin

4
@ Eric,LBushkin是正确的。这一些用途是无意义的,并不意味着所有的用途是无意义的。这个问题也困扰着我,因为我正在实现抽象解析接口IFoo <T0,T1,..>,该接口实现了分段IParseable <T0>,IParseable <T1>,...以及在IParseable上定义的一组扩展方法。 ,但现在看来这是不可能的。C#/ CLR有许多令人沮丧的极端情况,例如这样。
naasking

1
@EricLippert我是否正确地假设已经添加了一些功能,以允许在“某些”条件下产生歧义?似乎您现在(.NET 4.5)可以表达上面的示例,而仍然不允许通用版本,即class Danger : IFoo<IABC>, IFoo<DEF> {...}允许,而class Danger<T1,T2> : IFoo<T1>, IFoo<T2>仍然不允许。消除歧义似乎是通过确定性仲裁来完成的,其中使用了第一个定义的最具体的实现方式(适用多个情况)?
micdah 2012年

8

显然,这是由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可以很好地编译。


已为连接链接投票;不幸的是,该解决方法无法与Microsoft编译器一起编译。
2011年

@Aaronaught:尝试制作IIFoo一个abstract class
Nawaz

尽管确实阻止了我从其他基类派生,但确实可以编译。我认为我实际上可以完成这项工作,尽管它将在类层次结构中强制要求另一个中间(hackish /没用)级别。
亚伦诺特,2011年

如果我没记错的话,这实际上违反了规范。埃里克(Eric)引用的部分指定了它不可能,不是吗?
Configurator

2
@Aaronaught:对于一个类型来说,通过其继承链实现相同的接口两次是合法的(用于隐藏并减轻脆弱的基类)。但是,对于同一类型,在同一位置两次实现同一接口是不合法的-因为没有“更好”的接口。
配置器

4

如果将一个接口放在基类上,则可以将其隐藏起来。

public interface IFoo<T> {
}

public class Foo<T> : IFoo<T>
{
}

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

我怀疑这行得通,因为如果类型做到“统一”,则显然派生类的实现是成功的。


2

在这里查看我对基本相同的问题的答复: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>()

其中第一和第二很简单:

interface First { }

interface Second { }

1

我知道自该主题发布以来已经有一段时间了,但是对于那些通过搜索引擎寻求帮助的人。注意,“ 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 }
    }

}

0

现在猜一猜...

不能在外部程序集中声明A,B和C,在外部程序集中,在编译MyFoo <T>之后类型层次结构可能会发生变化,从而给整个世界带来破坏吗?

简单的解决方法是仅实现Handle(A)而不是Handle(TA)(并使用IFoo <A>代替IFoo <TA>)。无论如何,与从A访问方法(由于A:TA约束)相比,使用Handle(TA)不能做更多的事情。

public class MyFoo : IFoo<A>, IFoo<B> {
    public void Handle(A a) { }
    public void Handle(B b) { }
}

不对此>> Couldn't A, B and C be declared in outside assemblies, where the type hierarchy may change after the compilation of MyFoo<T>, bringing havoc into the world?
纳瓦兹

更改类型层次结构将使几乎所有程序无效。它没有一大堆的道理,我认为编译器将立足于比其他任何事情作出决定的类型层次就是正确的,现在(虽然,我想这是没有少意义,我比错误本身的时刻。 ..)至于解决方法,可以编译,但不再通用,因此并没有太大帮助。
亚伦诺特,2011年

0

嗯,这呢:

public class MyFoo<TA> : IFoo<TA>, IFoo<B>
    where TA : A
{
    void IFoo<TA>.Handle(TA a) { }
    void IFoo<B>.Handle(B b) { }
}

2
不,错误在于类本身;它不依赖于接口的实现方式。
Qwertie 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.