Partager via


CovariantCollection

One of the known limitations of .NET generics is the limited support for covariance. This is sometimes a challenge when it comes to collections. As an example, consider this simple abstract class:

 public abstract class MyClass
 {
     protected MyClass() { }
 }

Okay, so that's not the most exciting class you've ever seen, but it will serve my demonstration purposes nicely. Assume that you have another class that exposes a list of MyClass objects:

 public abstract class MyContainer
 {
     protected MyContainer() { }
  
     public abstract IList<MyClass> MyClasses { get; }
 }

Here's a simple implementation that doesn't cause any problems at all:

 public class SimpleContainer : MyContainer
 {
     private List<MyClass> myClasses_;
  
     public SimpleContainer()
     {
         this.myClasses_ = new List<MyClass>();
     }
  
     public override IList<MyClass> MyClasses
     {
         get { return this.myClasses_; }
     }
 }

This implementation simply encapsulates a List<MyClass>, which implements IList<MyClass>, and the compiler is happy. Now, assume that you need to write a more complex implementation of MyContainer. This class exclusively uses a list of MyDerivedClass, which derives from MyClass. For type safety reasons, you prefer to work on a list of MyDerivedClass, since you have complex internal logic and you want to ensure that you only add MyDerivedClass instances to the list. Here's an outline of what I'm describing:

 public class NotSoSimpleContainer : MyContainer
 {
     private List<MyDerivedClass> myClasses_;
  
     public NotSoSimpleContainer()
     {
         this.myClasses_ = new List<MyDerivedClass>();
     }
 }

This class must also implement the MyClasses abstract property, or it isn't going to compile. However, due to the lack of covariance support in .NET, this attempt isn't going to compile either:

 // Doesn't compile
 public override IList<MyClass> MyClasses
 {
     get { return this.myClasses_; }
 }

The problem is that myClasses_ is an instance of List<MyDerivedClass>, and although MyDerivedClass derives from MyClass, List<MyDerivedClass> doesn't implement IList<MyClass>, but rather IList<MyDerivedClass>, which is an entirely different type. Any similarity to other types is purely coincidental and does not imply any specific intent of the author, or something...

Actually, this is quite annoying, so how can you solve this? My approach is to create a collection that wraps the original collection but exposes a different interface. It's a bit like how ReadOnlyCollection<T> works. In lack of a better idea, I've called this class CovariantCollection<TConcrete, TAbstract>. This collection doesn't copy the original list, but rather wraps it while exposing it differently. To see how it's used, let's look at the updated NotSoSimpleContainer implementation:

 public class NotSoSimpleContainer : MyContainer
 {
     private List<MyDerivedClass> myClasses_;
     private CovariantCollection<MyDerivedClass, MyClass> myClassesWrapper_;
  
     public NotSoSimpleContainer()
     {
         this.myClasses_ = new List<MyDerivedClass>();
         this.myClassesWrapper_ =
             new CovariantCollection<MyDerivedClass, MyClass>(this.myClasses_);
     }
 

 

     public override IList<MyClass> MyClasses
     {
         get { return this.myClassesWrapper_; }
     }
 }

From within NotSoSimpleContainer, you would still manipulate the original list via the myClasses_ member variable, which ensures that only instances of MyDerivedClass are being used. However, the myClassesWrapper_ member variable wraps myClasses_ and exposes it as an IList<MyClass>.

Here comes the complete implementation of CovariantCollection<TConcrete, TAbstract>. It's a bit of a long code listing, but I think it will still be more readable if I present it in one piece. I still have a few comments about the implementation below, so just scroll past the code if you don't care about all the details:

 public class CovariantCollection<TConcrete, TAbstract> : 
     Collection<TConcrete>, IList<TAbstract> where TConcrete : TAbstract
 {
     public CovariantCollection(IList<TConcrete> list)
         : base(list)
     {
     }
  
     #region IList<TAbstract> Members
  
     int IList<TAbstract>.IndexOf(TAbstract item)
     {
         if (item is TConcrete)
         {
             return this.IndexOf((TConcrete)item);
         }
         throw CovariantCollection<TConcrete, TAbstract>.
             CreateWrongConcreteTypeException("item");
     }
  
     void IList<TAbstract>.Insert(int index, TAbstract item)
     {
         if (!(item is TConcrete))
         {
             throw CovariantCollection<TConcrete, TAbstract>.
                 CreateWrongConcreteTypeException("item");
         }
         this.Insert(index, (TConcrete)item);
     }
  
     void IList<TAbstract>.RemoveAt(int index)
     {
         this.RemoveAt(index);
     }
  
     TAbstract IList<TAbstract>.this[int index]
     {
         get
         {
             return this[index];
         }
         set
         {
             if (!(value is TConcrete))
             {
                 throw CovariantCollection<TConcrete, TAbstract>.
                     CreateWrongConcreteTypeException("value");
             }
             this[index] = (TConcrete)value;
         }
     }
  
     #endregion
  
     #region ICollection<TAbstract> Members
  
     void ICollection<TAbstract>.Add(TAbstract item)
     {
         if (!(item is TConcrete))
         {
             throw CovariantCollection<TConcrete, TAbstract>.
                 CreateWrongConcreteTypeException("item");   
         }
         this.Add((TConcrete)item);
     }
  
     void ICollection<TAbstract>.Clear()
     {
         this.Clear();
     }
  
     bool ICollection<TAbstract>.Contains(TAbstract item)
     {
         if (item is TConcrete)
         {
             return this.Contains((TConcrete)item);
         }
         throw CovariantCollection<TConcrete, TAbstract>.
             CreateWrongConcreteTypeException("item");
     }
  
     void ICollection<TAbstract>.CopyTo(TAbstract[] array, int arrayIndex)
     {
         TConcrete[] concreteItems = new TConcrete[array.Length];
         this.CopyTo(concreteItems, arrayIndex);
         concreteItems.CopyTo(array, 0);
     }
  
     int ICollection<TAbstract>.Count
     {
         get { return this.Count; }
     }
  
     bool ICollection<TAbstract>.IsReadOnly
     {
         get { return ((ICollection<TConcrete>)this).IsReadOnly; }
     }
  
     bool ICollection<TAbstract>.Remove(TAbstract item)
     {
         if (item is TConcrete)
         {
             return this.Remove((TConcrete)item);
         }
         throw CovariantCollection<TConcrete, TAbstract>.
             CreateWrongConcreteTypeException("item");
     }
  
     #endregion
  
     #region IEnumerable<TAbstract> Members
  
     IEnumerator<TAbstract> IEnumerable<TAbstract>.GetEnumerator()
     {
         foreach (TAbstract item in this)
         {
             yield return item;
         }
     }
  
     #endregion
  
     private static Exception CreateWrongConcreteTypeException(string argumentName)
     {
         return new ArgumentException(string.Format(
             "This collection only supports instances of {0} (or derived types).",
             typeof(TConcrete).AssemblyQualifiedName), argumentName);
     }
 }

You will probably notice a common pattern here. In most of the members, I need to test if the input is indeed an instance of TConcrete (MyDerivedClass in the example) and throw an exception if this is not so. If the input is okay, I delegate to the implementation provided by Collection<T>. Notice that I don't need to delegate to base, but can delegate directly to this, since the implementation of IList<TAbstract> is an explicit imteface implementation. Collection<T> also provides the basic wrapper functionality, so I don't need to implement this manually.

You may also wonder about another implementation detail: In several cases (e.g. IndexOf), I first test the type of the input using the is keyword, and then subsequently cast to TConcrete. Since the is keyword causes a cast, I'm actually performing the same cast twice, which isn't terribly efficient. Normally, I'd resort to the as keyword and then test for null, but this isn't possible in this case since TConcrete may be a value type (in which case the as keyword is illegal).

It's worth noting that CovariantCollection<TConcrete, TAbstract> implements two different generic IList interfaces: IList<TConcrete> is implemented by Collection<TConcrete>, which also means that IList<TAbstract> has to be implemented explicitly, since some of the IList members (such as the indexer) only differ in return type, and .NET doesn't allow return type covariance.

You may not encounter this collection covariance conundrum every day, but I've personally found myself in this situation enough times that I'm happy that I have this totally reusable class to solve the problem. Now you have it too.

Comments