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)
If your interfaces are read-only, you can make your interfaces covariant. See @Alpha75 answer.
If your interfaces are not read-only, you can create non-generic interfaces that are "siblings" to your generic ones as a substitute to the Java
<?>wildcard generic. See @canton7 answer.
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