tags:

views:

81

answers:

2

I discovered that a list of concrete objects cannot be added to a list of interface object.

public static void AddJob(List<IJob> masterJobs, List<Job> jobs)
{
    masterJobs.AddRange(jobs);  //fail to compile
}

Instead, one needs to use the following code:

public static void AddJob(List<IJob> masterJobs, List<Job> jobs)
{
    masterJobs.AddRange(jobs.Cast<IJob>());  
}

What is the rational behind this?

+7  A: 

The reason is that a List<IJob> is not a List<Job>, although a Job implements IJob.

This is co- or contra-variance (I never remember which is which.)

The thinking goes something like this:

The compiler cannot guarantee that AddRange only reads things out of the parameter it is given, and thus it cannot guarantee that this is safe, and thus it fails to compile.

For instance, for all the compiler knows, AddRange could add another object into the jobs parameter, that implements IJob (because AddRange expects IJob collections), but isn't Job, which is what jobs expect, and thus that would not be safe.

In C# 4.0, there is some support for handling this, but I'm not sure it would handle your particular case since the support has to be specified on the interface-level, and not at the method-level.

In other words, you would have to specify on the interface type that everything that relates to T is only going into the collection, never out of it, and then the compiler would allow you do this. However, a collection you cannot read from would be pretty pointless.

Lasse V. Karlsen
"out" is covariance and "in" is contravariance; its now `IList<out T>` and so is covariant. (and I'm wrong on that, see below)
Will
Ooh, nice, don't think I've seen that short a summary of the two before, thanks!
Lasse V. Karlsen
This guy goes into good detail about covariance and contravariance generics support in 4.0: http://www.buunguyen.net/blog/new-features-of-csharp-4.html
Will
@Will: No, `IList<T>` is still invariant because it has values going both in and out. `IEnumerable<T>` is covariant though.
Jon Skeet
@Jon DAMNIT! I coulda sworn IList<T>'s definition was updated as well...
Will
And now I know *why*. Man, that's an awesome feeling when things all click into place.
Will
@Will: Isn't it just? :) Quick now - should `Action<Action<Action<T>>>` be declared as covariant or contravariant? ;)
Jon Skeet
@jon I'd have to say contravariant. What did I win?
Will
+7  A: 

Lasse is right about why this won't work in C# 3 - there is no conversion from List<IJob> to List<Job>.

In C# 4 it will work, not because of the list being covariant, but because IEnumerable<T> is covariant. So in other words, the code would effectively be:

public static void AddJob(List<IJob> masterJobs, List<Job> jobs)
{
    IEnumerable<IJob> jobsTmp = jobs; // This is the covariance working
    masterJobs.AddRange(jobs); // This is now fine
}

jobs implements IEnumerable<Job>, so there's a reference conversion to IEnumerable<IJob> through covariance, so it all works fine. The call to Cast<T> is effectively doing a similar job in your C# 3 workaround - you're using it to convert to an IEnumerable<IJob>.

If you want to more about generic variance, there's a video of my NDC 2010 talk available, or read Eric Lippert's series of blog posts on it.

Jon Skeet
So, let me get this straight... they used `in` and `out` to signify contra/covariance because it essentially describes that the class *only* "takes in" an instance of the generic type (i.e., a method argument) or if the class *only* "passes out" an instance of the class (i.e., return value of a method)? IEnumerable<T> does not "take in" any T's, so it is safe to mark the class as <out T> and therefore the generic type is covariant? IList<T> both takes in T and passes out T, which makes it invariant?
Will
@Will: Yes, that's exactly right. See Eric Lippert's blog post series about variance for a lot more information. I'll edit my post to link to it.
Jon Skeet
@Jon By George, I think I's got it! I think I's got it! (cue musical number)
Will