Unable to add a generic concrete type to a list of generic interface

187 Views Asked by At

Background:

I am trying to create a list that can store different types/implementations of people. This list will then be filtered and operated on based on what type of person they are, and what data source their information has come from.

This is what is failing:

When attempting to add my concrete types to my list that is typed against my interface, it is failing convertion and unable to perform casts to (see below), regardless of the type constraint on my generic.

var people = new List<Person<IPerson>>();

people.Add(new Person<Student>()); // Compile error: Cannot convert from Person<Student> to Person<IPerson> 
people.Add(new Person<StaffMember>() as Person<IPerson>); from Person<StaffMember> to Person<IPerson> via reference conversion, boxing, unboxing etc.

If I update my list to ignore the "wrapper" Person<> generic type and just store the list of people types, it looks like this is valid. So something is clearly wrong with my assumptions around the generic side of things!

var people = new List<IPerson>();

people.Add(new Student()); // This is valid (at least at compile time)
people.Add(new StaffMember()); // This is valid (at least at compile time)

Current implementation:

public interface IPerson
{ }

public class Person<TPerson> where TPerson : IPerson
{
    // Various common props here...
    public DataSource Source { get; set; }

    // My generic prop
    public TPerson Record { get; set; } 
}

public class Student : IPerson
{
    // Student specific props
}

public class StaffMember : IPerson
{
    // Staff member specific props
}

Any ideas?

2

There are 2 best solutions below

5
JonasH On BEST ANSWER

If you want Person<T> to be assignable to Person<IPerson> the type needs to be Covariant, and you need an interface to make covariant:

public interface IPerson { }
public interface IPerson<out T> {
    T Record { get; } // setter not allowed
 }

public class Person<TPerson> : IPerson<TPerson> where TPerson : IPerson
{
    // My generic prop
    public TPerson Record { get; set; }
}
public class Student : IPerson { }
...
var list = new List<IPerson<IPerson>>();
list.Add(new Person<Student>()); // works

This covariance will place some limits on how the TPerson can be used in the IPerson<T> interface. A property like T Record { get; } is fine, since you are returning a type T, and all such types are an IPerson. A method like void MyMethod(T value) is not fine, since it would allow calling a method on Person<Student> object with a StaffMember object, and that is not allowed.

But do consider if you need to use generics in the first place, and if you do, consider naming. Person<IPerson> or IPerson<IPerson> can easily be confusing.

3
Jonathan Dodds On

So something is clearly wrong with my assumptions around the generic side of things!

That would seem to be the case although I'm not clear on what your assumptions are.

Generics don't support composing types. If your assumption is that an instance of a Person<Student> would have the members of Student and the members of Person<T>, that is not correct.

Generics are sometimes called 'templates' and I think that name is useful in understanding generics.

In an abstract sense, what does a list provide? Add? Remove? Index? If you had to write a list for every type, the code would not vary much. The implementation of a List of Student and a list of StaffMember would be the same except for the type used. List<T> is a generic type. The implementation is written in terms of T and not a specific type. It can be thought of as a template for creating a list of any type.

You can't have a heterogeneous collection. If you use an ArrayList, everything from the perspective of the collection is an object. If you use a List<T> everything is whatever you specified for T.

If you define a List<IPerson> you can add any object that implements (or 'is a') IPerson. If the IPerson interface has a Name property, you can get the Name from any IPerson regardless of whether it is a Student or StaffMember.

List<IPerson>, List<Student>, and List<StaffMember> are different unique types. You cannot add a StaffMember to a List<Student>.

Your wrapper class could be written as follows and used with a List<Person>.

public class Person
{
    public Person(IPerson person)
    {
        Record = person;
        ...
    }

    public DataSource Source { get; set; }

    public IPerson Record { get; set; }
}