Well, I've been writing C# for 17 years, and I've came across it twice, so it is a very niche solution for certain circumstances. I can't say it's the only solution, but in those instances it was the one that offered the best type safety and allowed classes to have non-generic interfaces.
Say that we have some kind of datastorage where everything is an IEntity, and we need to be able to treat them as such. Them we have a specific entity IApple : IEntity. Every IEntity has IVersion as a Version property, that is passed in it's implementation's construction. IApple has a more specific IAppleVersion as it's Version Property. When we're working with an IApple instance we want to be able to have access to the version as IAppleVersion. But other classes handles only IEntities with IVersions. In this case it makes sense to have the Version as a generic parameter to the class:
Apple : Entity<IAppleVersion>
which makes sure its constructor is:
Apple(IAppleVersion version).
We need to put the stuff that's shared between all entities in the non-generic Entity class, add the Typing of version in a generic version of Entity so Apple can expose the type provided. The Apple class that implements IApple has specific methods for its type. Both entity classes are abstract so they will never be initiated.
I'm not sure that's clear but I hope you can see the problem that I'm trying to solve. I think it's the disconnect with wanting to be able to access both more specific and more general interfaces (and their properties) that causes trouble.
On the flip side, if we have a legitimate need for both a more specific and a more general version of an class/interface, can you tell me a specific reason the way I showed would be bad. It's easy to get hung up "new" is bad because it's 95% true, but as with everything there are exceptions.
But if you never have the need for it, don't use it. I stand by my opinion though that you should be careful and in some situations be able to provide non-generic interfaces rather than requiring each method and class in a library to be generic, but that's only my opinion man :)
> In this case it makes sense to have the Version as a generic parameter to the class
No it really doesn't. It makes sense to use inheritance properly:
public interface IVersion
{
string GetVersion();
}
public class Version : IVersion
{
string version;
public Version(string version)
{
this.version = version;
}
public string GetVersion() =>
version;
}
public class AppleVersion : IVersion
{
string version;
public AppleVersion(string version)
{
this.version = version;
}
public string GetVersion() =>
"apple-" + version;
}
public abstract class Entity
{
public IVersion Version { get; private set; }
public Entity(IVersion version) =>
Version = version;
}
public abstract class Apple : Entity
{
public readonly AppleVersion AppleVersion;
public Apple(AppleVersion version) : base(version)
{
AppleVersion = version;
}
}
That allows for "When we're working with an IApple instance we want to be able to have access to the version as IAppleVersion", as well as general access to the IVersion from an Entity.
If you don't like the storage overhead, you could just downcast knowing that the Version is an AppleVersion:
public abstract class Apple : Entity
{
public AppleVersion AppleVersion => Version as AppleVersion;
public Apple(AppleVersion version) : base(version)
{
}
}
> I'm not sure that's clear but I hope you can see the problem that I'm trying to solve. I think it's the disconnect with wanting to be able to access both more specific and more general interfaces (and their properties) that causes trouble.
You're describing issues with inheritance, not generics.
> I stand by my opinion though that you should be careful and in some situations be able to provide non-generic interfaces rather than requiring each method and class in a library to be generic, but that's only my opinion man :)
That's fine, you're entitled to your opinion, and if it works for you that's great. But your original comment was very 'matter of fact'. I've not seen anything that has communicated to me an issue with generics; and every example so far should have been implemented differently to the way you state (in my opinion).
a) You now have an AppleVersion Property as well as Version Property, that feels potentially confusing for any user of that class. I now have to find the differently named Version property every time I use an entity?
b) You have to manually write that exact boilerplate code on each specific case of Entity, and there might be a lot more if it isn't a simple example. In my case you get it for free through the generics.
The automatic casting doesn't strike me as a problem, eliminates boilerplate code and the where statement ensures type safety. I've yet to see a single concrete argument against my approach.
I didn't mean it as my way or the highway, I merely wanted to issue a warning on creeping dependencies that generics might cause if you don't have non-generic interfaces, the rest I guess is a matter of taste.
> a) You now have an AppleVersion Property as well as Version Property, that feels potentially confusing for any user of that class. I now have to find the differently named Version property every time I use an entity?
It's a specialised type, so it has specialised behaviour. That's the point of inheritance. You can't put the specialised behaviour in the base because then it's not a specialised type and doesn't understand the context. This is an argument against inheritance.
> b) You have to manually write that exact boilerplate code on each specific case of Entity, and there might be a lot more if it isn't a simple example. In my case you get it for free through the generics.
Yes, every type that has specialised behaviour needs to implement it. And no, you don't get it for free in your version, because you can't have a collection of entities.
IEnumerable<Entity<Version>>
Or,
IEnumerable<Entity<AppleVersion>>
All of your complaints about generics appear to be because you're using them to add specialisation to non-specialised types. That will always cause headaches, and that's why, I assume, you have the opinion you do on generics.
Say that we have some kind of datastorage where everything is an IEntity, and we need to be able to treat them as such. Them we have a specific entity IApple : IEntity. Every IEntity has IVersion as a Version property, that is passed in it's implementation's construction. IApple has a more specific IAppleVersion as it's Version Property. When we're working with an IApple instance we want to be able to have access to the version as IAppleVersion. But other classes handles only IEntities with IVersions. In this case it makes sense to have the Version as a generic parameter to the class:
Apple : Entity<IAppleVersion>
which makes sure its constructor is:
Apple(IAppleVersion version).
We need to put the stuff that's shared between all entities in the non-generic Entity class, add the Typing of version in a generic version of Entity so Apple can expose the type provided. The Apple class that implements IApple has specific methods for its type. Both entity classes are abstract so they will never be initiated.
I'm not sure that's clear but I hope you can see the problem that I'm trying to solve. I think it's the disconnect with wanting to be able to access both more specific and more general interfaces (and their properties) that causes trouble.
On the flip side, if we have a legitimate need for both a more specific and a more general version of an class/interface, can you tell me a specific reason the way I showed would be bad. It's easy to get hung up "new" is bad because it's 95% true, but as with everything there are exceptions.
But if you never have the need for it, don't use it. I stand by my opinion though that you should be careful and in some situations be able to provide non-generic interfaces rather than requiring each method and class in a library to be generic, but that's only my opinion man :)