kagmole
kagmole

Reputation: 2165

Infer or ignore nested generics in nested C# interfaces

Let's say we have a container IDevice that contains IDeviceTagBag objects, and IDeviceTagBag itself is a container of IDeviceTag objects.

Now I want them to be generic classes, while maintaining the constraints above.

In Java, I would do the following:

public interface IDeviceTag {}

public interface IDeviceTagBag<TDeviceTag extends IDeviceTag> {}

public interface IDevice<TDeviceTagBag extends IDeviceTagBag<?>> {}

Writing a method returning a IDevice (Java solution) now looks like this:

public class DeviceService
{
    // Compiler will infer the following covariant chain:
    // -> ? extends IDeviceTagBag -> ? extends IDeviceTag
    public IDevice<?> Get(string name)
    {
        return null;
    }

    // Or the invariant alternative:
    // -> IDeviceTagBag -> IDeviceTag
    public IDevice<IDeviceTagBag<IDeviceTag>> GetInvariant(string name)
    {
        return null;
    }
}

I tried to achieve the same thing using C# (either invariant or covariant), but I ended up with the below solution that feels like a big boilerplate:

// OK !
public interface IDeviceTag {}

// OK !
public interface IDeviceTagBag<TDeviceTag> where TDeviceTag : IDeviceTag {}

// Ouch, no substitute for wildcard "<?>"
public interface IDevice<TDeviceTagBag, TDeviceTag>
    where TDeviceTagBag : IDeviceTagBag<TDeviceTag>
    where TDeviceTag    : IDeviceTag
{}

Writing a method returning a IDevice (C# solution) looks like this:

public class DeviceService
{
    // So much to replace "<?>"
    public IDevice<IDeviceTagBag<IDeviceTag>, IDeviceTag> Get(string name)
    {
        return null;
    }
}

I actually have a fourth nested generic interface in my application and it gets nasty.

Am I missing something that would simplify that, like in the Java solution with <?>?

After finding those posts...

... I fear there is not much I can do about it.

Or am I over-engineering the thing? After all, if I remove the where constraints, I no longer have this boilerplate issue; but developers may implement IDevice of something else than IDeviceTagBag...


Conclusion from answers (19.10.2019)

Both answers are good, but I can only accept one... so I accept the one that replicates the most my Java solution. :(


Appendix: why I need my interfaces to be generic

I build a library that offers communication capabilities with various device types. A common action for all of them is to receive and send messages. Then, depending on the device type, there is additional capabilities.

Developers using the library may use the common DeviceService to access every devices, whatever the type, but will be limited to the common actions.

If they want to use a specific capability, they may use say SpecificDeviceService, but they will have to update their code if the underlying device type changes.

A SpecificDevice needs to implements IDevice if I want it to be accessible through either DeviceService or SpecificDeviceService:

public interface IDevice
{
    IDeviceTagBag Tags
    {
        get;
    }

    // ...
}

public interface ISpecificDevice : IDevice
{
    // Problem:
    // - "Tags" still return "IDeviceTagBag" here and not "ISpecificDeviceTagBag"
    // - End users will have to do explicit casts
}

To prevent the "problem" said in the comments, a solution is to use generics.

// New problem: "TDeviceTagBag" may be something else than "IDeviceTagBag"
public interface IDevice<TDeviceTagBag>
{
    TDeviceTagBag Tags
    {
        get;
    }

    // ...
}

public interface ISpecificDevice : IDevice<ISpecificDeviceTagBag>
{
    // First problem solved: "Tags" return "ISpecificDeviceTagBag"
}

But then, when constraining the generic types with where conditions to resolve the new problem above, I get the "boilerplate" code explained in my question above because I have a total of 4 layers:

IDeviceService -> IDevice -> IDeviceTagBag -> IDeviceTag

Upvotes: 2

Views: 729

Answers (2)

Alpha75
Alpha75

Reputation: 2280

As I see it, you don't need two generic types for the third interface. You can solve it with a single where:

public interface IDeviceTag { }

public interface IDeviceTagBag<out TDeviceTag>
    where TDeviceTag : IDeviceTag
{ }

public interface IDevice<TDeviceTagBag>
    where TDeviceTagBag : IDeviceTagBag<IDeviceTag>
{ }

Upvotes: 2

canton7
canton7

Reputation: 42225

If my understanding of your problem is correct, the C# way would be:

public interface IDeviceTag {}

public interface IDeviceTagBag {}
public interface IDeviceTagBag<TDeviceTag> : IDeviceTagBag where TDeviceTag : IDeviceTag {}

public interface IDevice<TDeviceTagBag> where TDeviceTagBag : IDeviceTagBag {}

That is, you have to manually split out the parts of IDeviceTagBag which depend on TDeviceTag, and those that don't. Then you expose the parts that don't depend on TDeviceTag on the non-generic interface IDeviceTagBag.

Your Java solution had:

public interface IDevice<TDeviceTagBag extends IDeviceTagBag<?>> {}

In my (somewhat rusty) understanding of Java wildcards, this means that you won't be able to treat any TDeviceTag members as anything other than object, which is roughly equivalent to only being able to access the members of the non-generic IDeviceTagBag.

Upvotes: 1

Related Questions