19

Every so often I am making a simple interface more complicated by adding a self-referencing ("reflexive") type parameter constraint to it. For example, I might turn this:

interface ICloneable
{
    ICloneable Clone();
}

class Sheep : ICloneable
{
    ICloneable Clone() { … }
} //^^^^^^^^^^

Sheep dolly = new Sheep().Clone() as Sheep;
                                //^^^^^^^^

into:

interface ICloneable<TImpl> where TImpl : ICloneable<TImpl>
{
    TImpl Clone();
}

class Sheep : ICloneable<Sheep>
{
    Sheep Clone() { … }
} //^^^^^

Sheep dolly = new Sheep().Clone();

Main advantage: An implementing type (such as Sheep) can now refer to itself instead of its base type, reducing the need for type-casting (as demonstrated by the last line of code).

While this is very nice, I've also noticed that these type parameter constraints are not intuitive and have the tendency to become really difficult to comprehend in more complex scenarios.*)

Question: Does anyone know of another C# code pattern that achieves the same effect or something similar, but in an easier-to-grasp fashion?


*) This code pattern can be unintuitive and hard to understand e.g. in these ways:

  • The declaration X<T> where T : X<T> appears to be recursive, and one might wonder why the compiler doesn't get stuck in an infinite loop, reasoning, "If T is an X<T>, then X<T> is really an X<X<…<T>…>>." (But constraints obviously don't get resolved like that.)

  • For implementers, it might not be obvious what type should be specified in place of TImpl. (The constraint will eventually take care of that.)

  • Once you add more type parameters and subtyping relationships between various generic interfaces to the mix, things get unmanageable fairly quickly.

stakx - no longer contributing
  • 77,057
  • 17
  • 151
  • 248
  • 3
    You'll be glad to know that this is common enough to have a name: It's called 'The curiously recurring template pattern' (CRTP for short). – Cameron Jan 14 '12 at 23:06
  • 1
    ... and it has nothing to do with with constraints (as standard C++ templates don't have them at all). – Krizz Jan 14 '12 at 23:09
  • Is there a reason it's not just `interface ICloneable { T Clone(); }`? –  Jan 14 '12 at 23:12
  • 2
    @pst, yes, because without the constraint, one could implement `Sheep` as `class Sheep : ICloneable { public Dog Clone() {…} }`, which might not be what one originally had in mind with the `ICloneable` interface. – stakx - no longer contributing Jan 14 '12 at 23:14
  • @pst yes, otherwise it will allow `class Cow : ICloneable`. – Krizz Jan 14 '12 at 23:15
  • 1
    @stakx At that point I'd just run around waving my hands wildly in the air :) –  Jan 14 '12 at 23:15
  • 2
    a related question last week http://stackoverflow.com/questions/8766365/why-cant-the-meaning-of-a-base-class-specification-recursively-depend-on-itself – Tim Schmelter Jan 14 '12 at 23:19
  • 1
    @pst, turns out you were right! `ICloneable` is sufficient. As the answers below show, the additional constraint would only ensure that the type argument implements `ICloneable<>` for *some type*, but not necessarily for the *same* type. – stakx - no longer contributing Jan 15 '12 at 10:03

2 Answers2

19

Main advantage: An implementing type can now refer to itself instead of its base type, reducing the need for type-casting

Though it might seem like by the type constraint referring to itself it forces the implementing type to do the same, that's actually not what it does. People use this pattern to try to express patterns of the form "an override of this method must return the type of the overriding class", but that's not actually the constraint expressed or enforced by the type system. I give an example here:

http://blogs.msdn.com/b/ericlippert/archive/2011/02/03/curiouser-and-curiouser.aspx

While this is very nice, I've also noticed that these type parameter constraints are not intuitive and have the tendency to become really difficult to comprehend in more complex scenarios

Yep. I try to avoid this pattern. It's hard to reason about.

Does anyone know of another C# code pattern that achieves the same effect or something similar, but in an easier-to-grasp fashion?

Not in C#, no. You might consider looking at the Haskell type system if this sort of thing interests you; Haskell's "higher types" can represent those sorts of type patterns.

The declaration X<T> where T : X<T> appears to be recursive, and one might wonder why the compiler doesn't get stuck in an infinite loop, reasoning, "If T is an X<T>, then X<T> is really an X<X<…<T>…>>."

The compiler does not ever get into infinite loops when reasoning about such simple relationships. However, nominal subtyping of generic types with contravariance is in general undeciable. There are ways to force the compiler into infinite regresses, and the C# compiler does not detect these and prevent them before embarking on the infinite journey. (Yet. I am hoping to add detection for this in the Roslyn compiler but we'll see.)

See my article on the subject if this interests you. You'll want to read the linked-to paper as well.

http://blogs.msdn.com/b/ericlippert/archive/2008/05/07/covariance-and-contravariance-part-twelve-to-infinity-but-not-beyond.aspx

Eric Lippert
  • 612,321
  • 166
  • 1,175
  • 2,033
  • Thanks for the detailed answer. Your blog post hits the nail on the head. +1 also for pointing out that the C# compiler is smarter than the Borg collective when it comes to apparently infinite thingies. :) – stakx - no longer contributing Jan 15 '12 at 10:13
6

Unfortunately, there isn't a way to fully prevent this, and a generic ICloneable<T> with no type constraints is enough. Your constraint only limits possible parameters to classes which themselves implement it, which doesn't mean they are the ones currently being implemented.

In other words, if a Cow implements ICloneable<Cow>, you will still easily make Sheep implement ICloneable<Cow>.

I would simply use ICloneable<T> without constraints for two reasons:

  1. I seriously doubt you will ever make a mistake of using a wrong type parameter.

  2. Interfaces are meant to be contracts for other parts of code, not to be used to code on autopilot. If a part of a code expects ICloneable<Cow> and you pass a Sheep which can do that, it seems perfectly valid from that point.

Groo
  • 45,930
  • 15
  • 109
  • 179
  • 1
    +1 "[`interface X where T:X{}`] only limits possible parameters to classes which themselves implement it, which doesn't mean they are the ones currently being implemented." Very succinct, outstanding! – Glenn Slayden Jul 27 '12 at 07:02