Covariance confusion. Can't assign tuples of implemented interfaces to list of tuples

428 views Asked by At

Preface: I am aware that there are a lot of questions and answers about covariance and contravariance but I'm still feeling muddled and not sure what solution to implement.

I have two interfaces whose implementations are intended to be used together in pairs. One provides the information about a sales item and one provides language dependent information for a sales item.

I do not have control over these interfaces:

public interface IItem
{
    decimal Price { get; set; }
}

public interface IItemTranslation
{
    string DisplayName { get; set; }
}

I also have two implementations of both these interfaces for a tangible GoodsItem as well as an intangible ServiceItem. Again, I do not have control over these interfaces:

public class GoodsItem : IItem
{
    public decimal Price { get; set; } //implementation
    public float ShippingWeightKilograms { get; set; } //Property specific to a GoodsItem
}

public class GoodsTranslation : IItemTranslation
{
    public string DisplayName { get; set; } //implementation
    public Uri ImageUri { get; set; } //Property specific to a GoodsTranslation
}


public class ServiceItem : IItem
{
    public decimal Price { get; set; } //implementation
    public int ServiceProviderId { get; set; } // Property specific to a ServiceItem
}

public class ServiceTranslation : IItemTranslation
{
    public string DisplayName { get; set; } //implementation
    public string ProviderDescription { get; set; } // Property specific to a ServiceTranslation
}

As I said, these are classes that I do not have control over. I want to create a generic list of these pairings (List<Tuple<IItem, IItemTranslation>>) but I cannot:

public class StockDisplayList
{
    public List<Tuple<IItem, IItemTranslation>> Items { get; set; }

    public void AddSomeStockItems()
    {
        Items = new List<Tuple<IItem, IItemTranslation>>();

        var canOfBeans = new Tuple<GoodsItem, GoodsTranslation>(new GoodsItem(), new GoodsTranslation());

        var massage = new Tuple<ServiceItem, ServiceTranslation>(new ServiceItem(), new ServiceTranslation());

        Items.Add(canOfBeans); //illegal: cannot convert from 'Tuple<GoodsItem, GoodsTranslation>' to 'Tuple<IItem, IItemTranslation>'
        Items.Add(massage); //illegal: cannot convert from 'Tuple<ServiceItem, ServiceTranslation>' to 'Tuple<IItem, IItemTranslation>'    }
}

Question: Without changing my IItem and ITranslation classes or their derived types, what's the cleanest way to be able to pass around a generic list of these pairings without casting them back and forth between the interface and their type?

Caveat. I was trying to simplify the question but I'm not actually using Tuples. In reality I'm using a class like this:

public class ItemAndTranslationPair<TItem, TItemTranslation> where TItem : class, IItem where TItemTranslation : class, IItemTranslation
{
    TItem Item;
    TTranslation Translation;
}

and my services are returning strongly typed lists, like List<ItemAndTranslationPair<GoodsItem, GoodsTranslation>> and therefore when I add items to the 'generic' list it looks like:

var differentBrandsOfBeans = SomeService.GetCansOfBeans();
//above variable is of type IEnumerable<ItemAndTranslationPair<GoodsItem, GoodsTranslation>>

var items = new List<ItemAndTranslationPair<IItem, IItemTranslation>>();
items.AddRange(differentBrandsOfBeans);
2

There are 2 answers

3
Jeppe Stig Nielsen On BEST ANSWER

Use the out modifier on the type parameter of the generic type to obtain covariance in that parameter.

In the current version of C#, this is not supported for class types, only interface types (and delegate types), so you would need to write an interface (notice use of out):

public interface IReadableItemAndTranslationPair<out TItem, out TItemTranslation>
  where TItem : class, IItem
  where TItemTranslation : class, IItemTranslation
{
  TItem Item { get; }
  TItemTranslation Translation { get; }
}

Note that the properties cannot have a set accessor since that would be incompatible with the covariance.

With this type, you can have:

var differentBrandsOfBeans = SomeService.GetCansOfBeans();
//above variable is of type
//IEnumerable<IReadableItemAndTranslationPair<GoodsItem, GoodsTranslation>>

var items = new List<IReadableItemAndTranslationPair<IItem, IItemTranslation>>();

items.AddRange(differentBrandsOfBeans);

It works because IEnumerable<out T> is covariant, and your type IReadableItemAndTranslationPair<out TItem, out TItemTranslation> is covariant in both TItem and TItemTranslation.

2
Sean On

You need to create the tuples using the interface types:

var canOfBeans = new Tuple<IItem, IItemTranslation>(new GoodsItem(), new GoodsTranslation());
var massage = new Tuple<IItem, IItemTranslation>(new ServiceItem(), new ServiceTranslation());

As you're storing them in a list of Tuple<IItem, IItemTranslation> this should be a problem for you.