1

Given these interfaces and classes...

public interface IPage
{
    string PageTitle { get; set; }
    string PageContent { get; set; }
}


public abstract class Page
    : IPage
{
    public string PageTitle { get; set; }
    public string PageContent { get; set; }
}


public class AboutPage
    : Page
    , IPage
{
}


public interface IPageAdminViewModel<out T>
    where T : IPage
{
    IEnumerable<T> Pages { get; }
}


public abstract class PageAdminViewModel<T>
    : IPageAdminViewModel<T>
    where T: IPage
{
    public IEnumerable<T> Pages { get; set; }
}

Why does using IPage as an interface type parameter not compile....

public class AboutPageAdminViewModel
    : PageAdminViewModel<AboutPage>
    , IPageAdminViewModel<IPage> // ERROR HERE - I realise this declaration is not required, just indicating where the error is (not)
{
}

'AboutPageAdminViewModel' does not implement interface member 'IPageAdminViewModel<IPage>.Pages'. 'PageAdminViewModel<AboutPage>.Pages' cannot implement 'IPageAdminViewModel<IPage>.Pages' because it does not have the matching return type of 'IEnumerable<IPage>'.

....when using the concrete class AboutPage does?

public class AboutPageAdminViewModel
    : PageAdminViewModel<AboutPage>
    , IPageAdminViewModel<AboutPage> // NO ERROR - I realise this declaration is not required, just indicating where the error is (not)
{
}

Essentially, I don't understand why the return type of IEnumerable<IPage> does not match the return type of IEnumerable<AboutPage>.

I would like to create a method that takes IPageAdminViewModel<IPage> as an argument. I am wondering how / if I can make AboutPageAdminViewModel fit that requirement.

Martin Hansen Lennox
  • 2,837
  • 2
  • 23
  • 64

4 Answers4

8

Essentially, I don't understand why the return type of IEnumerable<IPage> does not match the return type of IEnumerable<AboutPage>.

First off: you are correct that the property meets all the requirements of the IEnumerable<IPage> contract. It would be typesafe for C# to allow this.

The feature you want is called virtual return type covariance, and C# does not support it. You can see this with far simpler examples than yours:

class Animal {}
class Tiger : Animal {}
interface ICage { Animal GetAnimal(); }
class TigerCage : ICage { public Tiger GetAnimal() => new Tiger(); }

This is totally safe. ICage requires that implementers have a method that returns an animal, and TigerCage does; it returns a Tiger which is an Animal. This is safe, but not legal.

C++ has this feature. C# does not.

People have been asking for this feature for literally 15+ years; you can search for "C# return type covariance" on this site and you will find many answers (by me and others) about this.

It's never been implemented because the CLR does not support it natively, there are easy workarounds using explicit interface implementations and shadowing, and it introduces new kinds of brittle base class failures into the ecosystem. For these and other reasons it has always been a low priority for the compiler team compared to other features that are more value for the effort.

If you want it, go advocate for it on the GitHub forum; I'm sure you will have company, but I'd also not hold my breath waiting for the compiler team to implement it; we've waited a long time already!

Incidentally, no one ever asks for virtual parameter type contravariance -- that is, override a method that takes a Tiger with a method that takes an Animal. But it is equally safe.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Thank you Eric, nicely explained. Does that mean it's basically impossible to do what I'm after? *(I would like to create a method that takes `IPageAdminViewModel` as an argument. I am wondering how / if I can make `AboutPageAdminViewModel` fit that requirement)* – Martin Hansen Lennox Jul 20 '18 at 21:58
  • @MartinHansenLennox: As I said in the answer, one of the reasons it is not implemented in the language is because there are easy workarounds. Use a combination of explicit interface implementation and a shadowing method to achieve this. – Eric Lippert Jul 23 '18 at 17:01
  • Easy is relative... but I am working it out. It means a lot that people such as yourself and Jon are willing to hang out on here and continue explaining stuff to dweebs like myself. – Martin Hansen Lennox Jul 23 '18 at 23:08
2

Picking up on Eric’s example:

class Animal {}
class Tiger : Animal {}

interface ICage { 
    Animal GetAnimal(); }

class TigerCage : ICage { 
    //Won’t compile
    public Tiger GetAnimal() => 
        new Tiger(); }

You can solve this by implementing both an explicit interface member and a strongly typed method, one of which will delegate to the other to avoid code duplication:

class TigerCage: ICage {
    Animal ICage.GetAnimal() => GetAnimal();
    public Tiger GetAnimal() => new Tiger(); }
InBetween
  • 32,319
  • 3
  • 50
  • 90
1

IPageAdminViewModel<IPage> requires implementing IEnumerable<IPage> Pages { get; }, whereas your first example "only" implements IEnumerable<AboutPage> Pages { get; }.

While IEnumerable is coavariant, it doesn't mean these are identical and having one is insufficient to satisfy the requirements for the other.

Your <out T> only makes IPageAdminViewModel coavariant, that is, you could assign an instance of the type to a variable / property defined with a base generic type.

Amit
  • 45,440
  • 9
  • 78
  • 110
  • Thank you Amit. Between your answer, Eric's and John's I'm finally getting it. It seems that what I want to do is impossible. This way at least. :( – Martin Hansen Lennox Jul 20 '18 at 22:06
1

The problem isn't that it's a concrete type. The problem is that they have to match.

Both of these will compile:

public class AboutPageAdminViewModel
    : PageAdminViewModel<IPage>
    , IPageAdminViewModel<IPage> 
{
}


public class AboutPageAdminViewModel
    : PageAdminViewModel<AboutPage>
    , IPageAdminViewModel<AboutPage> 
{
}

If they don't match, the class will have to find a way to have a Pages property that returns both an IEnumerable<IPage> and an IEnumerable<AboutPage>, which isn't possible. They have a covariant relationship, but they are not the same thing as each other.

John Wu
  • 50,556
  • 8
  • 44
  • 80
  • Thank you John. I would like to have a service, or possibly even a view, that will take `IPageAdminViewModel` as an argument. Is there a way for me to make `AboutPageAdminViewModel` fit that requirement? – Martin Hansen Lennox Jul 20 '18 at 21:42