Export (0) Print
Expand All
20 out of 24 rated this helpful - Rate this topic

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 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 would like to be able to write code like this:

List<int> ints = new List<int>();
ints.Add(1);
ints.Add(10);
ints.Add(42);
List<object> objects = new List<object>();

// doesnt compile ints is not a IEnumerable<object>
//objects.AddRange(ints); 

In this case, you would like to treat a List<int> which is also an IEnumerable<int>, as an IEnumerable<object>. This seems like a reasonable thing to do, as int is convertible to object. It is very similar to being able to treat a string[] as an object[] as you can do today. If you find yourself in this situation, the feature you are looking for is called generics variance, 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>.

C# does not support variance for generic types, so when encountering cases like this you will need to try one of a couple of techniques you can use to work around the problem. For the simplest cases, like the case of a single method like 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:

// 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);
    }
}

Which allows you to do this:

// does compile
VarianceWorkaround.Add<int, object>(ints, objects);

This 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 allows the compiler to enforce that int is convertible to an object. Constraining a type parameter to derive from another type parameter is called a naked type parameter constraint.

Defining a single method to work around variance problems is not too bad. Unfortunately variance issues can become quite complex quite 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 only takes an IEnumerable<object>. Again, this makes some sense because you can think of an IEnumerable<object> as a sequence of objects, and an IEnumerable<int> is a sequence of ints. Since ints are objects, a sequence of ints should be treatable as a sequence of objects. For example:

static void PrintObjects(IEnumerable<object> objects)
{
    foreach (object o in objects)
    {
        Console.WriteLine(o);
    }
}

Which you would like to call like this:

// would like to do this, but cant ...
// ... ints is not an IEnumerable<object>
//PrintObjects(ints);

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 this:

// 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
{

Which allows you to do this:

PrintObjects(VarianceWorkaround.Convert<int, object>(ints));

Again, notice the naked type parameter constraint on the wrapper class and helper method. This machinery is getting pretty complicated, but the code in the wrapper class is pretty straightforward; it just delegates to the members of the wrapped interface doing nothing more than straightforward type conversions along the way. Why not have the compiler allow the conversion from IEnumerable<int> to IEnumerable<object> directly?

Although variance is type-safe in the case where you are looking at read-only views of your collections, variance is not type-safe in the case where both read and write operations are involved. For example, the IList<> interface could not be dealt with in this automatic way. You can still write a helper that will wrap all read operations on an IList<> in a type-safe manner, but the wrapping of write operations cannot be done so simply.

Here is part of a wrapper for dealing with variance on the IList<T> interface that shows the problems that arise with variance in both the read and write directions:

private class ListWrapper<S, D> : CollectionWrapper<S, D>, IList<D>
    where S : D
{
    public ListWrapper(IList<S> source) : base(source)
    {
        this.source = source;
    }
    
    public int IndexOf(D item)
    {
        if (item is S)
        {
            return this.source.IndexOf((S) item);
        }
        else
        {
            return -1;
        }
    }

    // variance the wrong way ...
    // ... can throw exceptions at runtime
    public void Insert(int index, D item)
    {
        if (item is S)
        {
            this.source.Insert(index, (S)item);
        }
        else
        {
            throw new Exception("Invalid type exception");
        }
    }

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>. Since D is a base type of S, not all Ds are Ss, so 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:

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(); 

In the IList<> example, the wrapper for the Insert method can simply 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 right 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 simply 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>>>.

Generating these wrappers to work around variance problems in your code 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 can lead to subtle bugs. When using generics, you should choose 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, including generics variance as a first class part of the type system would dramatically 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 right 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 = source;
        }

        public IEnumerator<D> GetEnumerator()
        {
            return new EnumeratorWrapper(this.source.GetEnumerator());
        }

        IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        private class EnumeratorWrapper : IEnumerator<D>
        {
            public EnumeratorWrapper(IEnumerator<S> source)
            {
                this.source = source;
            }

            private IEnumerator<S> source;

            public D Current
            {
                get { return this.source.Current; }
            }

            public void Dispose()
            {
                this.source.Dispose();
            }

            object IEnumerator.Current
            {
                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);
    }

    private class ListWrapper<S, D> : CollectionWrapper<S, D>, IList<D>
        where S : D
    {
        public ListWrapper(IList<S> source) : base(source)
        {
            this.source = source;
        }
        
        public int IndexOf(D item)
        {
            if (item is S)
            {
                return this.source.IndexOf((S) item);
            }
            else
            {
                return -1;
            }
        }

        // variance the wrong way ...
        // ... can throw exceptions at runtime
        public void Insert(int index, D item)
        {
            if (item is S)
            {
                this.source.Insert(index, (S)item);
            }
            else
            {
                throw new Exception("Invalid type exception");
            }
        }
    
        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>();

            // doesnt compile ints is not a IEnumerable<object>
            //objects.AddRange(ints); 
            
            // does compile
            VarianceWorkaround.Add<int, object>(ints, objects);

            // would like to do this, but cant ...
            // ... ints is not an 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(); 
        }
    }
}

Community Additions

ADD
Show:
© 2014 Microsoft. All rights reserved.