Reputation: 2165
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
Upvotes: 2
Views: 729
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
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