Search code examples
c#generic-collections

'Strongly Typed' Generic Collections that hold any <T> of a given Interface/Class


Is it possible to declare a generic collection to hold only objects implementing a generic Interface with any <T>?

My question burns down to: If I want to/have to store objects implementing a generic interface, is there a better way to express that fact than using a non generic collection or (generic of <Object>).

Example:

// An example Generic Interface
interface ISyncInterface<T>
{
    Task DoSync();
    IEnumerable<T> NewItems { get; }
}

// a manager-class that registers different classes implementing
// the generic interface.
// The code works - can it be done better?
class Manager
{
    private List<Object> _services = new List<Object>(); // <- works but is basically non generic
    // however the RegisterService() ensures that only correct types can be added.

    // would like to have something like below to indicate the Interface-Type
    // however: this would only allow _services2.Add to hold types of ISyncInterface<Object>
    //    - ISyncInterface<ServiceA_DTO> would fail.
    private List<ISyncInterface<Object>> _services2 = new List<ISyncInterface<Object>>();

    void RegisterService<T, U>(T service)
        where T : ISyncInterface<U>
    {
        _services.Add(service); // <- works e.g. for SyncServiceA 

        // _services2.Add(service); // <- FAILS for SyncServiceA - no conversion
        // _services2.Add((ISyncInterface<Object>) service); // <- FAILS also - no explicit cast
    }
}

// SETUP - The classes used above. Just to clarify.
class ServiceA_DTO { }
class ServiceB_DTO { }

class SyncServiceA : ISyncInterface<ServiceA_DTO>
{
    public Task DoSync() {}
    public IEnumerable<ServiceA_DTO> NewItems { get; }
}

class SyncServiceB : ISyncInterface<ServiceB_DTO>
{
    public Task DoSync() {}
    public IEnumerable<ServiceB_DTO> NewItems { get; } 
}

Is this possible at all? Any advice is highly appreciated!

Update: New, more verbose code to clarify the problem.

Below there was a suggestion to base the generic interface on an non generic one. But as a consequence all implementing classes of the generic interface would have to implement the non generic methods, properties etc. - or is there a way around it?

Thanks for your input!


Solution

  • Is it possible to declare a generic collection to hold only objects implementing a generic interface instantiated with any T?

    Short answer: no.

    Longer answer: no, because that is not useful.

    Let's consider a simple generic interface:

    interface I<T> { T Get(); }
    

    And a bunch of objects that implement it:

    class Lion : I<Lion> 
    {
      public Lion Get() => this;
    }
    class TaxPolicyFactory : I<TaxPolicy>
    {
      public TaxPolicy Get() => new TaxPolicy();
    }
    class Door: I<Doorknob>
    {
      public Doorknob Get() => this.doorknob;
      ...
    }
    

    OK, now suppose you have a List<I<ANYTHING>> like you want:

    var list = new List<I<???>> { new TaxPolicyFactory(), new Lion(), new Door() };
    

    You've got a list with a tax policy factory, a lion and a door in it. Those types have nothing in common with each other; there's no operation you can perform on each of those objects. Even if you could call Get on each of them, then you'd have a sequence with a tax policy, a lion and a doorknob in it, and what are you going to do with that?

    Nothing, that's what. The constraint "implements interface I<T> for any T" is simply not a useful constraint in C#, so there is no way to express it.

    It sounds like you have an "XY" problem. That is a problem where you have a bad solution in mind, and now you are asking questions about your bad solution. Ask us a question about the real problem you have, not the bad idea you've got for its solution. What's the real problem?


    UPDATE: With the new information in the question it is now much more clear. The feature you want is called generic interface covariance, which was my favourite feature for C# 4.

    If you update your interface definition to

    interface ISyncInterface<out T> { ... }
    

    then you can use an ISyncInterface<String> in a context where an ISyncInterface<Object> is expected. For example, you could put an ISyncInterface<Giraffe> into a List<ISyncInterface<Animal>> or whatever.

    However you are required to ensure that your interface definition only uses T in a covariantly valid position. Your interface is valid as stated, but if for example you ever want to add a method void M(T t); to your interface, it will no longer be covariantly valid. The "out" is a mnemonic telling you that T can only be used as output of methods. Since IEnumerable<T> is also covariantly valid, it's fine; there are no inputs of T in an IEnumerable<T>.

    Also, variance only works with generic interfaces and delegates, and the varying types must be reference types. You can't put an ISyncInterface<int> into a List<ISyncInterface<Object>> because int is not a reference type.

    There are many posts on SO about covariance and contravariance; you should also read the Microsoft documentation. It can be a confusing feature. If you're interested in the historical details of how we designed and implemented the feature, see my blog.