3

If I have a root interface

public interface IEntity {}

And a derived interface, and classes:

public interface IFruit : IEntity {}
public class Apple : IFruit {}
public class Orange: IFruit {}

And, irrelevant, but perhaps others that don't implement IFruit:

public class Computer : IEntity {}

And a generic class that uses all this:

public class PurchasedItem<T> where T : IFruit 
{
    public int Qty{get;set;}
    public T Item{get;set;}
}

How can I declare a list that contains PurchasedItem<IFruit> and work with it?
If I do this:

var list = new List<PurchasedItem<IFruit>>();
list.Add(new PurchasedItem<Apple>());

...then I get an error

Cannot convert from PurchasedItem<Apple> to PurchasedItem<IFruit>

Geoff
  • 8,551
  • 1
  • 43
  • 50
  • That *is* how you declare a list that contains `PurchasedItem` and work with it. Which means you can't store a `PurchasedItem` in it, since that is not a `PurchasedItem`. This has been asked many times before, I'll see if I can find a good question with detailed answer. –  May 17 '16 at 21:06
  • did you try casting to base type ? , new PurchasedItem()).Cast – Shachaf.Gortler May 17 '16 at 21:08
  • @Shachaf.Gortler That doesn't apply here, but wouldn't be useful even if it did apply. –  May 17 '16 at 21:10
  • @Shachaf.Gortler `.Cast` is an extension method of `IEnumerable`. Casting is not the issue. The problem is that it is a list of `PurchasedItem` not `PurchasedItem`. – Matt Rowland May 17 '16 at 21:11
  • I guess I'm having trouble understanding why `PurchasedItem` isn't the same as `PurchasedItem`. – Geoff May 17 '16 at 21:12
  • The `Apple` implements `IFruit` but `PurchasedItem` doesn't implement `PurchasedItem`. You would need a parent class that `PurchasedItem` could inherit from. – Matt Rowland May 17 '16 at 21:13
  • I *think* [Generics and casting - cannot cast inherited class to base class](http://stackoverflow.com/q/3528821) is a good dupe target (despite the base class / implemented interface difference), but I'll let someone else review and make the call. –  May 17 '16 at 21:17
  • 3
    @Geoff For your particular case, consider `list.Add(new PurchasedItem()); list[0].Item = new Orange();`. Since `list[0]` has type `PurchasedItem`, which has an `IFruit Item` property with an accessible setter, the second statement cannot be invalid. The two statements combined clearly must be invalid: `PurchasedItem`'s `Item` property does not accept `Orange` values. Therefore, the first statement must be invalid. –  May 17 '16 at 21:24
  • @hvd Ok, I think I understand your point - the list implies a wider `Item` type than the instance does. – Geoff May 18 '16 at 13:00

2 Answers2

4

You can use covariance if you create and work with an interface for PurchaseItem<T>

public interface IEntity
{
    int Id { get; set; }
}

public interface IFruit : IEntity
{
}

public class Apple : IFruit
{
    public int Id { get; set; }
}

public interface IPurchaseItem<out T> where T : IFruit
{
    int Qty { get; set; }
    T Item { get; } // can't have setter here
}

public class PurchaseItem<T> : IPurchaseItem<T>
    where T : IFruit
{
    public int Qty { get; set; }
    public T Item { get; set; } // setter here no problem
}


class Program
{

    static void Main()
    {
        var applePurchaseItem = new PurchaseItem<Apple>();

        var fruitPurchaseItems = new List<IPurchaseItem<IFruit>>();

        fruitPurchaseItems.Add( applePurchaseItem );
    }
}
Moho
  • 15,457
  • 1
  • 30
  • 31
  • This doesn't work for me. For `T Item` in `IPurchaseItem`, I get that "the type parameter T must be invariantly valid..." – xofz May 17 '16 at 21:42
  • You can't have a setter on the property in the `IPurchaseItem` interface, only on the concrete implementation (note the `T Item { get; }`) – Moho May 17 '16 at 21:43
  • @Moho Thanks for this - it helps a lot, and also prompted me to do a lot of reading! – Geoff May 19 '16 at 12:30
2

It all comes down to the design of the class tree. For example, see the code below:

public interface IEntity {}

public interface IFruit : IEntity {}

public class PurchasedItem
{
    public int Qty { get; set; }
}

public class Apple : PurchasedItem, IFruit {}
public class Orange: PurchasedItem, IFruit {}

var list = new List<PurchasedItem>();
Cubicle.Jockey
  • 3,288
  • 1
  • 19
  • 31
Riad Baghbanli
  • 3,105
  • 1
  • 12
  • 20
  • This solves the compilation error, but it doesn't solve the issue of being able to access an `IFruit` cleanly from the list. – John H May 17 '16 at 21:36
  • John H, nothing stops you from doing this: var list = new List(); – Riad Baghbanli May 18 '16 at 00:11
  • I don't want to subclass `PurchasedItem` -> `Apple` because `Apple` is a business object that has other uses, I don't want to tangle them up with `PurchasedItem` - and already has an ancestor. – Geoff May 18 '16 at 20:18