8

I have an abstract Content class and concrete subclasses

public abstract class Content

public class ContentA : Content

public class ContentB : Content

I also have an abstract generic ContentSource class and concrete subclasses

public abstract class ContentSource<T> where T : Content

public class SourceX : ContentSource<ContentA>

public class SourceY : ContentSource<ContentB>

And I want to have a list of ContentSource<Content> objects that are the subclasses of ContentSource

var ContentSources = new List<ContentSource<Content>>
{
    new SourceX(),
    new SourceY(),
};

But this doesn't compile - I get a 'Cannot convert from SourceX to ContentSource' error.

Why does this not work?

sean2000
  • 506
  • 4
  • 12

2 Answers2

7

This can be achieved in C# using covariance, but you'd have to use an interface as the list type:

public interface IContentSource<out T> where T : Content {}
public class SourceX : IContentSource<ContentA> {}
public class SourceY : IContentSource<ContentB> {}

var ContentSources = new List<IContentSource<Content>>
{
    new SourceX(),
    new SourceY(),
};

Working example
This is explained nicely here: <out T> vs <T> in Generics

You can still use an abstract class, but the list would still have to be a list of the interface:

public interface IContentSource<out T> where T : Content {}
public abstract class ContentSource<T> : IContentSource<T> where T : Content {}
public class SourceX : ContentSource<ContentA> {}
public class SourceY : ContentSource<ContentB> {}

There is also a great explanation of why it isn't supported for classes: Why does C# (4.0) not allow co- and contravariance in generic class types?

Kobi
  • 135,331
  • 41
  • 252
  • 292
  • Thank you, that helps a lot! But if I add a method like `string GetLocalPath(T content);` to the IContentSource interface I get a compile error "Invalid variance: The type parameter 'T' must be contravariantly valid" – sean2000 Oct 07 '18 at 06:15
  • @sean2000, The "out" means "T is only used in output positions". You are using it in an input position – Hasan Emrah Süngü Oct 07 '18 at 06:59
  • So there's no way of using `T` as a method parameter? – sean2000 Oct 07 '18 at 07:07
  • 1
    @sean2000, if you define T as out then you can not use T as an input paramater. It wouldn't make sense anyways. It may be better to explain what you want to do and how you came up with this scheme – Hasan Emrah Süngü Oct 07 '18 at 07:15
  • So I have a few different API endpoints where each one is represented by a `ContentSource` class and returns a list of `Content` . The `ContentSource` classes have methods `ContentResultSet Search(Query query)` (to return results of a search) and `string GetLocalPath(T content)` (to download content and return the path where it is saved) – sean2000 Oct 07 '18 at 21:27
2

While Kobi gives a perfect answer how you can get it to work, I will give you a simple answer why it does not work.

In your example, you want to use polymorphism to manage different types that derive from a common base class in a single list of type base class. Something along the lines→

public abstract class Base {}
public class DerivedFirst  : Base {}
public class DerivedSecond : Base {}

var first  = new DerivedFirst();
var second = new DerivedSecond(); 
var list   = new List<Base>{first,second}

Which is perfectly fine as they derive from the common Base class. Now let us take a look at your case. But before that you should understand the difference between an Open Type and Closed Type. Simply put any generic type without its type parameters are open types and can not be instantiated. List<> is an open type whereas List<int> is a closed type. When you create a closed type it does not derive from its open definition. It is a completely new type on its own.

var i_am_false = typeof(List<int>).IsSubclassOf(typeof(List<>));//this is false

Here is your case. You define your classes which will be type arguments.

public abstract class Content

public class ContentA : Content

public class ContentB : Content

Here you define your sources

public abstract class ContentSource<T> where T : Content

public class SourceX : ContentSource<ContentA>   

public class SourceY : ContentSource<ContentB>

Putting to words

You have SourceX which derives from ContentSource<ContentA> which derives from Object

You have SourceY which derives from ContentSource<ContentB> which derives from Object

and finally

You have var ContentSources = new List<ContentSource<Content>>, which is a list of ContentSource<Content> which is in no way connected to ContentSource<ContentA> or ContentSource<ContentB>. They simply have same implementation with respect to generic parameter.

In the end, let us call

ContentSource<Content> class M which derives from Object
ContentSource<ContentA> class N which derives from Object
ContentSource<ContentB> class O which derives from Object

and have a look at you are doing→

public class SourceX : N{}  

public class SourceY : O{}

var ContentSources = new List<M>{
    new SourceX(),
    new SourceY(),
};

Of course it would not work :). Lastly why it works with covariance please have a look at Kobi's answer.

Hasan Emrah Süngü
  • 3,488
  • 1
  • 15
  • 33
  • 1
    This is an excellent and educational answer. It is without doubt correct. However, conceptually, I think the truth of this shows that the language has been incorrectly implemented. – Frank Apr 29 '22 at 10:47