Reputation: 9729
I want to create a generic class, but one of the where conditions for the type is, that it must extend a specific class which unfortunately is a generic class, too.
Let me explain, say we already have this abstract base class (and can't change them, because it is part of a third party framework):
public abstract class SomeGenericBaseClass<Tx> : IDisposable
where Tx : class
{
public string SomeProperty { get; set; }
}
And there are several decendants of this base class, that implement it with concrete types, like this:
public class SomeConcreteClass : SomeGenericBaseClass<string>
{}
I want to create a class, that can wrap those descendants of SomeGenericBaseClass
:
public class MyClass<Ta> : IDisposable
// some magig where condition, that makes sure, Ta is a descendent of SomeGenericBaseClass
{
private Ta wrappedObject;
public MyClass(Ta objectToWrap)
{
this.wrappedObject = objectToWrap;
}
public DoSomething()
{
this.wrappedObject.SomeProperty = "If I want to use this property, I have to make sure, that Ta/wrappedObject is a descendant of SomeGenericBaseClass, which declares SomeProperty.";
}
}
How do I do this, without the where condition being something like
public class MyClass<Ta, Tb> : IDisposable
where Ta : SomeGenericBaseClass<Tb>, new()
where Tb : class
which would mean that I have to pass two types, every time I use MyClass
var obj = new MyClass<SomeConcreteClass, string>();
although Ta=SomeConcreteClass
allows only one valid type for Tb (string
) ...
Upvotes: 0
Views: 2038
Reputation: 13773
In your updated question, you've provided us with a more concrete usage, where Ta
= SomeConcreteClass
and Tb
= string
.
Here's an updated snippet to show how my answer should be used:
public class SomeGenericBaseClass<Tx> : IDisposable
where Tx : class { }
public class MyClass<Ta> : IDisposable
where Ta : SomeGenericBaseClass<string>, new()
var obj = new MyClass<SomeConcreteClass>();
If this is still not exactly what you want, then you are not explaining it clearly enough (unless someone else manages to answer it. We're talking about a very intricate setup, which means that there are many possibilities here, but their validity hinges on your specific requirements.
My last example here assumes that every MyClass<Ta>
object will have a Ta
which is a SomeGenericBaseClass<string>
.
If, however, MyClass<ASecondConcreteClass>
would be a SomeGenericBaseClass<int>
and that is correct according to your requirement, then I again refer to bullet point 3: if the Tb
is specific to which Ta
is being used, then you obviously have to mention both of them (since it is not implicitly clear which Tb
belongs to which Ta
).
Whoops, I made a typo, when typing the classes into SO. Tx / Tb should only have a class constraint. I edited the question.
public class SomeGenericBaseClass<Tx> : IDisposable
where Tx : class { }
public class MyClass<Ta> : IDisposable
where Ta : SomeGenericBaseClass<Tb>, new()
If this is the correct case, then there's no reason to mention where Tb : class
, because this is already inherently the case because of the already defined where Tx : class
Therefore, you can simply omit the requirement as it is redundant:
public class MyClass<Ta, Tb> : IDisposable
where Ta : SomeGenericBaseClass<Tb>, new()
The compiler will inherently require Tb
to fulfill the requirements of where Tx : class
.
First, you set up your classes like so:
public class SomeGenericBaseClass<Tx> : IDisposable
where Tx : class { }
Here, SomeGenericBaseClass<Tx>
is IDisposable
, but Tx
is nothing more than a class.
But later on, you expect something different:
public class MyClass<Ta> : IDisposable
where Ta : SomeGenericBaseClass<Tb>, new()
where Tb : IDisposable
But here, you expect Tb
(which is your Tx
from the earlier example) itself to be IDisposable
.
Your requirements have shifted. And I'm not sure whether this is by mistake or by intention. I see three options here:
Tx
/Tb
is always required to be IDisposable
This seems to make the most sense, based on my assumption of what you are trying to achieve.
public class SomeGenericBaseClass<Tx>
where Tx : class, IDisposable { }
public class MyClass<Ta> : IDisposable
where Ta : SomeGenericBaseClass<Tb>, new()
This forgoes the need for MyClass<Ta>
to require anything about Tb
(it only needs to mention the type itself of course), because your requirements are already part of the SomeGenericBaseClass<Tx>
definition.
Note: You can also still have SomeGenericBaseClass<Tx>
be IDisposable
too. There's nothing wrong with that; but I've omitted it from my example to prove that it isn't necessary.
Tx
/Tb
is never required to be IDisposable
If this is the case, then you've probably gotten confused between Tx
and SomeGenericBaseClass<Tx>
, but in fact only the latter needs to be IDisposable
.
public class SomeGenericBaseClass<Tx> : IDisposable
where Tx : class { }
public class MyClass<Ta> : IDisposable
where Ta : SomeGenericBaseClass<Tb>, new()
Tb
is sometimes required to be IDisposable
. It should be IDisposable
for MyClass<Ta>
, but not for the base implementation of SomeGenericBaseClass<Tb/Tx>
Notice that in both previous cases cases, I've omitted any type requirements for Tb
in the class definition of MyClass<Ta>
. This is because in both cases, the type requirements were global. Every instance of SomeGenericBaseClass<Tx>
would then behave the same way across your entire application.
It's not impossible that you want to add this requirement only for MyClass<Ta>
.
But if this is what you want, then you cannot avoid having to mention Tb
in your MyClass<Ta>
definition. If the type requirement only fits in the scope of MyClass<Ta>
, then that is where you'll have to put it (and having to mention the type is then an obvious consequence).
public class SomeGenericBaseClass<Tx> : IDisposable
where Tx : class { }
public class MyClass<Ta, Tb> : IDisposable
where Ta : SomeGenericBaseClass<Tb>, new()
where Tb : IDisposable
Notice that in this case, SomeGenericBaseClass<Tx>
is always required to be IDisposable
, but Tx
itself is required to be IDisposable
only for MyClass<A>
.
However, I think this design, while possible, isn't a good approach. At least, that is my opinion because I can't think of a valid use case for this setup. I'd be interested in hearing why you'd want to use it this way, if this is the case.
Upvotes: 1