Why are generic constraints not inherited?

Today’s episode is presented as a dialogue:

Why are generic constraints not inherited?

Members are inherited. Generic constraints are not members of a type.

But generic constraints are inherited on generic methods, right?

That’s true, though what is inherited is the method and the constraint comes along with it. What is a bit odd is: generic constraints on methods are invisibly inherited when overriding, which has always vexed me. My preferred design would have been to require that the constraint be re-stated. That is, when you say:

class B
{
  public virtual void M<T>() where T : struct { }
}
class D : B
{
  public override void M<U>() { }
}

U in D.M<U> is still constrained to struct, but it is actually illegal in C# to re-state that fact redundantly! This is a bit of a misfeature in my opinion; it works against code clarity for the reader. Particularly since the base class might not even be in source code; it might be in a different assembly entirely.

(Note that I’m emphasizing here that there is no requirement that the type parameters be named the same. Similarly there is no requirement that the formal parameters be named the same either, but it is a good idea to do so. We’ll come back to this point in a moment.)

So why then aren’t generic type constraints on type-level type parameters invisibly inherited in the same way?

I’m not following you. Can you give an example?

Sure!

class E<T> where T : struct { } 
// Illegal because U is not known to be struct
class F<U> : E<U> { }

Why is it that in the method overriding example the type constraint is invisibly inherited, but here the type constraint must be re-stated?

Ah, now I see what you’re asking.

These situations are similar but different. In the method overloading situation the substitution of U for T is essentially simply renaming the type parameter. In the subclass, U is a brand new type, and that type is being substituted for T via generic construction. Remember, you could have said class F : E<int> { }, but could not have said public override void M<int>() { }! These are similar-looking substitutions but are actually quite different logically.

OK, then let me re-phrase my question in the form of a feature request. I don’t want constraints to be inherited per se. Rather, in my example I would like all the constraints required to be placed on U to be automatically deduced and invisibly added.

That’s an interesting kind of inference that could be added to C#; were I still on the language design team and had this feature pitched, here are the objections I would raise.

The example given yields a trivial solution; obviously U must have a value type constraint placed on it in order to satisfy its usage in E<U>. So let’s start looking at some non-obvious examples:

class G<T, U> where T : X where U : Y {}
class H<V> : G<V, V> {}

Notice that I haven’t said whether X and Y are classes or interfaces. Should we deduce that V must be constrained to both X and Y? Suppose X is a subclass of Y – we would normally not allow

class H<V> : G<V, V> where V : Tiger where V : Animal {}

but presumably you’d like that to be simplified to

class H<V>: G<V, V> where V : Tiger {}

which adds cost to the feature.

Suppose the compiler can deduce that there is no type that satisfies the inferred constraints — perhaps in this case X and Y are Animal and Fruit — should the compiler be required to produce a warning? An error? Again, this is adding a lot of cost to the feature. Detecting error cases is tricky and writing error messages that the user can understand is also tricky.

But again, we’ve got a scenario here where the deductions are very easy to make, it’s just what to do with them once their made that is in question. What if the deductions themselves are hard to make? Here, let me just whip up some types off the top of my head:

class Banana<T> where T : Coffee<T> {}
class Coffee<U> : Banana<Giraffe<U>> where U : INewspaper<Coffee<U>> {}
class Giraffe<V> : Coffee<Banana<V>> where V : IVehicle<V> {}

Every type has constraints already. Are we now required by the feature to add more constraints? T is required to be Coffee<T>Banana<T> is constructed with type argument Giraffe<U>, so Giraffe<U> is required to be Coffee<U>, but it actually inherits from Coffee<Banana<U>>. Should we then deduce that U is required to be Banana<U>? Wait a minute, is that deduction correct? Or is that deduction only valid if classes are covariant — which they are not in C#? Now I’m confused and we’ve only just started. And that brings up a good question: what if those interfaces are covariant? Or contravariant? How does that change the analysis?

And you see how this goes. Any time you step away from the most trivial cases, the difficulty of the inference just explodes. There are techniques for making inferences like this; they are a lot of work for very little gain.

My final criticism of the feature proposal is that it is insufficiently general. The feature request is to make a certain kind of inference – a type constraint – based on a certain kind of evidence – a type substitution in a generic type found in the list of base types. Well what is so special about that list of base types? Surely I should be able to make this inference based on any substitution, not just substitutions in the base type. If I say

class Frob<T> where T : struct {}
class Blob<U> { Frob<U> frobu; }

then surely I can deduce that U must also be constrained to struct from the field declaration. What’s so special about the base type? And if there’s nothing special about the base type, where does it stop? The logical extension of the feature requires a deep analysis of the whole program, which seems like far too much work for so little savings.

No, the whole thing is too complicated, and the whole thing is putting the cart before the horse. When you construct a generic type, you are required to prove to the compiler that the type you’ve supplied meets the constraints. In the case of generic type parameters that are themselves supplied as type arguments, you provide that proof by constraining them explicitly, not relying on the compiler to make a guess at what you meant.

12 thoughts on “Why are generic constraints not inherited?

  1. I’m really don’t see the problem with H(of V): G(of V,V). A generic type may be constrained in terms of an arbitrary number of other generic type parameters or interface types, specified in any order. It’s true that the compiler would not allow `where V : Giraffe, Animal` [it will squawk on whichever type is listed second, claiming it must be first], but I believe that restriction exists simply because the authors of the language saw no harm to forbidding constraints that were redundant, rather than because of any harm that would come from processing constraints without regard for redundancy. Would allowing such redundancy cause any harm of which I’m unaware?

    I think the fundamental issue is that constraints only grant themselves to inheritance if the derived-class parameters are directly passed to the base class. If they are instead used as parameters to some other class which is then passed to the base, inference doesn’t work. (e.g. given class `Foo(of T,U) where T,U` and `Bar(of T,U) : Foo(of Boz(of T),U)` it may be difficult to define the set of classes for which a `Boz(of T)` is a `U`. Allowing constraints to be inherited in the special (but hardly infrequent) case when types are passed through directly might be helpful, but might also be confusing.

  2. Just googled for ‘C# generic inheritance’ and found a few interesting StackOverflow QAs. Basically, all of them boil down to the fact that C# generics are *not* C++ templates: the former are runtime constructs, whereas the later are compile-time ones. I wonder: if the C# generics *were* compile-time constructs, would that be possible:

    class Base<A> where A : something
    {…}

    class Derived : Base{…} // Illegal now, but suppose the empty angle barckets mean, “whatever was there for the base class”

    class ManyTimesRemoved : OneTimeLessRemoved{…}

    So that, when you say:
    var q = new ManyTimesRemoved();

    the compiler generates *the entire hierarchy* up to Base and then you are good to go?

  3. Pingback: The Morning Brew - Chris Alcock » The Morning Brew #1399

  4. Ahhh Eric,

    That had me scratching my chin thoughtfully for a moment.

    “perhaps in this case X and Y are Animal and Fruit — should the compiler be required to produce a warning?”

    Finally I stumbled on the answer to the implied question: “can a fruit be an animal?”

    The answer is most definitely yes. Proof by existence: The Kiwi.

    It’s best not to over think these things.
    lb

  5. Interesting as always. What came to my mind when reading this was an old discussion I had with a fellow programmer about typedefs in C#. Ok, there is “using a = b;” but that is file local. I’d love to see a feature request discussion like the one above on this topic. Anyone else?

  6. Another great article!

    One type of constraint I would really like to be improved, is the new() constraint. I can’t find any reason (besides it was never considered) why is not generalized for constructors with any kind or amount of arguments, and just limited to default constructors.

    eg: where X : new(Y, int), new (string), new()

  7. // Illegal because U is not known to be struct
    class F<U> : E<U> { }

    Why not simply allow this and refuse to instantiate it if E<U> can’t be instantiated? At the time you’re instantiating F<int> you know you can, and same with F<object> that you can’t.

    • I suspect that the answer is simply because the c# compiler doesn’t have enough information about the implementation of a type to perform this checking at the point of usage. You have to be careful about what information is required to compile what modules, because it’s very easy to end up with a design where modules can’t contain circular references to each other, because each requires information that will only be known after the other is fully compiled.

  8. Pingback: cannot inherit from base class | 我爱源码网

  9. Pingback: Правильное использование ограничений в обобщённых типах - c# generics

Leave a comment