【问题标题】:ICollection<T> not Covariant?ICollection<T> 不是协变的?
【发布时间】:2013-06-04 12:00:00
【问题描述】:

这样做的目的是同步两个集合,发送方和接收方,包含一个图边,以便在发生某些事情时(删除边、添加边等)通知双方。

为此,对集合的(反向)引用包含在集合中的元素中

class EdgeBase {
    EdgeBase(ICollection<EdgeBase> rCol, ICollection<EdgeBase> sCol)     
    { RecvCol=rCol;  SendCol=sCol; }      
    ICollection<EdgeBase> RecvCol;      
    ICollection<EdgeBase> SendCol;       
    public virtual void Disconnect() // Synchronized deletion         
    { RecvCol.Remove(this);  SendCol.Remove(this); }                 
}         
class Edge : EdgeBase {       
    Edge(ICollection<EdgeBase> rCol, ICollection<EdgeBase> sCol)     
    : base(rCol, sCol) {}
    int Weight;     
}      

删除(断开)没问题,但创建过程中出现问题:

HashSet<Edge> receiverSet, senderSet;
var edge = new Edge(receiverSet, senderSet); // Can't convert Edge to EdgeBase!

虽然Edge 派生自EdgeBase,但这是非法的。 (问题是Edge 部分,而不是HashSet&lt;&gt; 部分。)

写了数百行后,我发现ICollection&lt;&gt; 不像IEnumerable&lt;&gt; 那样协变。

有什么办法可以解决?

编辑:

如果我在不违反 C# 的协方差规则的情况下编写上面的代码,它会是这样的:

public class EdgeBase<T, U>
    where T : ICollection<U<T>> // illegal
    where U : EdgeBase<T, U>    // legal, but introduces self-reference
{
    public EdgeBase(T recvCol, T sendCol) {...}
    protected T ReceiverCollection;
    protected T SenderCollection;
    public virtual void Disconnect() {...}
}

但这是非法的; 'U' 不能与形参 T 一起使用。

【问题讨论】:

标签: c# generics inheritance covariance


【解决方案1】:

Eric Lippert said that C# will only support type-safe covariance and contravariance. 如果你想的话,使ICollection 协变不是类型安全的。

假设你有

ICollection<Dog> dogList = new List<Dog>();
ICollection<Mammal> mammalList = dogList; //illegal but for the sake of showing, do it
mammalList.Add(new Cat());

您的mammalList(实际上是dogList)现在将包含Cat

IEnumerable&lt;T&gt; 是协变的,因为您不能 Add 对它...您只能从中读取 - 这反过来又保留了类型安全性。

【讨论】:

  • 反之,ICollection&lt;Mammal&gt; 可能包含Cat,这意味着它不能安全地转换为ICollection&lt;Dog&gt;。所以它也不能安全地进行逆变。
  • 值得注意的是,虽然 IList&lt;T&gt; 以类型安全的方式实现 IList 会很尴尬(它将具有提供只读访问的非通用接口,但是让两个接口报告 IsReadOnly 的冲突值会令人困惑),ICollection 不会有这样的问题,我认为不幸的是 ICollection&lt;T&gt; 没有继承 ICollection,因为这意味着有对于需要 IEnumerable&lt;Animal&gt; 但给出 IList&lt;Cat&gt; 来确定其中有多少项目的代码,没有好的方法。
  • 非泛型ICollection 的唯一成员,从类型安全的角度来看甚至有点可疑,这将是CopyTo,而泛型ICollection&lt;T&gt;.CopyTo 在使用时可以提供更好的性能值类型,它并不比非泛型版本更安全。 Cat 的集合可以成功复制到 Object[]Animal[]Cat[],即使只有非通用 ICollection&lt;T&gt; 会接受前两个。这两种方法都允许尝试复制到SiameseCat[];这种尝试的成功或失败可能取决于收集内容。
  • @supercat 真的,应该有一个 IEnumerableWithCount&lt;T&gt; 或其他 ReadOnlyCollection&lt;T&gt; 没有实现 Add
  • @Alxandr:恕我直言,最好的办法是IEnumerable 拥有大量方法,但在类型加载器中包含一个设施,这样如果一个类声明自己为实现一个接口但没有声明所有成员,类型加载器将生成存根,该存根链接到与该接口关联的静态类的方法。 Enumerable 的几乎任何实现都可以实现,例如一个Snapshot 方法,它会返回一个IEnumerable&lt;T&gt;,如果没有被接收者修改,它总是会返回相同的序列...
【解决方案2】:

你基本上是在搞乱类型安全。您的支持集合是ICollection&lt;EdgeBase&gt;(这意味着您可以将任何EdgeBase 添加到其中),但是您传递的是一个非常特定的类型HashSet&lt;Edge&gt;。您将如何将AnotherEdgeBaseDerived 添加(或删除)到HashSet&lt;Edge&gt;?如果是这样,那么这应该是可能的:

edge.Add(anotherEdgeBaseDerived); // which is weird, and rightly not compilable

如果您自己执行转换并传递一个单独的列表,那么这是可编译的。比如:

HashSet<Edge> receiverSet, senderSet;
var edge = new Edge(receiverSet.Cast<EdgeBase>().ToList(), 
                    senderSet.Cast<EdgeBase>().ToList()); 

这意味着您的receiverSetsenderSet 现在与Edge 中的基本列表不同步。您可以拥有类型安全或同步(相同的引用),您不能同时拥有。

我担心是否没有好的解决方案,但这是有充分理由的。要么将HashSet&lt;EdgeBase&gt; 传递给Edge 构造函数(更好),要么让EdgeBase 集合成为ICollection&lt;Edge&gt;(这样做似乎很奇怪)。

或者,考虑到 imo 的设计约束,你能给出的最好的就是通用的

class EdgeBase<T> where T : EdgeBase<T>
{

}

class Edge : EdgeBase<Edge>
{
    public Edge(ICollection<Edge> rCol, ICollection<Edge> sCol) : base(rCol, sCol)
    {

    }
}

现在你可以照常打电话了:

HashSet<Edge> receiverSet = new HashSet<Edge>(), senderSet = new HashSet<Edge>();
var edge = new Edge(receiverSet, senderSet);

对我来说,根本问题是模糊和臭的设计。一个EdgeBase 实例包含许多类似实例,包括更多派生实例?为什么不分别EdgeBaseEdgeEdgeCollection?但你更了解你的设计。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-03-25
    • 2015-09-08
    • 2010-12-05
    • 2020-02-10
    • 1970-01-01
    • 2011-01-22
    相关资源
    最近更新 更多