Search code examples
c#listgenericsinterfaceconceptual

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


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?


Solution

  • 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.