问题 ICollection 不协变?


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

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

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<> 部分。)

写完数百行后,我发现了 ICollection<> 是不协变的 IEnumerable<>

什么是解决方法?

编辑:

如果我在不破坏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一起使用。


2246
2018-06-08 06:07


起源

receiverSet.Cast <EdgeBase>() - Daniel Möller
原因: 问题 - 约-C-尖锐协方差 你承认。解决方案: 为什么灿我 - 不分配-A-列表的混凝土类型到一个列表 - 的 - 也就是说,concrete's接口。第二部分是经常被问到的。 - nawfal
上面链接上发布的答案建议使用泛型方法但是它可以用于构造函数吗? - Jeffrey Goines
@JeffreyGoines有可能。我会做一个 通用 回答 :) - nawfal


答案:


Eric Lippert说C#只支持类型安全的协方差和逆变。 如果你想到它,制作 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<T> 是协变的,因为你做不到 Add 对它...你只能从中读取 - 这反过来又保留了类型安全性。


14
2018-06-11 05:31



另一方面,一个 ICollection<Mammal> 可能包含一个 Cat,这意味着它无法安全地转换为 ICollection<Dog>。所以它也不能安全地做逆变。 - Eric Lippert
值得注意的是,尽管它很尴尬 IList<T> 实施 IList 以类型安全的方式(它将具有非通用接口以提供只读访问,但让两个接口报告相互矛盾的值 IsReadOnly 本来会让人感到困惑),不会出现这样的问题 ICollection,我认为这很不幸 ICollection<T> 不继承 ICollection因为这意味着对于期望的代码没有好的方法 IEnumerable<Animal> 但给了一个 IList<Cat> 确定其中有多少项。 - supercat
唯一的非泛型成员 ICollection 从类型安全的角度来看,这甚至会有些可疑 CopyTo,而通用 ICollection<T>.CopyTo 在使用值类型时可以提供更好的性能,它不比非泛型版本更安全。一个集合 Cat 可以成功复制到 Object[], Animal[], 要么 Cat[],即使只是非通用的 ICollection<T> 会接受前两个。这两种方法都允许尝试复制到 SiameseCat[];这种尝试的成功或失败可能取决于收集内容。 - supercat
@supercat真的,应该有一个 IEnumerableWithCount<T> 或者其他 ReadOnlyCollection<T> 那没有实现 Add。 - Alxandr
@Alxandr:恕我直言,本来应该是最好的 IEnumerable 拥有大量方法,但在类型加载器中包含一个工具,如果一个类声明自己实现了一个接口而没有声明所有成员,那么类型加载器将生成链接到静态类方法的存根隶属于界面。几乎任何实现 Enumerable 可以实现例如一个 Snapshot 将返回的方法 IEnumerable<T> 即使没有修改,也会如此 由收件人,总会返回相同的序列...... - supercat


你基本上搞乱了类型安全。你的支持收集是一个 ICollection<EdgeBase> (这意味着你可以添加任何 EdgeBase 进入它)但是你传递的是一种非常特殊的类型, HashSet<Edge>。你会如何添加(或删除) AnotherEdgeBaseDerived 成 HashSet<Edge>?如果是这种情况那么这应该是可能的:

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()); 

这意味着你的 receiverSet 和 senderSet 现在与基本列表不同步 Edge。你可以有类型安全或同步(相同的参考), 你不能两者兼得。

我担心如果没有好的解决办法,但有充分的理由。要么通过 HashSet<EdgeBase> 至 Edge 构造函数(更好)或让 EdgeBase 收藏品 ICollection<Edge> (这看起来很奇怪)。

或者,给定设计约束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 实例持有很多类似的实例,包括更多派生的实例?为什么不 EdgeBaseEdge 和 EdgeCollection 分别?但你更了解你的设计。


2
2018-06-12 18:07