Reputation: 3214
So I am working with .NET Core, and I am working out some data contracts that need to be expandable, to this end, I intend to use parametric polymorphism aka generics.
Now, My sample interfaces and classes here look like so:
public interface IEmailAttachment<TEmail, TAttachment> where TEmail : IEmail<TAttachment> where TAttachment : IEmailAttachment<IEmail<TAttachment>, TAttachment>
{
...
TEmail Email { get; set; }
...
}
public interface IEmail<TAttachment> where TAttachment : IEmailAttachment<IEmail<TAttachment>, TAttachment>
{
...
IEnumerable<TAttachment> Attachments { get; set; }
...
}
public class DefaultEmail : IEmail<DefaultEmailAttachment>
{
...
public IEnumerable<DefaultEmailAttachment> Attachments { get; set; }
...
}
public class DefaultEmailAttachment : IEmailAttachment<DefaultEmail, DefaultEmailAttachment>
{
...
public DefaultEmail Email { get; set; }
...
}
However, this does not work. I get this error:
For the DefaultEmail class:
The type 'DefaultEmailAttachment' cannot be used as type parameter 'TAttachment' in the generic type or method 'IEmail'. There is no implicit reference conversion from 'DefaultEmailAttachment' to 'IEmailAttachment<IEmail, DefaultEmailAttachment>'.
For the DefaultEmailAttachment:
The type 'DefaultEmailAttachment' cannot be used as type parameter 'TAttachment' in the generic type or method 'IEmailAttachment<TEmail, TAttachment>'. There is no implicit reference conversion from 'DefaultEmailAttachment' to 'IEmailAttachment<IEmail, DefaultEmailAttachment>'.
I realise it is probably because I am assuming a bit much, and likely cycling with the type parameters, but if I am missing something obvious that would make this work, please inform me, or suggest another idea of how this could work.
Upvotes: 0
Views: 153
Reputation: 8691
Olivier raises some good points. I agree with all his sentiments, and would not recomment this approach particularly because of how difficult it is to understand the constraints. However, yes, it is possible to implement. It becomes a lot easier to understand if both classes have the same parameters and the same constraints:
public interface IEmailAttachment<TEmail, TAttachment> where TEmail : IEmail<TEmail, TAttachment> where TAttachment : IEmailAttachment<TEmail, TAttachment>
{
TEmail Email { get; set; }
}
public interface IEmail<TEmail, TAttachment> where TEmail : IEmail<TEmail, TAttachment> where TAttachment : IEmailAttachment<TEmail, TAttachment>
{
IEnumerable<TAttachment> Attachments { get; set; }
}
public class DefaultEmail : IEmail<DefaultEmail, DefaultEmailAttachment>
{
public IEnumerable<DefaultEmailAttachment> Attachments { get; set; }
}
public class DefaultEmailAttachment : IEmailAttachment<DefaultEmail, DefaultEmailAttachment>
{
public DefaultEmail Email { get; set; }
}
The error you are currently seeing is because Foo<Derived>
is not assignable to Foo<Base>
by default. IEmailAttachment<DefaultEmail, DefaultEmailAttachment>
is not the same as IEmailAttachment<IEmail<DefaultEmailAttachment>, DefaultEmailAttachment>
. This problem could possibly be solved with covariance, but it would restrict you to getters only instead of get/set.
Upvotes: 1
Reputation: 112682
While this sort of contracts seem to be a good idea in the beginning, you inevitably run into a cascade of (often insovable) problems. Also, this construct is very inflexible. How are you going to add attachments of different types to an email or different types of mails to an email list?
And how are you going to create emails for different types of attachments dynamically?
While being flexible at design-time, generics are not dynamic. This means that the concrete types must be known at compile time.
The fundamental questions are:
There is also a way in between. Often a way out to this sort of problem is to have a non-generic base interface (or class) and derived generic types:
public interface IAttachment
{
IEmail Email { get; set; } // Only reference the non-generic base interface.
}
public interface IEmail
{
// Only members not requiring a generic type parameter.
}
public interface IEmail<TAttachment> where TAttachment : IAttachment
{
ICollection<TAttachment> Attachments { get; set; }
}
Now you can have a list of different types of email
var mails = new List<IEmail>();
mails.Add(new EMail<SpecificAttatchment>());
mails.Add(new EMail<DefaultAttatchment>()); // or
mails.Add(new DefaultEmail());
This is slightly more flexible, but you still have the problem of accessing the attachments of the different IEmail<T>
types at runtime. I don't know what type of problem you are trying to solve with this recursive parametric polymorphism, but I would go with non-generic IAttachment
and IEmail
interfaces alone. If generic type parameters are still required, try to avoid recursions.
Upvotes: 1