0

I feel like interface (contra?)variance is the answer, but cannot find the right solution.

Let us have these classes:

public abstract class Fruit { }
public class Banana : Fruit { }
public class Apple : Fruit { }

public abstract class Picture { }
public class FruitPicture<T> : Picture, Contravariant<T>, Covariant<T> where T : Fruit
{
    T myFruit = null;

    public Type GetFruitType() { return typeof(T); }

    public void AddFruit(T fruit) { this.myFruit = fruit; }
}

public interface Contravariant<in T> { void AddFruit(T model); }
public interface Covariant<out T> { Type GetFruitType(); }

My situation being:

  • I have a collection of Bananas and Apples already initialized, such as these two (but I can use a different one):

    Fruit[] myFruits = new Fruit[2]
    {
        new Banana(),
        new Apple()
    };
    
  • I have a collection of Pictures, such as these two:

    Picture[] myPictures = new Picture[2]
    {
        new FruitPicture<Banana>(),
        new FruitPicture<Apple>(),
    };
    

Now, I seek to do a very simple thing, but in a versatile manner, meaning I want to avoid any switches/ifs where I would have to change code each time a new fruit is found and new FruitPicture may appear in the collection => I want to .AddFruit() from my collection to the proper type of FruitPicture. I can change pretty much any of the logic, but I want to keep the generic FruitPicture class.

Closest I got would be:

foreach(Picture curPicture in myPictures)
{
    foreach (Fruit curFruit in myFruits)
    {
        Covariant<Fruit> fruitType = (Covariant<Fruit>)curPicture;
        if (curFruit.GetType() == fruitType.GetFruitType())
        {
            // what now?
        }
    }
}

Thank you mr. Skeet (joking; sort of)

Brian McCutchon
  • 8,354
  • 3
  • 33
  • 45
Demo
  • 394
  • 1
  • 4
  • 16
  • Why is the `Covariant` interface generic when the type parameter is not used? – pinkfloydx33 Oct 22 '18 at 21:46
  • @pinkfloydx33 I wouldn't pretend to have completely clear ideas about this, but: to actually allow the covariance: without `out T`, line `Covariant fruitType = (Covariant)curPicture` would fail. But possibly no interface is needed at all, I'm open to anything :) – Demo Oct 22 '18 at 21:53
  • `IGetType` or something with a better name, lacking the type parameter. You don't use it, it serves no purpose – pinkfloydx33 Oct 22 '18 at 21:55
  • @pinkfloydx33 You were right, the "Covariant" interface doesn't need to be generic at all. In order for it to be actually covariant its method would have to return `T`, otherwise simple IGetType is sufficient – Demo Oct 23 '18 at 16:06

1 Answers1

0

Since the issue you have is you want to do compile-time type-safety but you don't know the types until run-time, you can push the decision off until run-time by using dynamic. I am not necessarily recommending this, just saying it will work.

I changed the Covariant interface to not need be generic since it made no use of the type parameter. I renamed AddFruit to SetFruit since it didn't add anything, but replaced.

foreach (var fruit in myFruits) {
    foreach (var picture in myPictures) {
        if (picture is Covariant cov) {
            if (cov.GetFruitType() == fruit.GetType())
                ((dynamic)picture).SetFruit(Convert.ChangeType((dynamic)fruit, cov.GetFruitType()));
        }
    }
}

The (dynamic) ChangeType is needed since the type of fruit is Fruit, which is not a valid type to pass to any SetFruit. It has to be dynamic since the static compile-time type of ChangeType is object, which is also not a valid type for any SetFruit.

Alternatively, what if you pushed the decision into the FruitPicture?

public interface Covariant {
    void SetCompatibleFruit(Fruit f);
}

public class FruitPicture<T> : Picture, Covariant where T : Fruit {
    T myFruit = null;

    public void SetCompatibleFruit(Fruit f) {
        if (f is T tf)
            this.myFruit = tf;
    }
}

Then just ask each Covariant picture to set the fruit if it can:

foreach (var fruit in myFruits) {
    foreach (var picture in myPictures) {
        if (picture is Covariant cov)
            cov.SetCompatibleFruit(fruit);
    }
}
NetMage
  • 26,163
  • 3
  • 34
  • 55
  • Thank you, both seem to work. The first one I didn't like much and it required .NET 4.5, but I'm running 4.0 which I forgot to mention. The second one interests me tho, I didn't think of that! I'll check it in more depth later as it's late in my time zone now. – Demo Oct 22 '18 at 22:28
  • Basically, the second is a version of double-dispatch which is used for two way polymorphism. – NetMage Oct 22 '18 at 23:41
  • Marked as answer because of the alternative approach: delegating the decision inside of the `FruitPicture` class. No `` interface is needed and it allows me to keep my FruitPicture generic, which is what I needed. The fact that the model is set by checking the type at runtime is of no complication for me. Also thanks for showing me the syntactic sugar `varA is MyType varB` - I didn't know one can do the varB thing inline like this :) – Demo Oct 23 '18 at 16:16
  • It is one of the C# 7 features - a part of [pattern matching](https://blogs.msdn.microsoft.com/seteplia/2017/10/16/dissecting-the-pattern-matching-in-c-7/). I sometimes think C# evolves too slowly (or doesn't evolve uniformly enough), but they do good work on new features as well. – NetMage Oct 23 '18 at 20:32