16

In object-oriented languages that support generic type parameters (also known as class templates, and parametric polymorphism, though of course each name carries different connotations), it is often possible to specify a type constraint on the type parameter, such that it be descended from another type. For example, this is the syntax in C#:

//for classes:
class ExampleClass<T> where T : I1 {

}
//for methods:
S ExampleMethod<S>(S value) where S : I2 {
        ...
}

What are the reasons to use actual interface types over types constrained by those interfaces? For example, what are the reasons for making the method signature I2 ExampleMethod(I2 value)?

5
  • 4
    class templates (C++) are something completely different and far more powerful than measly generics. Even though languages having generics borrowed template syntax for them. Commented Mar 16, 2015 at 16:17
  • Interface methods are indirect calls, whereas type methods can be direct calls. So the latter can be faster than the former, and in the case of ref value type parameters, might actually modify the value type.
    – user541686
    Commented Mar 16, 2015 at 19:33
  • @Deduplicator: Considering that generics are older than templates, I fail to see how generics could have borrowed anything from templates at all, syntax or otherwise. Commented Mar 16, 2015 at 22:49
  • 3
    @JörgWMittag: I suspect that by "object-oriented languages that support generics", Deduplicator might have understood "Java and C#" rather than "ML and Ada". Then the influence from C++ on the former is clear, despite that not all languages having generics or parametric polymorphism borrowed from C++. Commented Mar 17, 2015 at 1:10
  • 2
    @SteveJessop: ML, Ada, Eiffel, Haskell predate C++ templates, Scala, F#, OCaml came after, and none of them share C++'s syntax. (Interestingly, even D, which heavily borrows from C++, especially templates, doesn't share C++'s syntax.) "Java and C#" is a rather narrow view of "languages having generics", I think. Commented Mar 17, 2015 at 7:09

3 Answers 3

22

Using the parametric version gives

  1. More information to the users of the function
  2. Constrains the number of programs you can write (free bug checking)

As a random example, suppose we have a method which calculates the roots of a quadratic equation

int solve(int a, int b, int c) {
  // My 7th grade math teacher is laughing somewhere
}

And then you want it to work on other sorts of number like things besides int. You can write something like

Num solve(Num a, Num b, Num c){
  ...
}

The issue is that this doesn't say what you want it to. It says

Give me any 3 things that are number like (not necessarily in the same way) and I'll give you back some sort of number

We can't do something like int sol = solve(a, b, c) if a, b, and c are ints because we don't know that the method is going to return an int in the end! This leads to some awkward dancing with downcasting and praying if we want to use the solution in a larger expression.

Inside the function, someone might hand us a float, a bigint, and degrees and we'd have to add and multiply them together. We'd like to statically reject this because the operations between these 3 classes is going to be gibberish. Degrees are mod 360 so it won't be the case that a.plus(b) = b.plus(a) and similar hilarities will arise.

If we use the parametric polymorphism with subtyping we can rule all this out because our type actually says what we mean

<T : Num> T solve(T a, T b, T c)

Or in words "If you give me some type which is a number, I can solve equations with those coefficients".

This comes up in a lot of other places as well. Another good source of examples are functions which abstract over some sort of container, ala reverse, sort, map, etc.

2
  • 9
    In summary, the generic version guarantees that all three inputs (and the output) will be the same type of number. Commented Mar 16, 2015 at 16:54
  • However, this falls short when you don't control the type in question (and thus can't add an interface to it). For maximum generality you would have to accept an interface parametrized by the argument type (e.g. Num<int>) as an extra argument. You can always implement the interface for any type through delegation. This is essentially what Haskell's type classes are, except much more tedious to use since you have to explicitly pass around the interface.
    – Doval
    Commented Mar 16, 2015 at 17:00
17

What are the reasons to use actual interface types over types constrained by those interfaces?

Because that's what you need...

IFoo Fn(IFoo x);
T Fn<T>(T x) where T: IFoo;

are two decidedly different signatures. The first takes any type implementing the interface and the only guarantee it makes is that the return value satisfies the interface.

The second takes any type implementing the interface and guarantees that it will return at least that type again (rather than something that satisfies the less restrictive interface).

Sometimes, you want the weaker guarantee. Sometimes you want the stronger one.

4
  • Can you give an example of how you would use the weaker guarantee version?
    – GregRos
    Commented Mar 16, 2015 at 14:43
  • 4
    @GregRos - For example, in some parser code I wrote. I've got a function Or that takes two Parser objects (an abstract base class, but the principle holds) and returns a new Parser (but with a different type). The end user should not know or care what the concrete type is.
    – Telastyn
    Commented Mar 16, 2015 at 14:47
  • In C# I imagine returning a T other than the one that was passed in is nearly impossible (w/o reflection pain) without the new constraint as well making your strong guarantee pretty useless on its own.
    – NtscCobalt
    Commented Mar 16, 2015 at 16:02
  • 1
    @NtscCobalt: It's more useful when you combine both parametric and interface generic programming. E.g. what LINQ does all the time (accepts an IEnumerable<T>, returns another IEnumerable<T> which is e.g. actually an OrderedEnumerable<T>)
    – Ben Voigt
    Commented Mar 16, 2015 at 20:01
2

Use of constrained generics for method parameters can allow a method to very its return type based upon that of the thing passed in. In .NET they can have additional advantages as well. Among them:

  1. A method which accepts a constrained generic as a ref or out parameter may be passed a variable which satisfies the constraint; by contrast, a non-generic method with an interface-type parameter would be limited to accepting variables of that exact interface type.

  2. A method with generic type parameter T can accept generic collections of T. A method that accepts an IList<T> where T:IAnimal will be able to accept a List<SiameseCat>, but a method which wanted an IList<Animal> would not be able to do so.

  3. A constraint can sometimes specify an interface in terms of the generic type, e.g. where T:IComparable<T>.

  4. A structure which implements an interface may be kept as a value type when passed to a method accepting a constrained generic parameter, but must be boxed when passed as an interface type. This can have a huge effect on speed.

  5. A generic parameter can have multiple constraints, while there is no other way to specify a parameter of "some type that implements both IFoo and IBar". Sometimes this can be a double-edged sword, since code which has received a parameter of type IFoo will find it very hard to pass it to such a method expecting a double-constrained generic, even if the instance in question would satisfy all constraints.

If in a particular situation there would be no advantage to using a generic, then simply accept a parameter of the interface type. Use of a generic will force the type system and JITter to do extra work, so if there's no benefit one shouldn't do it. On the other hand, it's very common that at least one of the above advantages will apply.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.