Hinek
Hinek

Reputation: 9729

Set a constraint for generic type, that T inherits from another generic type

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

Answers (1)

Flater
Flater

Reputation: 13773

Second update

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).



Update, as per your update

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.



The old answer

Your setup is logically inconsistent.

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:

1. 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.

2. 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()

3. 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

Related Questions