Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ICollection<T> not Covariant?

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.

like image 922
Jeffrey Goines Avatar asked Jun 08 '13 06:06

Jeffrey Goines


People also ask

What is the difference between Covariance and contravariance?

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.

Why is IEnumerable covariant?

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> .

What is covariance and Contravariance in generics?

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.

What is contravariant c#?

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.


2 Answers

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.

like image 192
aiapatag Avatar answered Oct 12 '22 20:10

aiapatag


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.

like image 37
nawfal Avatar answered Oct 12 '22 19:10

nawfal