다음을 통해 공유


LINQ Farm: Covariance and Contravariance in C# 4.0

This post covers the upcoming C# 4.0 support for covariance and contravariance when working with delegates and interfaces. Eric Lippert’s series of posts on this subject are definitely the definitive reference at this time. I’m writing this overview of the subject simply as an appendix to his explanation, and as quick reference for folks who want to get up to speed on this technology. Please remember that this post covers pre-beta technology as defined in the October 2008 CTP of Visual Studio 2010.

The support for covariance and contravariance in the next version of the C# language will ensure that delegates and interfaces will behave as expected when you are working with generic types. In Visual Studio 2008, there are rare occasions when developers might expect a delegate or interface to behave one way, only to find that it does not conform to their expectations. In Visual Studio 2010 delegates will behave as expected. Other C# types support have always automatically supported covariance and contravariance. C# 4.0 will simply ensure that generic delegates and interfaces follow suit.

Consider this simple class hierarchy:

class Animal{ }
class Cat: Animal{ }

Here we have a class called Animal, and a simple descendant of it called Cat. Suppose you declare a delegate declaration that defines a method that returns an arbitrary type:

 delegate T Func1<T>();

 An instance of this delegate could be defined to return a Cat:
 Func1<Cat> cat = () => new Cat();

The delegate declaration shown here defines a delegate that returns a type T. The second line of code initializes T to be of type Cat and assigns this delegate to a simple lambda that creates an instance of a cat and returns it. For those of you not yet comfortable with lambda syntax, I’ll add that the lambda shown here is simply a shorthand way of writing a method that looks like this:

 public Cat MyFunc()
{
    return new Cat();
}

So one could also have written the cat delegate like this:

Func1<Cat> cat = MyFunc;

Given either of these declarations, our intuition tells us that we could assign a cat to a delegate that returns an Animal:

 Func1<Animal> animal = cat;

This code looks like it should succeed because a Cat is type of Animal, and one should always be able to assign a smaller type of object, such as a Cat, to a larger type of object, such as a Animal.  We think of the type Animal as being large, since a creature such as a Cat, Dog, Whale or Bird would be a type of Animal, and hence compatible with it. In other words, a big type like an Animal is assignment compatible with lots of smaller types, such as a Cat, Dog or Whale. We think of the type Cat as being smaller than an Animal, since only other cats can be assigned to it. It is not assignment compatible with as wide a range of types as an Animal, and hence we think of it as being smaller.

In fact, the assignment shown above does not work in Visual Studio 2008. The new feature being added in Visual Studio 2010 ensures that this assignment will work if you make one minor change to the declaration for your delegate. In particular, you need to use the keyword out in your type parameter:

 delegate T Func1<out T>();

Now the assignment will succeed, and the code will run as expected, as shown in Listing 1. This type of assignment is called covariance.

Listing 1: Examples of covariance and contravariance.

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SimpleVariance
{
    class Animal { }
    class Cat: Animal { }

    class Program
    {
        // To understand what the new CoVariance and ContraVariance code does for you
        // Try deleting or adding the words out and in from the following 2 lines of code:
        delegate T Func1<out T>();
        delegate void Action1<in T>(T a);

        static void Main(string[] args)
        {
           // Covariance
            Func1<Cat> cat = () => new Cat();
            Func1<Animal> animal = cat;

            // Contravariance
            Action1<Animal> act1 = (ani) => { Console.WriteLine(ani); };
            Action1<Cat> cat1 = act1;
        }        
    }
}

Looking at Listing 1, you will notices that there is a second example included in it. This second example illustrates contravariance. Most developers, including myself, find contravariance more difficult to understand than covariance. It is, however, a similar concept.

The second example starts out by including a delegate declaration. This time the declaration defines a method that takes a parameter, rather than returning a value:

delegate void Action1<in T>(T a);

Notice also that we use the keyword in, rather than the keyword out. That is because we are passing a parameter in, rather than returning a result “back out” of a function.

Next we define a delegate that takes an animal as a parameter:

Action1<Animal> act1 = (ani) => { Console.WriteLine(ani); };

In the contravariant example shown in Listing 1, the assignment of act1 to the delegate cat1 would fail if we did not include the keyword in when declaring the Action1 delegate type. Here is the assignment:

Action1<Cat> cat1 = act1;

This would fail in C# 3.0 no matter what you did. It will work in C# 4.0 so long as you use the keyword in when declaring the delegate type Action1. If you omitted the keyword, then it would fail. For instance, the following declaration of Action1 would cause the assigned to fail because the keyword in is omitted:

delegate void Action1<T>(T a);

Contrast this declaration with the one in Listing 1.

In the contravariant example we use a lambda to define a method that is equivalent to a standard method that looks like this:

public static void Contra(Animal ani)

{

      Console.WriteLine(ani);

}

I’m using lambdas in Listing 1 simply to keep the code short, not because lambdas are connected to covariance and contravariance. This code would still illustrate covariance and contravariance whether or not I used lambdas. What is important here is not the presence of lambdas, but the presence of a generic delegate.

Finally, lets take one moment to make sure you understand why the assignment of act1 to cat1 works. The peculiar thing about contravariance is that it appears that we are assigning a larger type to a smaller type. In other words, it appears that we are trying to fit a big thing like an Animal, into a small type like a Cat. This is not what we are doing. The point here is that act1 will work with any animal, and a Cat is animal, therefore you can safely make the assignment. In other words, cat1 can only be passed Cats, and all cats are animals, therefore anything you can assign to cat1 could also be safely passed to act1. This is why the assignment should work, and does in fact work if you use the keyword in.

As I say, contravariance is a bit confusing. If you find the subject a bit much, then you are joining a happy throng of other developers who struggle with the subject. My suggestion is just to relax, think about the example I’ve shown here for a bit, and you will probably have an “ah-ha” moment either in the next few moments, or sometime in the hopefully very lengthy remainder of your life.

Almost everyone who has written or talked about this subject, from Eric Lippert to Anders himself, points out that this technology is not so much an innovation as a means of bringing C# in line with developers expectations. Adding support for covariance and contravariance to generic interfaces and delegates simply ensures that the language behaves as many would intuitively expect it to behave. All you have to do is remember to add the the keywords in or out when you are making an assignment that involves delegates or interfaces, and the language isn’t behaving as you would expect it to behave.

I apologize for focusing only on delegates in this post. Hopefully I’ll find the time to come back and illustrate the same subject using interfaces. I should add that all the code shown here is written against a very shaky pre-beta version of the C# 4.0, and it is always possible, though that not necessarily likely, that this technology will change before it ships.

kick it on DotNetKicks.com

Comments

  • Anonymous
    October 28, 2008
    You've been kicked (a good thing) - Trackback from DotNetKicks.com

  • Anonymous
    October 29, 2008
    Couple of questions for you: Can a type param be both in and out so would the following be allowed: public interface ICopier<in out T> { T Copy(t item); } Also can you explain why the need for the extra keywords? I can't imagine why but I am sure there is a reason. thanks Josh

  • Anonymous
    October 29, 2008
    I'm with Josh, please explain us why we need those in-out keywords. I can't figure out why is needed to do something that is what you expect, specially when talking about covariance

  • Anonymous
    October 29, 2008
    I think I understand the background.  This is a feature created to support F# functional programming types.  Immutable functions without side effects is a HUGE thing in functional programming.  The contravariant delegates are a way of representing these functions in C#. Basically, delegates with "in" parameters are delegates that can produce no side effects. "out" delegates are the more common garden variety delegates we all know and love.

  • Anonymous
    October 29, 2008
    Daedius, in this example the "out" keyword is not a "common delegate". Indeed, it is making something that actually cannot be done "covariance". delegate T Func1<out T>();        delegate void Action1<in T>(T a);        static void Main(string[] args)        {           // Covariance            Func1<Cat> cat = () => new Cat();            Func1<Animal> animal = cat;            // Contravariance            Action1<Animal> act1 = (ani) => { Console.WriteLine(ani); };            Action1<Cat> cat1 = act1;        }         What I´m really want to know, is why the compiler cannot do this automatically, simply by allowing you do covariance and contravariance without any keyword at all. I think if it was possible actually in some parts of the language why not in generics and delegates? As Charlie has said "In Visual Studio 2010 delegates will behave as expected", so if it's something we expect, why we are forced to use a special keyword to get expected behavior?

  • Anonymous
    October 29, 2008
    It is a limitation in the name of type safety.  You are declaring that the interface or delegate can accept base or derived classes of the generic parameter by explicitly defining that said interface or delegate will either only allow T as input, or only allow T as output.  The interface or delegate cannot be both. If you are using covariance it means that the interface or delegate can output a value that is a class of the specified type parameter or derived from that class.  If you pass a Func<Cat> to a method that expects a Func<Animal> it works fine since it will return a Cat, which is derived from Animal.  That method can then treat the Cat as if it were an Animal without any ill effects. If you are using contravariance it means that the interface or delegate will receive input of a value that is a class of the specified type parameter or derived from that class.  If you pass an Action<Animal> to a method that expects an Action<Cat> it works fine as it will pass a Cat which is derived from Animal.  The body of the called delegate can treat the Cat as if it were an Animal without any ill effects. These roles are pretty specific and cannot be reversed.  A method that takes a Func<Cat> cannot accept a Func<Animal> because the Animal that it returns might not be a valid Cat and it would fail.  Similarly a method that takes an Action<Cat> cannot accept an Action<Animal> for the same reason.  The new keywords really only apply to the implementers of fairly fundamental structures, such as IEnumerable<out T> or Action<in T>.  Their benefit comes offering the flexibility in the consumption of types that consume those structures.  You might never use those keywords, but because of them you can now do this: // Illegal in C# 3.0 IEnumerable<Animal> animals = new List<Cat>();

  • Anonymous
    October 29, 2008
    "IEnumerable<Animal> animals = new List<Cat>();" This is HUGE. I can't believe how many special interfaces I have to use to get true "programming to an interface" right now just because of this limitation. For example, right now I have to do this: Domain object, with CRUD or Mapper ops (much easier to code to a concrete or abstract class collection than to an interface that has no CRUD methods): public IList<RuleState> ChildStates {    get { return m_ChildStates; } } then in the interface for my runtime configurator (on the same class), I have to do this: IList<IRuntimeRuleState> IRuntimeConfig.GetRuleStates() {   List<IRuntimeRuleState> list = new List<IRuntimeRuleState>();    foreach(IRuntimeRuleState rs in m_ChildStates)        list.Add(rs);    return list; } In c# 4, I could just do: IList<IRuntimeRuleState> IRuntimeConfig.GetRuleStates() {   return m_ChildStates; } Much easier to code, not to mention far more efficient. Can't wait.

  • Anonymous
    October 29, 2008
    Your comment about this "acting as the programmer expects" is exactly right. I was very excited about generics, but my excitement was tempered when I found out I couldn't do: ... private List<SomeObject> m_Field; public IList<ISomeObject> Prop {    get { return m_Field; } } ... As I posted previously, this covariance fix will alleviate this problem, and make us all better programmers by giving us the tools to be better programmers.

  • Anonymous
    October 29, 2008
    The comment has been removed

  • Anonymous
    October 29, 2008
    I'm with int19h on this one. Since you cannot have both in and out applied to the parameter, it seems like you won't be able to use covariance/contravariance with the most interesting collections... btw, should I assume that we won't be getting Design By Contract on C# 4.0? Why isn't this part of tha language?It's one of those things that should have been there for a long time!

  • Anonymous
    October 29, 2008
    One of the things that we’ll have in C# 4.0 is covariance/contravariace working on generics…I mean, sort

  • Anonymous
    October 29, 2008
    The comment has been removed

  • Anonymous
    October 29, 2008
    One of the things that we’ll have in C# 4.0 is covariance/contravariace working on generics…I mean, sort

  • Anonymous
    October 30, 2008
    Thanks Halo_Four, you have cleared a lot of things. So the problem here is with classes that output and input the same generic parameter, so that is why it cannot be done without the "in" and "out" keywords. Also note that as Bruce Pierson and int19h commented, unfortunately this will not cover all the expected cases, like those when the parameter is used both in and out. OOP Sceptic I´m in a real world project right know and those discussions allow me to understand better the tool that I´m using every day. And my better understanding of the tool means better products for the client, and better support.

  • Anonymous
    October 30, 2008
    Thanks, int19h, for the clarification, even though it's depressing... @OOP Sceptic I currently have many customers using my software to actually run their manufacturing businesses (boats, trailers, clothing, and more), and it suffers from being difficult to modify to suit their needs because of the lack of flexible design patterns, and old-style procedural programming. I'm currently re-writing it using expert design pattern guidance, and I cannot believe the difference it makes to my sanity when I take the time to understand and apply these principles.

  • Anonymous
    October 30, 2008
    The comment has been removed

  • Anonymous
    October 30, 2008
    The comment has been removed

  • Anonymous
    October 30, 2008
    Sorry - I obviously meant to say that the indexer get would be covariant, while the indexer set would be contravariant.

  • Anonymous
    October 30, 2008
    Nice to finally see this in the language, but the choice of keywords is just horrible: 1.) we already have the out keyword for parameters, but with completly different semantics. And generic type "parameters" are similar enough to function parameters to confuse many people, especially if they are new to the language. 2.) without looking I cannot tell which one of the keywords is used for contravariance and which is used for covariance. "In" and "out" is not vocabulary I'd normally use when talking about type hierachy relations. 3.) there are far better alternatives. Why not simply use "super" and "sub" instead. It is far easier to remember that "sub T" means you can use subtypes of T and "super T" means you can use supertypes of T.

  • Anonymous
    October 30, 2008
    Actually, "in" and "out" are fairly obvious because they clearly outline the restrictions on the usage of a type parameter. An "in" type parameter can only be used for values that are inputs of methods - that is, non-out/ref arguments. An "out" type parameter can only be used for values that are outputs of methods - return values, and out-arguments > Would it not be possible to define a new set of collection interfaces without this problem? It is possible if you basically split each collection interface into three parts: covariant, contravariant, and both. E.g. for a list: interface IListVariant {  int Count { get; }  bool IsReadOnly { get; }  void Clear();  void RemoveAt(int); } interface IListCovariant<out T> {  T Item[int] { get; set; }  IEnumerator<T> GetEnumerator(); } interface IListContravariant<in T> {  void Add(T);  bool Contains(T);  void CopyTo(T[], int);  int IndexOf(T);  void Insert(int, T);  bool Remove(T); } interface IList<T> : IListVariant<T>, IListCovariant<T>, IListContravariant<T> { } And so on for all other collection types (and all other interfaces that could also be so split). So far I haven't seen any indication that this is going to happen in .NET 4.0 (at least it's not mentioned on the "what's new in 4.0" poster), and, looking at the code above, I think it is understandable :) > Or alternatively, add a mechanism that allows specifying the variance at the method level instead of at type level, so that when a generic type parameter is used for both in and out the specific use (for a method) can be explicitly specified. I will repeat what I said elsewhere, and just say that the proper way to enable full variance is to do what Java guys did, and use variance markers at use-site, not at declaration-site, same as Java does it. For example, here are two methods that use IList<T> differently: // Method can take IList<T> where T is string or any base class of string void AddString(IList<in string> list, string s) {  // cannot use any methods of list that return values of type T here, only those who take arguments of type T  list.Add(s);  //list[0]; // illegal here! } // Method can take IList<T> where T is object or any class derived from object object GetFirst(IList<out object> list, int i) {  // cannot use any methods of list that take arguments of type T here, only those that return values of type T  return list[i];  //list.Add(s); // illegal here! }

  • Anonymous
    October 30, 2008
    @ iCe and Bruce Pierson Thanks for responding. I too have many hundreds of people running their finance businesses on my software. I appreciate that advances in any particular technology are going to improve that technology, BUT it seems to me that all these somewhat esoteric terminologies are just solutions waiting for problems. Maybe this new stuff will help you, but it's a little late in the day for magic solutions - surely these wacko extension merely highlight the flaws in OOP any way!

  • Anonymous
    October 30, 2008
    @ int19h: CopyTo can't be contravariant, as I tried to explain above. Passing an array is equivalent to passing a ref parameter. Although the method shouldn't read from the array, there's nothing in the declaration to prevent it. Although it would be nice if "out" parameters could be used in contravariant interfaces, I don't think the CLR would support it. As I understand it, the only difference between "out" and "ref" parameters is the C# compiler's definite assignment checks.

  • Anonymous
    November 03, 2008
    >As I say, contravariance is a bit confusing. maybe the names are weird but usage is just polymorphism (some interface is expected). I can already see this is going get missused.  instead of using a factory pattern for Covariance and aggregation to implement Contravariance. (pass you concrete object implementing an interface into another object that will make it work) i wish there was some real world usage in these examples..

  • Anonymous
    November 03, 2008
    Welcome to the 47th Community Convergence. We had a very successful trip to PDC this year. In this post

  • Anonymous
    November 04, 2008
    > instead of using a factory pattern for Covariance and aggregation to implement Contravariance. (pass you concrete object implementing an interface into another object that will make it work) Since covariance and contravariance in C# 4.0 will work only on interfaces (and delegates, which are semantically really just one-method interfaces), I don't see your point. In fact, I don't understand it at all. How would aggregation help deal with the present problem that IEnumerable<Derived> cannot be treated as (i.e. cast to) IEnumerable<Base>, even though it is clearly typesafe and meaningful to do so?

  • Anonymous
    November 05, 2008
    The comment has been removed

  • Anonymous
    November 08, 2008
    C# 4.0 Dynamic Lookup I really like the way the C# team tackled bring dynamic programming to the language

  • Anonymous
    November 10, 2008
    > IEnumerable<Derived> cannot be treated as (i.e. cast to) IEnumerable<Base>, even though it is clearly typesafe and meaningful to do so? just so all can see what you mean, here is the test:    class Test    {        public Test()        {            List<Base> items = new List<Base>(this.GetItems());        }        public IEnumerable<Derived> GetItems()        {            yield return new Derived();        }    }    public class Base    {    }    public class Derived : Base    {    } it fails to compile. I agree semantically it 'could' compile. On purpose during the Symantic Analysis stage the compiler won't let it compile. Why? You should be returning:        public IEnumerable<Base> GetItems()        {            yield return new Derived();        } so you never couple higher layers with concrete types. not IEnumerable<Derived> which is not meaningful since the Test class only needs the Base interface. again i wish there was some real world examples on why this is actually needed.

  • Anonymous
    November 11, 2008
    The comment has been removed

  • Anonymous
    November 12, 2008
    There are a few new features coming out in C# 4.0. I gathered some posts that will help you to &quot;get

  • Anonymous
    December 18, 2008
    The comment has been removed

  • Anonymous
    December 18, 2008
    The comment has been removed

  • Anonymous
    January 25, 2009
    전에 쓴 post에 있는 새로운 IDE 기능은 dynamic과 COM interop에 관련되어 새로 추가된 기능들이고, 당연히 이 밖에도 여러가지 새로 VS10에 추가 되는 IDE

  • Anonymous
    April 17, 2009
    Wow, that's intense stuff. Thanks for the summary. Sure, I'll learn about lambduh's, dynamic and functional C# programming features, but seriously, I haven't had OOP problems in C# where I even need to know how to spell variance, covariance and contravariance. Oh, and I have personally released hundred's of thousands of lines of pure C# in production right now for my clients, just one app right now is 557,000 loc that. I bet I could reduce the loc, but at what price, maintainability and readability? No thanks. Small focused classes lead naturally to composition, which is far superior to inheritance for most pattern implementations to get green tests and keep them green.

  • Anonymous
    April 27, 2009
    The comment has been removed

  • Anonymous
    February 17, 2010
    I don't know how some 'genius' came up with 'out' and 'in' when you are talking about inheritance. Maybe it was about the limited list of restricted key words in c#, but something like base+derived or sub+super would have made much more sense. It's a shame a not trivial concept was made more difficult by a poor choice of key words.

  • Anonymous
    July 02, 2010
    Hi, Thank you for the post, I really learned a lot. Will you be kind to publish a post about the changes in Reflection due to the net covariance changes? Specifically, how IsAssignableFrom now behave for open and closed generic types. For example: Type closedAnimalListType = typeof(List<Animal>); Type closedCatListType = typeof(List<Cat>); Type openListType = typeof(List<>); Type openEnumType = typeof(IEnumerable<>); type closedAnimalEnumType = typeof(IEnumerable<Animal>); Console.WriteLine(closedAnimalListType.IsAssignableFrom(closedCatListType); Console.WriteLine(closedCatListType.IsAssignableFrom(closedAnimalListType); Console.WriteLine(openEnumType.IsAssignableFrom(closedCatListType); ...... All the other combinations - and let us know what easy one will return. Thank you, Ido