Variance in Generic Types (C# Programming Guide)
One of the main benefits of the addition of generics to C# is the ability to easily create strongly typed collections by using types in the System.Collections.Generic namespace. For example, you can create a variable of type List<int>, and the compiler will check all accesses to the variable, ensuring that only ints are added to the collection. This is a big usability improvement over the untyped collections available in version 1.0 of C#.
Unfortunately, strongly typed collections have drawbacks of their own. For example, suppose you have a strongly typed List<object> and you want to append all the elements from a List<int> to your List<object>. You might want to be able to write code as in the following example:
In this case, you would, which treats an instantiation of a generic type, in this case IEnumerable<int>, as a different instantiation of that same type, in this case IEnumerable<object>.
Because C# does not support variance for generic types, when you encounter cases such as this you will have to try one of several techniques you can use to work around this problem. For the simplest cases, such as the case of a single method such as AddRange in the previous example, you can declare a simple helper method to do the conversion for you. For example, you could write this method:
Which enables you to do this:
The previous example shows some characteristics of a simple variance workaround. The helper method takes two type parameters, for the source and destination, and the source type parameter S has a constraint which is the destination type parameter D. This means that the List<> being read from must contain elements which are convertible to the element type of the List<> being inserted into. This enables the compiler to enforce that int is convertible to an object.
Defining a single method to work around variance problems is not too bad. Unfortunately variance issues can become very complex quickly. The next level of complexity is when you want to treat an interface of one instantiation as an interface of another instantiation. For example, you have an IEnumerable<int>, and you want to pass it to a method that:
Which you would want to call as in the following example:
The workaround for the interface case, is to create a wrapper object that does the conversions for each member of the interface. This would look something like the following code segment, taken from the example at the end of the topic.
Which enables you to write the following code.
Again, notice the use of a type parameter as a constraint on the wrapper class and helper method. The machinery is becoming more complex, but the code in the wrapper class is fairly that will wrap all read operations on an IList<> in a type-safe manner, but the wrapping of write operations cannot be done so easily.
Here is part of a wrapper for dealing with variance on the IList<T> interface that shows the problems that occur with variance in both the read and write directions:"); } }
The Insert method on the wrapper has a problem. It takes as an argument a D, but it must insert it into an IList<S>. Because D is a base type of S, not all Ds are Ss, and the Insert operation may fail. This example has an analogue with variance with arrays. When inserting an object into an object[], a dynamic type check is performed because the object[] may in fact be a string[] at run time. For example:
In the IList<> example, the wrapper for the Insert method can just throw when the actual type does not match the desired type at run time. So again, you could imagine that the compiler would automatically generate the wrapper for the programmer. There are cases where this policy is not the correct thing to do, however. The IndexOf method searches the collection for the item provided, and returns the index in the collection if the item is found. However, if the item is not found, the IndexOf method just returns -1, it does not throw. This kind of wrapping cannot be provided by an automatically generated wrapper.
So far, the two simplest work arounds for generic variance issues have been described. However, variance issues can get arbitrarily complex. For example, when you treat a List<IEnumerable<int>> as a List<IEnumerable<object>>, or treating a List<IEnumerable<IEnumerable<int>>> as a List<IEnumerable<IEnumerable<object>>>.
When you generate these wrappers to work around variance problems in your code, you can introduce a significant overhead in your code. Also, it can introduce referential identity issues, as each wrapper does not have the same identity as the original collection which could lead to subtle bugs. When using generics, you should select your type instantiation to reduce mismatches between components that are tightly coupled. This may require some compromises in the design of your code. As always, design involves tradeoffs between conflicting requirements, and the constraints of the types system in the language should be considered in your design process.
There are type systems that include generic variance as a first class part of the language. Eiffel is the prime example of this. However, the inclusion of generics variance as a first class part of the type system would significantly increase the complexity of the type system of C#, even in relatively straightforward scenarios that do not involve variance. As a result, the C# designers felt that not including variance was the appropriate choice for C#.
Here is the full source code for the previous examples.
using System; using System.Collections.Generic; using System.Text; using System.Collections; static class VarianceWorkaround { // Simple workaround for single method // Variance in one direction only public static void Add<S, D>(List<S> source, List<D> destination) where S : D { foreach (S sourceElement in source) { destination.Add(sourceElement); } } // Workaround for interface // Variance in one direction only so type expressinos are natural public static IEnumerable<D> Convert<S, D>(IEnumerable<S> source) where S : D { return new EnumerableWrapper<S, D>(source); } private class EnumerableWrapper<S, D> : IEnumerable<D> where S : D { public EnumerableWrapper(IEnumerable<S> source) { this.source = { get { return this.source.Current; } } public bool MoveNext() { return this.source.MoveNext(); } public void Reset() { this.source.Reset(); } } private IEnumerable<S> source; } // Workaround for interface // Variance in both directions, causes issues // similar to existing array variance public static ICollection<D> Convert<S, D>(ICollection<S> source) where S : D { return new CollectionWrapper<S, D>(source); } private class CollectionWrapper<S, D> : EnumerableWrapper<S, D>, ICollection<D> where S : D { public CollectionWrapper(ICollection<S> source) : base(source) { } // variance going the wrong way ... // ... can yield exceptions at runtime public void Add(D item) { if (item is S) { this.source.Add((S)item); } else { throw new Exception(@"Type mismatch exception, due to type hole introduced by variance."); } } public void Clear() { this.source.Clear(); } // variance going the wrong way ... // ... but the semantics of the method yields reasonable semantics public bool Contains(D item) { if (item is S) { return this.source.Contains((S)item); } else { return false; } } // variance going the right way ... public void CopyTo(D[] array, int arrayIndex) { foreach (S src in this.source) { array[arrayIndex++] = src; } } public int Count { get { return this.source.Count; } } public bool IsReadOnly { get { return this.source.IsReadOnly; } } // variance going the wrong way ... // ... but the semantics of the method yields reasonable semantics public bool Remove(D item) { if (item is S) { return this.source.Remove((S)item); } else { return false; } } private ICollection<S> source; } // Workaround for interface // Variance in both directions, causes issues similar to existing array variance public static IList<D> Convert<S, D>(IList<S> source) where S : D { return new ListWrapper<S, D>(source); }"); } } public void RemoveAt(int index) { this.source.RemoveAt(index); } public D this[int index] { get { return this.source[index]; } set { if (value is S) this.source[index] = (S)value; else throw new Exception("Invalid type exception."); } } private IList<S> source; } } namespace GenericVariance { class Program { static void PrintObjects(IEnumerable<object> objects) { foreach (object o in objects) { Console.WriteLine(o); } } static void AddToObjects(IList<object> objects) { // this will fail if the collection provided is a wrapped collection objects.Add(new object()); } static void Main(string[] args) { List<int> ints = new List<int>(); ints.Add(1); ints.Add(10); ints.Add(42); List<object> objects = new List<object>(); // The following statement does not compile // becuase �ints� is not IEnumerable<object>. //objects.AddRange(ints); // does compile VarianceWorkaround.Add<int, object>(ints, objects); // The following statement does not compile // becuase �ints� is not IEnumerable<object>. //PrintObjects(ints); PrintObjects(VarianceWorkaround.Convert<int, object>(ints)); AddToObjects(objects); // this works fine AddToObjects(VarianceWorkaround.Convert<int, object>(ints)); } static void ArrayExample() { object[] objects = new string[10]; // no problem, adding a string to a string[] objects[0] = "hello"; // runtime exception, adding an object to a string[] objects[1] = new object(); } } }