问题
The purpose of this is to synchronize two collections, sender-side & receiver-side, containing a graph edge, so that when something happens (remove edge, add edge, etc) both sides are notified.
To do so, (back-)references to the collections were included in the element in collections
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;
}
Deletion (Disconnect) was ok , but the problem occurred during creation:
HashSet<Edge> receiverSet, senderSet;
var edge = new Edge(receiverSet, senderSet); // Can't convert Edge to EdgeBase!
Although Edge is derived from EdgeBase, this is illegal.
(The problem is Edge part, not HashSet<> part.)
After writing hundreds of lines I found out ICollection<> is not covariant as is IEnumerable<>.
What could be a workaround?
EDIT:
If I wrote the code above while not breaking the C#'s covariance rules it would have been like this:
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() {...}
}
But this is illegal; 'U' can't be used with formal parameter T.
回答1:
Eric Lippert said that C# will only support type-safe covariance and contravariance. If you would think of it, making ICollection covariant is not type-safe.
Let's say you have
ICollection<Dog> dogList = new List<Dog>();
ICollection<Mammal> mammalList = dogList; //illegal but for the sake of showing, do it
mammalList.Add(new Cat());
Your mammalList (which is actually a dogList) would now then contain a Cat.
IEnumerable<T> is covariant because you cannot Add to it... you can only read from it -- which, in turn, preserves type-safety.
回答2:
You're messing with type safety basically. Your backing collection is an ICollection<EdgeBase> (which means you can add any EdgeBase into it) but what you're passing a very specific type, HashSet<Edge>. How would you add (or remove) AnotherEdgeBaseDerived into HashSet<Edge>? If that is the case then this should be possible:
edge.Add(anotherEdgeBaseDerived); // which is weird, and rightly not compilable
If you perform a cast yourself and pass a separate list then that's compilable. Something like:
HashSet<Edge> receiverSet, senderSet;
var edge = new Edge(receiverSet.Cast<EdgeBase>().ToList(),
senderSet.Cast<EdgeBase>().ToList());
which means your receiverSet and senderSet are now out of sync with base list in Edge. You can either have type safety or sync (same reference), you cant have both.
I worry if there exist no good solution to this, but for a good reason. Either pass HashSet<EdgeBase> to Edge constructor (better) or let EdgeBase collections be ICollection<Edge> (which seems very odd to do).
Or, the best you can have given the design constraints imo is generic
class EdgeBase<T> where T : EdgeBase<T>
{
}
class Edge : EdgeBase<Edge>
{
public Edge(ICollection<Edge> rCol, ICollection<Edge> sCol) : base(rCol, sCol)
{
}
}
Now you can call as usual:
HashSet<Edge> receiverSet = new HashSet<Edge>(), senderSet = new HashSet<Edge>();
var edge = new Edge(receiverSet, senderSet);
To me the fundamental problem is the fuzzy and smelly design. An EdgeBase instance holding a lot of similar instances, including more derived ones? Why not EdgeBase, Edge and EdgeCollection separately? But you know your design better.
来源:https://stackoverflow.com/questions/16996626/icollectiont-not-covariant