【问题标题】:Why does the C# compiler complain that "types may unify" when they derive from different base classes?为什么 C# 编译器从不同的基类派生时会抱怨“类型可能会统一”?
【发布时间】:2021-06-09 03:39:30
【问题描述】:

我目前的非编译代码是这样的:

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 派生类型的单独非泛型类?

【问题讨论】:

  • 我认为 TItem 应该读 TA,不是吗?
  • 它不太可能是编译器中的错误。公平地说,错误消息确实使用了“可能统一”这个词,我的猜测是因为你同时使用了这两个接口。
  • 编辑:没关系,我认为 B 是一个类型参数。 是什么阻止您将相同的东西传递给类型参数B,因为您传递给TA
  • @JoshEinstein:B 不是类型参数,而是实际类型。唯一的类型参数是TA
  • @Ramhound 我不明白为什么它“不太可能”成为编译器错误。我看不出其他解释,真的。这似乎是一个容易犯的错误,而且这种情况并不经常出现。

标签: c# generics


【解决方案1】:

这是 C# 4 规范第 13.4.2 节的结果,其中指出:

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

注意那里的第二句话。

因此这不是编译器中的错误;编译器是正确的。有人可能会争辩说这是语言规范中的一个缺陷。

一般来说,几乎在所有必须推导出关于泛型类型的事实的情况下,都会忽略约束。约束主要用于确定泛型类型参数的有效基类,仅此而已。

不幸的是,这有时会导致语言过于严格,正如您所发现的那样。


两次实现“相同”接口通常是一种不好的代码气味,在某种程度上只能通过泛型类型参数来区分。例如,有class C : IEnumerable&lt;Turtle&gt;, IEnumerable&lt;Giraffe&gt; 是很奇怪的——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 的哪个实现被调用???

有关此问题的更多想法,请参阅本文和 cmets:

http://blogs.msdn.com/b/ericlippert/archive/2007/11/09/covariance-and-contravariance-in-c-part-ten-dealing-with-ambiguity.aspx

【讨论】:

  • @asawyer:是的。在这种情况下,CLR 进行哪种转换是实现定义的。事实上,您可以构建更复杂的情况,其中由 CLI 规范定义,实现应该采用哪一种,而 CLR 采用“错误”的一种。我不确定 CLR 团队和 CLI 规范所有者是否已经解决了这个争议;我个人的看法是,在最有可能的情况下,CLI 规则实际上给出的结果不如 CLR 规则好。 (并不是说这些情况很常见;它们都是非常奇怪的极端情况。)
  • 当然,接口是类型参数是没有意义的;我错误地认为使用具体类可以解决这个问题。至于场景,该类是一个 Saga,它必须实现多个消息处理程序(每个消息对应一个作为 saga 一部分的消息)。大约有 10 个几乎相同的 saga,其唯一区别是其中一条消息的确切具体类型,但我不能有一个抽象消息处理程序,所以我想我会尝试使用通用基类并只使用一堆存根类;唯一的选择是大量的复制和粘贴。
  • @Eric:如果您考虑像IComparable&lt;T&gt;IEquatable&lt;T&gt; 这样的接口而不是IEnumerable&lt;T&gt;,则更有可能多次实现相同的通用接口。拥有一个可以与不止一种类型进行比较的对象是很合理的......事实上,对于这种确切的情况,我已经多次遇到类型统一问题。
  • @Eric,LBushkin 是正确的。 some 使用是无意义的,并不意味着 所有 使用是无意义的。这个问题也困扰着我,因为我正在实现一个抽象解析接口 IFoo 它实现了一个分段的 IParseable, IParseable, ... 并在 IParseable 上定义了一组扩展方法,但现在这似乎是不可能的。 C#/CLR 有很多像这样令人沮丧的极端情况。
  • @EricLippert 我是否正确假设已添加功能以允许“某些”条件下的歧义?看来您现在(.NET 4.5)允许表达上述示例,而仍然不允许使用通用版本,即允许使用 class Danger : IFoo&lt;IABC&gt;, IFoo&lt;DEF&gt; {...},而仍然不允许使用 class Danger&lt;T1,T2&gt; : IFoo&lt;T1&gt;, IFoo&lt;T2&gt;。似乎消歧是通过确定性仲裁完成的,其中使用了第一个定义的最具体的实现(在多个适用的情况下)?
【解决方案2】:

显然这是 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 编译器进行编译。
  • @Aaronaught:然后尝试将IIFoo 设为abstract class
  • 确实可以编译,尽管它显然阻止了我从不同的基类派生。我认为我实际上可能能够完成这项工作,尽管它将在类层次结构中要求另一个中间(hackish/无用)级别。
  • 如果我没记错的话,这实际上违反了规范。 Eric 引用的部分指定它不应该是可能的,不是吗?
  • @Aaronaught:一个类型通过其继承链实现两次相同的接口是合法的(用于隐藏和减轻脆弱的基类)。但是同一个类型在同一个地方两次实现同一个接口是不合法的——因为没有“更好”的接口。
【解决方案3】:

如果你把一个接口放在基类上,你可以偷偷摸摸。

public interface IFoo<T> 
{
}

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

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

我怀疑这是可行的,因为如果类型确实“统一”了,那么派生类的实现显然会获胜。

【讨论】:

    【解决方案4】:

    在此处查看我对基本相同问题的回复: https://stackoverflow.com/a/12361409/471129

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

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

    在此处查看我的帖子,并在另一个上下文中提供了一个完整的示例。 https://stackoverflow.com/a/12361409/471129

    基本上,您所做的就是向IIndexer添加另一个类型参数,使其变为IIndexer &lt;TKey, TValue, TDifferentiator&gt;

    然后当你使用它两次时,你将“First”传递给第一次使用,并将“Second”传递给第二次使用

    所以,Test类变成:Test&lt;TKey, TValue&gt; : IIndexer&lt;TKey, TValue, First&gt;, IIndexer&lt;TValue, TKey, Second&gt;

    因此,您可以这样做new Test&lt;int,int&gt;()

    First 和 Second 是微不足道的:

    interface First { }
    
    interface Second { }
    

    【讨论】:

      【解决方案5】:

      现在猜一猜……

      不能在外部程序集中声明 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) { }
      }
      

      【讨论】:

      • 不同意>>Couldn't A, B and C be declared in outside assemblies, where the type hierarchy may change after the compilation of MyFoo&lt;T&gt;, bringing havoc into the world?.
      • 更改类型层次结构将使 any 程序失效;对我来说,编译器会根据类型层次结构正确现在以外的任何东西做出决定,这对我来说没有多大意义(尽管我想这对我来说意义不亚于目前的错误本身......)至于解决方法,嗯,它会编译但它不再是通用的,所以它并没有太大帮助。
      【解决方案6】:

      嗯,这个呢:

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

      【讨论】:

      • 不,错误出在类本身;它不依赖于接口的实现方式。
      【解决方案7】:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-09-12
        • 1970-01-01
        • 1970-01-01
        • 2012-09-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-12-22
        相关资源
        最近更新 更多