Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

List Generics - implicit casting

I'm targeting .NET 3.5. Let's say I have a class, Bob, which is an abstract base class for SubBob.

I can declare this:

Bob b = new SubBob();

But I can't do this:

 // compliation error - can't convert
BindingList<Bob> myList = new BindingList<SubBob>(); 

My guess is that BindingList doesn't want you to do this because it has to know that the type that's on the right hand side has the same memory layout as the left hand side. SubBob may have a larger size than a Bob.

Is there a way I can do the implicit conversion, or is a cast required?

like image 921
Stealth Rabbi Avatar asked Aug 01 '11 12:08

Stealth Rabbi


5 Answers

The Short Answer

By instantiating BindingList<SubBob> you confine it to work with SubBob and more specific types (e.g. SubSubBob).

If you want Bob to fit there as well, declare myList as a list of the least specific type you want to support:

BindingList<Bob> myList = new BindingList<Bob>(); 

(or, more conveniently,)

var myList = new BindingList<Bob>(); 

Explanation

It is not about memory (BindingList would only hold a reference to the object, and all references are of the same size), rather it is about logical inconsistency you would introduce.

If such code was possible, you would be able to arbitrarily break type restrictions:

BindingList<Animal> myList = new BindingList<Cat>(); 
myList.Add(new Dog()); // bang!

myList is a list of Cats, how would you expect it to handle a Dog?

Compiler wouldn't know there is a problem and would happily compile your code. What should happen when this code runs? An exception? But generics were introduced exactly to solve type safety problem.

A Side-Note on Co- and Contravariance

It's correct that in .NET 4.0, generic covariance and contravariance were added for delegates and interfaces (not for classes). For example, IEnumerable<out T> is covariant that means you can assign it to variable of any type less derived than T:

IEnumerable<Cat> cats = new List<Cat> { new Cat("Hudson"), new Cat("Crookshanks") };
IEnumerable<Animal> animals = cats; // sequence of cats is sequence of animals

But this is only possible because IEnumerable<out T> guarantees it only returns T (keyword out) and never accepts it. If it accepted T as parameter, it would open the door to the problem described above. For this reason, ICollection is not covariant.

In a similar fashion, some interfaces guarantee they only accept T (keyword in) and never return it. Such interfaces are called contravariant and allow assignment to a variable with more specific T:

IComparer<Animal> animalComparer = // ...
IComparer<Dog> dogComparer = animalComparer; // comparer of animals is comparer of dogs
like image 119
Dan Abramov Avatar answered Nov 13 '22 10:11

Dan Abramov


Regarding your case specifically:
You are dealing with contravariance here and a class can only support covariance OR contravariance. IEnumerable<T> supports covariance.

Its signature is IEnumerable<out T>, so it supports inherited classes of the class in T.

In general:
Unfortunately this is not available in .NET 3.5.

It was introduced in .NET 4 as Covariance and Contravariance.

There is a FAQ on the subject at MSDN. Here's an excerpt:

// Assignment compatibility. 
string str = "test";
// An object of a more derived type is assigned to an object of a less derived type. 
object obj = str;

// Covariance. 
IEnumerable<string> strings = new List<string>();

// Contravariance.           
// Assume that I have this method: 
// static void SetObject(object o) { } 
Action<object> actObject = SetObject;
like image 35
Seb Nilsson Avatar answered Nov 13 '22 10:11

Seb Nilsson


Is there a way I can do the implicit conversion, or is a cast required?

The following should let any class that inherits Bob to be added to the list as a Bob.

BindingList<Bob> myList = new BindingList<Bob>();  

What exactly are you trying to do?

like image 26
Security Hound Avatar answered Nov 13 '22 11:11

Security Hound


The reason your code doesn't work is that generics don't inherit the hierarchy of their arguments: BindingList<SubBob> does not derive from BindingList<Bob>. It has nothing to do with their memory layouts.

A BindingList<Bob> can already contain SubBob objects (and anything else deriving from Bob), so there might not actually be a need for a BindingList<SubBob>. If for some reason there is a reason you need both, your only "easy" option may be to use the non-generic IBindingList interface.

.NET 4 supports interface covariance which allows downconverting read-only generic interfaces into the same interface with a base class, so it may or may not be helpful in your situation... that is, if you weren't stuck with 3.5:

IEnumerable<Bob> myList = new BindingList<SubBob>();
like image 36
Cory Nelson Avatar answered Nov 13 '22 11:11

Cory Nelson


If you're stuck with .NET 3.5 and cannot change your data access layer to provide something more convenient, you can use a rather dirty conversion:

BindingList<SubBob> subBobs = new BindingList<SubBob>;
BindingList<Bob> bobs = new BindingList(subBobs.Cast<Bob>().ToList())

Apart from mediocre design, this introduces the cost of reconstructing the list from IEnumerable<> in ToList(). BindingList<> constructor on the other hand only wraps the list.

like image 30
Jacek Gorgoń Avatar answered Nov 13 '22 10:11

Jacek Gorgoń