Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Please help me understand polymorphism when using generics in c#

I am having a problem understanding how polymorphism works when using generics. As an example, I have defined the following program:

public interface IMyInterface
{
    void MyMethod();
}

public class MyClass : IMyInterface
{
    public void MyMethod()
    {
    }
}

public class MyContainer<T> where T : IMyInterface
{
    public IList<T> Contents;
}

I can then do this, which works just fine:

MyContainer<MyClass> container = new MyContainer<MyClass>();
container.Contents.Add(new MyClass());

I have many classes that implement MyInterface. I would like to write a method that can accept all MyContainer objects:

public void CallAllMethodsInContainer(MyContainer<IMyInterface> container)
{
    foreach (IMyInterface myClass in container.Contents)
    {
        myClass.MyMethod();
    }
}

Now, I'd like to call this method.

MyContainer<MyClass> container = new MyContainer<MyClass>();
container.Contents.Add(new MyClass());
this.CallAllMethodsInContainer(container);

That didn't work. Surely, because MyClass implements IMyInterface, I should be able to just cast it?

MyContainer<IMyInterface> newContainer = (MyContainer<IMyInterface>)container;

That didn't work either. I can definitely cast a normal MyClass to IMyInterface:

MyClass newClass = new MyClass();
IMyInterface myInterface = (IMyInterface)newClass;

So, at least I haven't completely misunderstood that. I am unsure exactly how I am to write a method that accepts a generic collection of classes that conform to the same interface.

I have a plan to completely hack around this problem if need be, but I would really prefer to do it properly.

Thank you in advance.

like image 610
Steve Rukuts Avatar asked Aug 25 '10 12:08

Steve Rukuts


2 Answers

Note: In all cases, you will have to initialize the Contents field to a concrete object that implements IList<?>

When you keep the generic constraint, you can do:

public IList<T> Contents = new List<T>();

When you don't, you can do:

public IList<MyInterface> Contents = new List<MyInterface>();

Method 1:

Change the method to:

public void CallAllMethodsInContainer<T>(MyContainer<T> container) where T : IMyInterface
{
    foreach (T myClass in container.Contents)
    {
        myClass.MyMethod();
    }
}

and the snippet to:

MyContainer<MyClass> container = new MyContainer<MyClass>();
container.Contents.Add(new MyClass());
this.CallAllMethodsInContainer(container);

Method 2:

Alternatively, move the CallAllMethodsInContainer method to the MyContainer<T> class like this:

public void CallAllMyMethodsInContents()
    {
        foreach (T myClass in Contents)
        {
            myClass.MyMethod();
        }
    }

and change the snippet to:

MyContainer<MyClass> container = new MyContainer<MyClass>();
container.Contents.Add(new MyClass());
container.CallAllMyMethodsInContents();

Method 3:

EDIT: Yet another alternative is to remove the generic constraint from the MyContainer class like this:

public class MyContainer
{
    public IList<MyInterface> Contents;
}

and to change the method signature to

  public void CallAllMethodsInContainer(MyContainer container)

Then the snippet should work as:

MyContainer container = new MyContainer();
container.Contents.Add(new MyClass());
this.CallAllMethodsInContainer(container);

Note that with this alternative, the container's Contents list will accept any combination of objects that implement MyInterface.

like image 69
Ani Avatar answered Oct 23 '22 16:10

Ani


Wow, this question's been coming up a lot lately.

Short answer: No, this isn't possible. Here's what is possible:

public void CallAllMethodsInContainer<T>(MyContainer<T> container) where T : IMyInterface
{
    foreach (IMyInterface myClass in container.Contents)
    {
        myClass.MyMethod();
    }
}

And here's why what you tried isn't possible (taken from this recent answer of mine):

Consider the List<T> type. Say you have a List<string> and a List<object>. string derives from object, but it does not follow that List<string> derives from List<object>; if it did, then you could have code like this:

var strings = new List<string>();

// If this cast were possible...
var objects = (List<object>)strings;

// ...crap! then you could add a DateTime to a List<string>!
objects.Add(new DateTime(2010, 8, 23));23));

The above code illustrates what it means to be (and not to be) a covariant type. Note that casting a type T<D> to another type T<B> where D derives from B is possible (in .NET 4.0) if T is covariant; a generic type is covariant if its generic type argument only ever appears in the form of output -- i.e., read-only properties and function return values.

Think of it this way: if some type T<B> always supplies a B, then one that always supplies a D (T<D>) will be able to operate as a T<B> since all Ds are Bs.

Incidentally, a type is contravariant if its generic type parameter only ever appears in the form of input -- i.e., method parameters. If a type T<B> is contravariant then it can be cast to a T<D>, as strange as that may seem.

Think of it this way: if some type T<B> always requires a B, then it can step in for one that always requires a D since, again, all Ds are Bs.

Your MyContainer class is neither covariant nor contravariant because its type parameter appears in both contexts -- as input (via Contents.Add) and as output (via the Contents property itself).

like image 25
Dan Tao Avatar answered Oct 23 '22 17:10

Dan Tao