Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# casting an inherited Generic interface

I'm having some trouble getting my head around casting an interface I've come up with. It's an MVP design for C# Windows Forms. I have an IView class which I implement on my form classes. There's also an IPresenter which I derive into various specific Presenters. Each Presenter will manage the IView differently depending on the role, for example opening the dialog to enter a new set of data with an AddPresenter as opposed to editing existing data with an EditPresenter which would preload data onto the form. Each of these inherit from IPresenter. I want to use the code as such:

AddPresenter<ConcreteView> pres = new AddPresenter<ConcreteView>();

I basically have this working but these presenters and the views they manage are bundled into plugins which are loaded post runtime which means I need a Manager class which acts as the plugin interface take a "mode" parameter. This mode parameter is use for a factory method to create either the Add or Edit Presenter, but because the call to show the dialog is made later on then I need to make the call via the IPresenter interface like so:

private IPresenter<IView> pres;
public ShowTheForm()
{
    pres.ShowDialog();
}

Now I'm having issues when it comes to casing my concrete instantiation of an AddPresenter say, to the 'pres' member. Here's a cut down simplified version of what I have:

interface IView
{
    void ViewBlah();
}

interface IPresenter<V> where V : IView
{
    void PresBlah();
}

class CView : IView
{
    public void ViewBlah()
    {        
    }
}

class CPresenter<T> : IPresenter<T> where T : IView
{
    public void PresBlah()
    {
    }
}

private void button3_Click(object sender, EventArgs e)
{
    CPresenter<CView> cpres = new CPresenter<CView>();
    IPresenter<IView> ipres = (IPresenter<IView>)cpres;
}

This is the error:

Unable to cast object of type 'CPresenter`1[MvpApp1.MainForm+CView]' to type 'IPresenter`1[MvpApp1.MainForm+IView]'.

Both the Presenter and the Generic type specification from what I can tell ARE subclasses of the interfaces so I can't understand why it won't cast.

Any thoughts?

Steve

like image 564
Stephen York Avatar asked Sep 11 '12 08:09

Stephen York


2 Answers

The problem is the generic type parameter. If you make the interface parameter covariant then the cast will work.

This is accomplished by adding the out keyword, like so:

interface IPresenter<out V> where V : IView
{
    void PresBlah();

}

You can learn more about how this works with the following MSDN article: Covariance and Contravariance in Generics. The section Generic Interfaces with Covariant Type Parameters specifically applies to your question.

Update: Make sure you check the comments between @phoog and me. If your actual code accepts a V as an input, you will be unable to make it covariant. The referenced article and @phoog's answer explains this case in further detail.

like image 98
smartcaveman Avatar answered Nov 05 '22 12:11

smartcaveman


CPresenter<CView> is not an IPresenter<IView>, just as List<int[]> is not an IList<IEnumerable>.

Think about it. If you could get an IList<IEnumerable> reference to a List<int>, you could add a string[] to it, which would have to throw an exception. The whole point of static type checking is to prevent the compilation of such code.

If the interface allows, you could declare the type parameter as covariant (IPresenter<out V> where V : .... Then the interface would behave more like IEnumerable<out T>. This is only possible if the type parameter is never used in an input position.

To go back to the List<int[]> example, it is safe to treat it as an IEnumerable<IEnumerable>, because you can't add anything to an IEnumerable<T> reference; you can only read things out of it, and, in turn, it is safe to treat an int[] as an IEnumerable, so all is well.

like image 43
phoog Avatar answered Nov 05 '22 12:11

phoog