【问题标题】:C# Why can I not add a hashset typed to a subclass to a hashset typed to its superclassC# 为什么我不能将类型化到子类的哈希集添加到类型化到其超类的哈希集
【发布时间】:2013-05-27 06:50:13
【问题描述】:

为什么在下面的代码中会出现编译错误(见注释行)?

    public void Test()
    {
        HashSet<HashSet<Animal>> setWithSets = new HashSet<HashSet<Animal>>();
        HashSet<Cat> cats = new HashSet<Cat>();
        setWithSets.Add(cats); // Compile error
    }

    private class Animal { }

    private class Cat : Animal { }

VS2012 给了我两个错误,第一个是重要的:

  • 错误 2 参数 1:无法从 'System.Collections.Generic.HashSet' 转换为 'System.Collections.Generic.HashSet'
  • 错误 1 ​​'System.Collections.Generic.HashSet>.Add(System.Collections.Generic.HashSet) 的最佳重载方法匹配' 有一些无效的参数

我的问题是:为什么我不能在“setWithSets”中添加“cats”?

【问题讨论】:

标签: c# generics collections hashset


【解决方案1】:

即使Cat 派生自AnimalHashSet&lt;Cat&gt; 派生自HashSet&lt;Animal&gt; 也是不正确的。 (HashSet&lt;Anything&gt; 的唯一基类是 object 类。)

要获得您想要的行为,HashSet&lt;T&gt; 泛型类型需要在其类型参数T协变。但事实并非如此,原因有两个:

  1. 在 C# 中,只有泛型接口和泛型委托类型可以是协变或逆变的。 HashSet&lt;&gt; 是一个类。
  2. 您不仅可以读取HashSet&lt;&gt;,还可以添加(以及做其他事情)。因此协方差在逻辑上是不可能的。否则可以将HashSet&lt;Cat&gt; 视为HashSet&lt;Animal&gt;,然后将Dog 添加到其中。但是一组猫不允许狗。

如果您将 HashSet&lt;T&gt; 更改为例如 IReadOnlyCollection&lt;T&gt;(请参阅 .NET 4.5 documentation: IReadOnlyCollection&lt;out T&gt; Interface),则一切正常,因为后一种类型 (1) 是一个接口,(2) 只允许读取,并且 (3)因此,该类型的作者决定应用一个标记“我在T 中是协变的”。

【讨论】:

    【解决方案2】:

    为了更好地理解为什么不允许这样做,请考虑以下程序。

    setOfSets.First().Add(new Dog()); 行对编译器来说是可以接受的,因为动物的集合肯定可以包含Dog 的实例。问题是集合中的第一个动物集合是Cat实例的集合,而Dog没有扩展Cat

    class Animal { }
    class Cat : Animal { }
    class Dog : Animal { }
    
    class Program {
        static void Main(string[] args) {
    
            // This is a collection of collections of animals.
            HashSet<HashSet<Animal>> setOfSets = new HashSet<HashSet<Animal>>();
    
            // Here, we add a collection of cats to that collection.
            HashSet<Cat> cats = new HashSet<Cat>();
            setOfSets.Add(cats);
    
            // And here, we add a dog to the collection of cats. Sorry, kitty!
            setOfSets.First().Add(new Dog());
        }
    }
    

    【讨论】:

    • 我正要对苹果和香蕉做同样的事情:)
    • 像 Java 这样有趣的语言支持数组中的协方差,这会导致一些运行时检查来阻止这些类型的事情,从而影响性能
    • 将此标记为“答案”,因为它简单明了地说明了我的思维问题。不过,我真的很欣赏其他后续行动,因为它们让我对这个主题有了更深入的了解。
    • @RicardoRodrigues .NET 还具有数组类型的这种“疯狂”协方差,导致在所有写入时都进行类型检查,并造成相同的性能损失。从 .NET 1 开始就是这样(即在泛型之前)。
    【解决方案3】:

    因为HashSet&lt;Cat&gt; 不是从HashSet&lt;Animal&gt; 派生的,这是您想做的事情所必需的。

    可以做的是将Cat 添加到HashSet&lt;Animal&gt;,因为Cat 派生自Animal不能做的是将HashSet&lt;Cat&gt; 添加到HashSet&lt;HashSet&lt;Animal&gt;&gt;

    您可能认为可以使用协方差,它允许您这样做:

    IEnumerable<Cat> cats = new List<Cat>();
    IEnumerable<Animal> animals = cats;
    

    这是可行的,因为这是 IEnumerable 的接口声明:

    public interface IEnumerable<out T> : IEnumerable
    {
        IEnumerator<T> GetEnumerator();
    }
    

    注意到“out T”了吗?这就是协方差。它基本上允许您在泛型类型的类上具有类似继承的行为。请注意,您只能在接口上声明协方差。现在我们来看一下HashSet实现的接口ISet:

    public interface ISet<T> : ICollection<T>, IEnumerable<T>, IEnumerable
    {
        ...
    }
    

    如您所见,没有“out”关键字。这意味着你不能这样做:

    ISet<Cat> cats = new HashSet<Cat>();
    ISet<Animal> animals = cats;
    

    【讨论】:

      【解决方案4】:

      你得到一个编译器错误,因为 HashSet 的类型构造函数是invariant

      有关术语不变量的解释,请查看Covariance and contravariance

      【讨论】:

      • 我不会说 constructor 是不变的;我想说类(或类型)是不变的(在T)。
      • @JeppeStigNielsen 我修正了措辞以匹配 wiki 文章。
      • 如果你稍微改变一下措辞,这将是正确的答案,它不是 HashSet 的构造函数是不变的,它是 T 中的 HashSet 本身。只是为了添加更多信息,以便有可能,HashSet 必须在其类型参数 T 中是协变的,就像 IEnumerable 是自 .NET 4.0
      猜你喜欢
      • 2021-04-25
      • 1970-01-01
      • 2011-08-10
      • 1970-01-01
      • 1970-01-01
      • 2011-03-24
      • 2019-05-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多