Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inherited Generic Type Unification

Tags:

c#

generics

For a scenario such as this:

public interface IAnimal
{

}

public interface IGiraffe : IAnimal
{

}

public interface IQuestionableCollection : IEnumerable<IAnimal>
{
    void SomeAction();
}

public interface IQuestionableCollection<out T> : IQuestionableCollection, IEnumerable<T>
    where T : IAnimal
{

}

public class QuestionableCollection<T> : IQuestionableCollection<T>
    where T:IAnimal
{
    // Implementation... 
}

The complier will generate an error:

'IQuestionableCollection<T>' cannot implement both 'System.Collections.Generic.IEnumerable<IAnimal>' and 'System.Collections.Generic.IEnumerable<T>' because they may unify for some type parameter substitutions

And that makes sense, there is indeed an ambiguity between the two interfaces which C# can't resolve unless it uses the type constraint, which it doesn't per the language spec as @ericlippert explains here.

My question is how should I implement something to the same effect here?

It seems like I should be able to express that the collection is enumerable for the base interface. (I'd like to provide a set of methods that could be utilized without knowing the concrete type, as well as it make some APIs/reflection code cleaner, so I'd like to keep the base collection as non-generic if at all possible. Otherwise, there would be no need for two interfaces.)

The only implementation I can think of that compiles is something like:

public interface IQuestionableCollectionBase
{
    void SomeAction();
}

public interface IQuestionableCollection : IQuestionableCollectionBase, IEnumerable<IAnimal>
{

}

public interface IQuestionableCollection<out T> : IQuestionableCollectionBase, IEnumerable<T>
    where T : IAnimal
{

}

public class QuestionableCollectionBase<T> : IQuestionableCollection
    where T : IAnimal
{
    protected List<T> _items = new List<T>();

    public void SomeAction() { }

    IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); }
    IEnumerator<IAnimal> IEnumerable<IAnimal>.GetEnumerator() { return ((IEnumerable<IAnimal>)_items).GetEnumerator(); }
}

public class QuestionableCollection<T> : QuestionableCollectionBase<T>, IQuestionableCollection<T>
    where T : IAnimal
{
    public IEnumerator<T> GetEnumerator() { return ((IEnumerable<T>)_items).GetEnumerator(); }
}

Note that I've had to move any methods I'd like to use on both interfaces to a base method and have two levels of implementation for the class itself - which seems like I'm jumping through enough hoops here that I've got to be missing something...

How should this be implemented?

like image 631
Gene Avatar asked Apr 27 '15 23:04

Gene


2 Answers

The simplest workaround is to change the IEnumerables from "is-a" to "has-a", like this:

public interface IAnimal { }
public interface IGiraffe : IAnimal { }

public interface IQuestionableCollection
{
    IEnumerable<IAnimal> Animals { get; }
    void SomeAction();
}

public interface IQuestionableCollection<out T> : IQuestionableCollection
    where T : IAnimal
{
    new IEnumerable<T> Animals { get; }
}

public class QuestionableCollection<T> : IQuestionableCollection<T>
    where T : IAnimal, new()
{
    private readonly List<T> list = new List<T>();

    public IEnumerable<T> Animals
    {
        get { return list; }
    }

    IEnumerable<IAnimal> IQuestionableCollection.Animals
    {
        get { return (IEnumerable<IAnimal>)list; }
    }

    public void SomeAction()
    {
        list.Add(new T());
    }
}

class Giraffe : IGiraffe { }

[TestMethod]
public void test()
{
    var c = new QuestionableCollection<Giraffe>();
    IQuestionableCollection<Giraffe> i = c;
    IQuestionableCollection<IGiraffe> i2 = i;

    Assert.AreEqual(0, c.Animals.Count());
    Assert.AreEqual(0, i.Animals.Count());
    c.SomeAction();
    i.SomeAction();
    Assert.AreEqual(2, c.Animals.Count());
    Assert.AreEqual(2, i.Animals.Count());
}

Note that you can avoid the cast in QuestionableCollection<T> if you add a where T : class constraint.

like image 116
default.kramer Avatar answered Nov 15 '22 20:11

default.kramer


Changing IQuestionableCollection to a non-generic IEnumerable sorts the compiler issues.

public interface IQuestionableCollection : IEnumerable {...}

I've seen MS use this pattern in their collections, with the non-generic versions using IEnumerable, and the generic ones using IEnumerable<T>.

Alternatively, making the others IEnumerable<IAnimal> also stops the compiler errors, though it means you get IAnimals back instead of T's when enumerating.

like image 37
Will Avatar answered Nov 15 '22 18:11

Will