27

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?

1
  • Now how can you make a Repository<Employee>? You can't. You'd have to use a BusinessObjectRepository and cast to Employee every time you Get from it. And hope you don't get the employees repository mixed up with the inventory repository.
    – user253751
    Dec 29, 2020 at 18:32

3 Answers 3

32

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
4
  • i understand, that abstraction over types is useful. i just don't see its use in "real life". Func<> and Task<> and Action<> are library types. and thanks i remembered about interface IFoo<T> where T : IFoo<T> also. that's obviously real life application. the example is great. but for some reason i not feel satisfied. i rather want to get my mind around when it's appropriate and when it's not. answers here have some contribution to this process, but i'm still feel myself incomfy around this all. it's strange because language-level problems don't bother me for so long already. Jan 23, 2016 at 0:25
  • 1
    Great Example. I felt like I was back in the class room. +1
    – Chef_Code
    Sep 25, 2016 at 23:20
  • 1
    @Chef_Code: I hope that's a compliment :-P Sep 26, 2016 at 0:03
  • Yes, it is. I later thought about how it could be perceived after I had already commented. So to confirm sincerity... Yes it is a compliment nothing else.
    – Chef_Code
    Sep 26, 2016 at 19:08
16

"most likely clients of this Repository will want to get and use objects through IBusinessObject interface".

No, they won't.

Let's consider that the IBusinessObject has the following definition:

public interface IBusinessObject
{
  public int Id { get; }
}

It just define the Id because it is the only shared functionality between all business objects. And you have two actual business objects: Person and Address (since Persons don't have streets and Addresses dont have names you can't constrain both of them to a commom interface with funcionality of both. That would be a terrible design, violating the Interface Segregation Principle, the "I" in SOLID)

public class Person : IBusinessObject
{
  public int Id { get; private set; }
  public string Name { get; private set; }
}

public class Address : IBusinessObject
{
  public int Id { get; private set; }
  public string City { get; private set; }
  public string StreetName { get; private set; }
  public int Number { get; private set; }
}

Now, what happens when you use the generic version of the repository:

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

When you call the Get method on the generic repository the returned object will be strongly typed, allowing you to access all the class members.

Person p = new Repository<Person>().Get(1);
int id = p.Id;
string name = p.Name;

Address a = new Repository<Address>().Get(1);
int id = a.Id;
string cityName = a.City;
int houseNumber = a.Number;

On the other hand, when you use the non-generic Repository:

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

You will only be able to access the members from the IBusinessObject interface:

IBussinesObject p = new Repository().Get(1);
int id = p.Id; //OK
string name = p.Name; //Oooops, you dont have "Name" defined on the IBussinesObject interface.

So, the previous code won't compile because of the following lines:

string name = p.Name;
string cityName = a.City;
int houseNumber = a.Number;

Sure, you can cast the IBussinesObject to the actual class, but you'll lose all the compile time magic that generics allows (leading to InvalidCastExceptions down the road), will suffer from cast overhead unnecessarily... And even if you don't care about compile time checking neither performance (you should), casting after will definitely not give you any benefit over using generics on the first place.

13

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?

No. You're thinking too much about Repository, where it is pretty much the same. But that's not what the generics are there for. They're there for the users.

The key point here isn't that Repository itself is more generic. It's that the users are more specialized- i.e., that Repository<BusinessObject1> and Repository<BusinessObject2> are different types, and furthermore, that if I take a Repository<BusinessObject1>, I know that I will get BusinessObject1 back out from Get.

You cannot offer this strong typing from simple inheritance. Your proposed Repository class does nothing to prevent people confusing repositories for different kinds of business object or guaranteeing the correct kind of business object coming back out.

5
  • Thanks, that makes sense. But is the whole point of using this highly-praised language feature as simple as helping users who have IntelliSense switched off? (I'm exaggerating a bit, but I'm sure you get the point) Nov 22, 2015 at 21:47
  • @zloidooraque: Also IntelliSense does not know which kind of Objects are stored in a repository. But yes, you could do anything without generics if you are willing to use casts instead.
    – gexicide
    Nov 22, 2015 at 22:41
  • @gexicide that's the point: I don't see where I need to use casts if I use common interface. I've never said "use Object". Also I understand why use generics when writing collections (DRY principle). Probably, my initial question should have been something about using generics outside the collections context.. Nov 23, 2015 at 12:08
  • 1
    @zloidooraque: It has nothing to do with the environment. Intellisense cannot tell you if an IBusinessObject is a BusinessObject1 or a BusinessObject2. It can't resolve overloads based on the derived type that it doesn't know. It can't reject code that passes in the wrong type. There's a million bits of the stronger typing that Intellisense can do absolutely nothing about. Better tooling support is a nice benefit but really nothing to do with the core reasons.
    – DeadMG
    Nov 23, 2015 at 13:36
  • @DeadMG and that's my point: intellisense can't do it: use generic, so it could? does it matter? when you get the object by its interface, why downcast it? if you do it, it's bad design, no? and why and what is "resolve overloads"? user must not decide, whether call method or not based on derived type if he delegates the call of right method to system ( which polymorphism is). and this again leads me to a question: are generics useful outside of containers? i'm not arguing with you, i really need to understand that. Dec 13, 2015 at 19:22

Your Answer

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

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