C# – Generics vs common interface

cgenericsinterfacesobject-oriented

I don't remember when I wrote generic class last time. Every time I think I need it after some thinking I make a conclusion I don't.

The second answer to this question made me to ask for clarification (since i can't comment yet, i made a new question).

So let's take given code as an example of case where one needs generics:

public class Repository<T> where T : class, IBusinessOBject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

It has type constraints: IBusinessObject

My usual way of thought is: the class is constrained to use IBusinessObject, so are the classes that use this Repository are. Repository stores these IBusinessObjects, most likely clients of this Repository will want to get and use objects through IBusinessObject interface. So why not just to

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

Tho, the example is not good, as it is just another collection type and generic collection is classics. In this case type constraint looks odd too.

In fact the example class Repository<T> where T : class, IBusinessbBject looks
pretty much similar to class BusinessObjectRepository to me. Which is the thing generics are made to fix.

The whole point is: are generics good for anything except collections and don't type constraints make generic as specialized, as the use of this type constraint instead of generic type parameter inside the class does?

Best Answer

Let's first talk about pure parametric polymorphism and get into bounded polymorphism later.

Parametric Polymorphism

What does parametric polymorphism mean? Well, it means that a type, or rather type constructor is parameterized by a type. Since the type is passed in as a parameter, you cannot know in advance what it might be. You cannot make any assumptions based on it. Now, if you don't know what it might be, then what is the use? What can you do with it?

Well, you could store and retrieve it, for example. That's the case you already mentioned: collections. In order to store an item in a list or an array, I need to know nothing about the item. The list or array can be completely oblivious to the type.

But what about the Maybe type? If you are not familiar with it, Maybe is a type which maybe has a value and maybe doesn't. Where would you use it? Well, for example, when getting an item out of a dictionary: the fact that an item might not be in the dictionary is not an exceptional situation, so you really shouldn't throw an exception if the item isn't there. Instead, you return an instance of a subtype of Maybe<T>, which has exactly two subtypes: None and Some<T>. int.Parse is another candidate of something that really should return a Maybe<int> instead of throwing an exception or the whole int.TryParse(out bla) dance.

Now, you might argue that Maybe is kinda-sorta like a list which can only have zero or one elements. And thus kinda-sorta a collection.

Then what about Task<T>? It's a type which promises to return a value at some point in the future, but doesn't necessarily have a value right now.

Or what about Func<T, …>? How would you represent the concept of a function from one type to another type if you cannot abstract over types?

Or, more generally: considering that abstraction and reuse are the two fundamental operations of software engineering, why wouldn't you want to be able to abstract over types?

Bounded Polymorphism

So, let's now talk about bounded polymorphism. Bounded polymorphism is basically where parametric polymorphism and subtype polymorphism meet: instead of a type constructor being completely oblivious about its type parameter, you can bind (or constrain) the type to be a subtype of some specified type.

Let's go back to collections. Take a hashtable. We said above that a list doesn't need to know anything about its elements. Well, a hashtable does: it needs to know that it can hash them. (Note: in C#, all objects are hashable, just like all objects can be compared for equality. That's not true for all languages, though, and is sometimes considered to be design mistake even in C#.)

So, you want to constrain your type parameter for the key type in the hashtable to be an instance of IHashable:

class HashTable<K, V> where K : IHashable
{
  Maybe<V> Get(K key);
  bool Add(K key, V value);
}

Imagine if instead you had this:

class HashTable
{
    object Get(IHashable key);
    bool Add(IHashable key, object value);
}

What would you do with a value you get out of there? You can't do anything with it, you only know it's an object. And if you iterate over it, all you get yielded is a pair of something you know is an IHashable (which doesn't help you much because it only has one property Hash) and something you know is an object (which helps you even less).

Or something based on your example:

class Repository<T> where T : ISerializable
{
    T Get(int id);
    void Save(T obj);
    void Delete(T obj);
}

The item needs to be serializable because it is going to be stored on disk. But what if you have this instead:

class Repository
{
    ISerializable Get(int id);
    void Save(ISerializable obj);
    void Delete(ISerializable obj);
}

With the generic case, if you put a BankAccount in, you get a BankAccount back, with methods and properties like Owner, AccountNumber, Balance, Deposit, Withdraw, etc. Something you can work with. Now, the other case? You put in a BankAccount but you get back a Serializable, which has just one property: AsString. What are you going to do with that?

There are also some neat tricks you can do with bounded polymorphism:

F-bounded polymorphism

F-bounded quantification is basically where the type variable appears again in the constraint. This can be useful in some circumstances. E.g. how do you write an ICloneable interface? How do you write a method where the return type is the type of the implementing class? In a language with a MyType feature, that's easy:

interface ICloneable
{
    public this Clone(); // syntax I invented for a MyType feature
}

In a language with bounded polymorphism, you can do something like this instead:

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

class Foo : ICloneable<Foo>
{
    public Foo Clone()
    {
        // …
    }
}

Note that this is not as safe as the MyType version, because there is nothing stopping someone from simply passing the "wrong" class to the type constructor:

class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
    public SomethingTotallyUnrelatedToBar Clone()
    {
        // …
    }
}

Abstract Type Members

As it turns out, if you have abstract type members and subtyping, you can actually get by completely without parametric polymorphism and still do all the same things. Scala is heading in this direction, being the first major language that started out with generics and then trying to remove them, which is exactly the other way around from e.g. Java and C#.

Basically, in Scala, just like you can have fields and properties and methods as members, you can also have types. And just like fields and properties and methods can be left abstract to be implemented in a subclass later, type members can also be left abstract. Let's go back to collections, a simple List, that would look something like this, if it were supported in C#:

class List
{
    T; // syntax I invented for an abstract type member
    T Get(int index) { /* … */ }
    void Add(T obj) { /* … */ }
}

class IntList : List
{
    T = int;
}
// this is equivalent to saying `List<int>` with generics