0

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);
Zac
  • 1,722
  • 1
  • 19
  • 22

2 Answers2

2

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.

Sean
  • 60,939
  • 11
  • 97
  • 136
  • Thanks for the response @Sean. I've added a caveat to my answer. But essentially the service layer is returning the strongly typed pairings. I also updated the answer to show how I'm really trying to represent these pairings, it's not actually a Tuple but a class with generic type parameters. I'm trying to avoid casing back to the interface when adding these items to the list. – Zac Jan 06 '17 at 12:29
  • 1
    @Zac You cannot avoid having them typed as the interface, rather than the underlying type, because that's what the `List` requires. – Servy Jan 06 '17 at 14:31
1

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.

Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
  • Thanks a lot for your answer. But how would I actually instantiate one of these pairings? e.g. inside the `GetCansOfBeans()` method what implementation of `IReadableItemAndTranslationPair` do I use? This is where I'm getting lost with other online QAs for covariance and the `out` modifier. – Zac Jan 06 '17 at 14:13
  • @Zac You should have a class such as ` class ItemAndTranslationPair` which implements the `IReadableItemAndTranslationPair` interface. The class could have both `get` and `set` for its properties (only the `get` part would be part of the interface "contract"). The return type of the `GetCansOfBeans()` method should involve the interface only, but the actual pairs you "yield" should be instances of the class. – Jeppe Stig Nielsen Jan 06 '17 at 14:34
  • Cheers @Jeppe. Having both the `get` and `set` part on the implementation whilst only having `get` on the contract was where I was getting confused. I can live with my `GetCansOfBeans()` method returning the interface, although my other methods like `DeleteCansOfBeans(IEnumerable>)` will presumably no longer work with those lists, which seems unsatisfactory. – Zac Jan 08 '17 at 18:14