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.
In C#, covariance and contravariance enable implicit reference conversion for array types, delegate types, and generic type arguments. Covariance preserves assignment compatibility and contravariance reverses it.
Covariance means that you can use IEnumerable<string> in place where IEnumerable<object> is expected. Contravariance allows you to pass IComparable<object> as an argument of a method taking IComparable<string> .
Covariance and contravariance are terms that refer to the ability to use a more derived type (more specific) or a less derived type (less specific) than originally specified. Generic type parameters support covariance and contravariance to provide greater flexibility in assigning and using generic types.
Contravariance allows you to utilize a less derived type than originally specified, and covariance lets you use a more derived type. In a sense, the reason they were brought to the C# language is so you can extend arrays, delegate types and generic types with polymorphistic features.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With