Freigeben über


More on generic variance

In my entry on generic variance in the CLR, I said that you can’t convert a List<String> to a List<Object>, or even an IEnumerable<String> to IEnumerable<Object>.  I should point out however that the real-world scenarios where you’d want to do this usually involve passing an object of a more specific type to an API that (for abstraction reasons) takes a less specific type.  For example, say you have a class hierarchy like this:

 

    abstract class Shape

    {

        public abstract double ComputeArea();

    }

    class Square : Shape

    {

        public Square(double height)

        {

            m_height = height;

        }

        public override double ComputeArea()

        {

            return m_height * m_height;

        }

        private double m_height;

    }

    class Circle : Shape

    {

        public Circle(double radius)

        {

            m_radius = radius;

        }

        public override double ComputeArea()

        {

            return Math.PI * m_radius * m_radius;

        }

        private double m_radius;

    }

You’d like to be able to use it uniformly like this:

 

    class Program

    {

        static void Main(string[] args)

        {

            List<Square> ls = new List<Square>();

            ls.Add(new Square(2));

            ls.Add(new Square(3));

            double totS = GetTotalArea(ls); // won’t compile

            List<Circle> lc = new List<Circle>();

            lc.Add(new Circle(2));

            lc.Add(new Circle(3));

            double totC = GetTotalArea(lc); // won’t compile

        }

        public static double GetTotalArea( IEnumerable<Shape> l)

        {

            double total = 0;

            foreach (Shape s in l)

            {

                total += s.ComputeArea();

            }

            return total;

        }

    }

 

This would only work if C# supported generic variance and IEnumerable was defined as IEnumerable<+T>.  However, there is another option in this case.  You could make GetTotalArea a generic method and rely on a base-type constraint:

        public static double GetTotalArea<T>(IEnumerable<T> l)

            where T : Shape

        {

            double total = 0;

            foreach (Shape s in l)

            {

                total += s.ComputeArea();

            }

            return total;

        }

 

Now the above Main method will work perfectly, you don’t even have to modify it to specify the type parameters (C#’s type inference can figure them out automatically).  So although you're not really converting an IEnumerable<Square> to an IEnumerable<Shape>, you are able to use it that way.

 

This is certainly great.  And in practice, most .NET developers will find the support in .NET 2.0 for generics to be more than powerful enough.  However, if you’re trying to do some serious “generic programming” [3], or if you’re excited by programming language theory like I am, then this does still leave something to be desired.  Most notably, we can specify an upper-bound using a constraint like this (rather than require our language to support covariant type parameters), but the CLR and C# don’t have support for lower bounds (“supertype constraints”) so you can’t use this technique in place of contravariant type parameters (eg. my IComparer<-T> example).  See section 4.4 (“Comparison with Parametric Methods”) of [1] for more details.

 

Java 5 addresses these scenarios with “Wildcard types” [2].  Eg. you would use a “List<? extends Shape>” in Java to implement my example above, and there is also a syntax “? super T” for lower-bounds.  It’s interesting to note that the authors of the paper on wildcards indicate that the main advantage of wildcard types over using normal generic methods (as I’ve done above) is that wildcard types don’t require exact type information (see section 4.4 of [2]).  The big difference between generics in .NET and Java is that in .NET, the CLR supports generics to the core and so we have exact type information everywhere (which is why you can get information about generic types using Reflection at run-time).  In Java, generics are “erased” by the compiler, and so at run-time, the information about generic instantiations are lost.  Using erasure has the benefit of better compatibility with old code and a much simpler implementation, but extending the VM (like we did for the CLR) has several important benefits including avoiding boxing for instantiations at value types and therefore better performance (eg. a List<int> can be very efficient), and the ability to use exact type information at run-time.

 

The big difference between wildcards and generic variance in the CLR is that wildcard types are an example of “usage-site variance” where MSIL uses “definition-site variance” (meaning it’s the type definition that specifies the variance annotation, not the user of the type).  I was reading about a cool academic programming language called Scala recently, and was pleased to see that after some experience with usage-site variance, they decided to switch to definition-site variance because they found it easier to use correctly (see “Comparison with wildcards” in [4]).  Scala is a very cool language (my programming languages professor recently mentioned it as a great example of a language on the forefront of modern academic language design), and can target .NET.  Unfortunately they haven’t built support for the V2.0 CLR yet so they aren’t actually making use of the definition-site variance support in the CLR (for the moment <grin>).

 

Anyway, I think that’s about all I have to say about generic variance.  I’ve got a programming languages exam on Tuesday (I’m working on my masters in computer science) so I think I better stop procrastinating and study for it <grin>.

 

References

[1] On variance-based subtyping for parametric types

[2] Adding Wildcards to the Java Programming Language

[3] A Comparative Study of Language Support for Generic Programming

[4] An Overview of the Scala Programming Language (2. Edition)

Comments

  • Anonymous
    June 03, 2006
    When people start using C# generics for the first time, they are sometimes surprised that they can’t...

  • Anonymous
    June 06, 2006
    I have found an alternative way to cast to a base type using ConvertAll.

    IF you look in MSDN for ConvertAll they give you an example using Arrays but works fine for List objects.

  • Anonymous
    June 06, 2006
    Santiago,
    Yes you can do that, but it's creating a NEW list, calling the conversion function for EACH element, and storing the resulting element into the new list.  This is not the same thing as what I'm talking about.  First of all, copying the whole list to a new list takes time and space proportional to the size of the list.  Also, if you now go an add or remove an element from the new list, there is no change to the original list.  There are certainly cases where this is what you want, but it's not what I'm talking about here.

    The stuff I'm talking about has no run-time effects - it's purely about convincing the compiler to allow us to convert between types at COMPILE time.  The actual run-time in-memory representation of the objects do not change.

  • Anonymous
    June 19, 2006
    PingBack from http://onlytalkingsense.wordpress.com/2006/06/19/c-generics/

  • Anonymous
    July 04, 2006
    Odd coincidence - I just wrote this up in my own blog, before I found this!

  • Anonymous
    July 08, 2006
    The comment has been removed

  • Anonymous
    July 11, 2006
    Uwe, what exactly is it you want to do?  Can you give me an example code snippit?  

    If you mean use enum types as type parameters to generic types, then I agree that makes sense and from my experiments it seems to work fine:

    enum MyEnums
    {
       One,
       Two,
       Three
    }

    static void Main(string[] args)
    {
       List<MyEnums> l = new List<MyEnums>();
       l.Add(MyEnums.One);
    }

    Rick

  • Anonymous
    July 17, 2006
    "Variance and Generalized Constraints for C# Generics"
    by Burak Emir, Andrew J. Kennedy, Claudio Russo, and Dachuan Yu discusses a possible solution:
    http://research.microsoft.com/research/pubs/view.aspx?type=inproceedings&id=1215

    (Originally found at http://lambda-the-ultimate.org/node/1573)

  • Anonymous
    July 31, 2006
    This post is aresult of an expensive lesson I learned. The situation: A MVP style architecture, Presentation

  • Anonymous
    August 08, 2006
    Why having an abstract class Shape, I guess an IShape interface will do the job ?

  • Anonymous
    August 08, 2006
    Yep, an interface would demonstrate the same thing.  I thought using an abstract base class would be slightly simpler, but either way is fine...

  • Anonymous
    August 08, 2006
    So I just wanted to follow up on my last post where I described in detail (agonizing) how I thought it

  • Anonymous
    July 24, 2007
    Not often I get the jump on it's old master but MbUnit answers Peli's post on unit testing generics

  • Anonymous
    August 08, 2007
    When people start using C# generics for the first time, they are sometimes surprised that they can’t

  • Anonymous
    December 05, 2007
    Also, few blog enteries on generics and variance in generics. Find them to be really useful. First one

  • Anonymous
    December 05, 2007
    PingBack from http://msdnrss.thecoderblogs.com/2007/12/06/generics/

  • Anonymous
    December 05, 2007
    Also, few blog enteries on generics and variance in generics. Find them to be really useful. First one

  • Anonymous
    September 01, 2008
    PingBack from http://bitsofmind.wordpress.com/2008/09/02/generic_variance_in_csharp_and_java/

  • Anonymous
    November 01, 2008
    Although horribly outdated, but JIT didn't inline functions containing value type parameters or return value. JIT also maintained separate implementations for each distinct value type instantiation of generics. This shortcoming has been fixed as of .netfx 3.5 SP1.