Grasping Generics
Maybe you’ve seen some odd <T> things in your code, or heard about Generics and want to know what’s going on. Maybe you’ve even used them yourself but don’t really understand the details.
Read on as we take a look at a simple use case, and then expand to get into the details that will really let you grasp generics.
Standardised, but different
Let’s say you have written the following classes that Fetch an object from … somewhere (a database, file, web service, etc.).
public class ClientFetcher {
public Client Fetch() { ... };
}
public class ProjectFetcher{
public Project Fetch() { ... };
}
Your team lead insists that classes should implement an interface. So you start writing an IFetchClients interface and an IFetchProjects interface. This seems pretty pointless and a waste of effort to you, but hey, there must be some reason the lead gets the big bucks, right?
But then you notice: Both of these classes have a Fetch method. Wouldn’t it be cool if you could create one interface that they both implement?
Then if you created any other Fetcher classes, they could implement that same interface and you would have some kind of standardisation going on, wouldn’t that be grand? Plus there would be a host of other benefits to using interfaces.
So rather than IFetchClients and IFetchProjects you decide to create something a little more, well, a little more generic. How about an IFetchThings interface? Hey, you’re a developer not a wordsmith, right (Although seriously don’t name things like this).
Scene 2: Encountering a Problem
Looking at your ClientFetcher you develop the following interface.
public interface IFetchThings {
Client Fetch();
}
This works fine for ClientFetcher as its Fetch method returns a Client, just like the interface expects.
public class ClientFetcher : IFetchThings {
public Client Fetch() { ... }; // implements IFetchThings.Fetch()
}
But it’s not such good news for ProjectFetcher as its Fetch method returns a Project not a Client. Not what the interface expects.
public class ProjectFetcher : IFetchThings {
public Project Fetch() { ... }; // Does not implement IFetchThings.Fetch() as they have differing return types. A Project isn't a Client.
}
A (Poor) Solution
Client and Project are both objects, so perhaps the interface should return an object?
public interface IFetchThings {
object Fetch();
}
But ClientFetcher and ProjectFetcher would need their Fetch methods modified to return objects not Client and Project respectively.
public class ClientFetcher : IFetchThings {
public object Fetch() { ... }
}
public class ProjectFetcher : IFetchThings {
public object Fetch() { ... }
}
Which means whatever calls Fetch would receive an object and need to convert that object back to a Client or Project. This is potentially dangerous as it isn’t type-safe, meaning you could get an object from a ClientFetcher and try to convert it to a Project and the compiler wouldn’t be able to catch this error, it would only occur at run-time.
IFetchThings fetcher = new ClientFetcher();
object fetched = fetcher.Fetch(); // object is really a Client (Boxed as an object)
// let's convert the object to a Client
Client client = (Client)fetched; // Unbox `fetched` into Client (slight performance hit)
// the compiler is happy and it works at run-time
// let's convert the object to a project
Project project = (Project)fetched; // Unbox `fetched` into Project (slight performance hit)
// the compiler can't see a problem with this
// but when it runs you get an InvalidCastException - Whoops
What if we wanted a little more control, a little more safety and a little bit more efficient?
Enter: Generics
Generics are a way of specif type, ithout specifying a type. Wait, let me try that again.
Generics introduce type parameters. These allow you when designing classes and methods, to defer type specification until you actually use them.
Okay, let’s try with an example. Compare the following code with our previous interface.
public interface IFetchThings<T> {
T Fetch();
}
The angle brackets (<T>) mean this interface is a generic interface. The T within the angle brackets is a placeholder for a generic-type parameter (I’m just going to call them generic placeholders from now on).
If effect <T> says, “In this interface we can use T instead of a real type (like a return, variable or or parameter type) for now, and we can tell you what type it is later.”
So within our interface, we can use T, instead of a specificying a type, anywhere in our code where it makes sense. For example as a method parameter type or return type.
To talk about this interface you would say “IFetchThings of type T”
So how do we use this generic interface?
Now our ClientFetcher looks something like this:
public class ClientFetcher : IFetchThings<Client> {
public Client Fetch() { ... }
}
So we saying ClientFetcher implements IFetchThings with it’s T placeholder replaced with the type Client. So wherever the T placeholder is used on IFetchThings it will be replaced with a Client type.
In this case IFetchThings’s Fetch method is transformed from returning T to returning Client.
As ClientFetcher has a Fetch method that returns a Client this matches the interface.
Spoken out loud you would say something like “ClientFetcher implements the interface IFetchThings of type Client”.
Our *ProjectFetcher *class would be as follows:
public class ProjectFetcher : IFetchThings<Project> {
public Project Fetch() { ... }
}
The same thing happens, the T placeholder on IFetchThings is replaced by Project, so ProjectFetcher’s Fetch which returns a Project matches its interface.
Now our two classes are standardised, they both implement the same interface, but return differect types.
Now if we wanted to create a new Fetcher for Documents then it’s easy to follow the same format:
public class DocumentFetcher : IFetchThings<Document> {
public Document Fetch() { ... }
}
Going further with Generics
Legal Placeholder use
The parameter placeholder can be used in several places. For example:
public class simpleExample<T> {
private T myVar;
public T myProp { get; set; }
public T myMethod1() { ... }
public void myMethod2(T arg) { ... }
public T myMethod3(T arg) { ... }
}
Multiple placeholders
You can have multiple generic placeholders and use them as you wish. For example take a look at the c#.net Dictionary (not all methods shown)
public class Dictionary<TKey,TValue> {
public void Add (TKey key, TValue value);
public bool ContainsKey (TKey key);
public bool ContainsValue (TValue value);
}
Generic Parameter Naming
You can call your generic placeholders anything you want, but there are some fairly common conventions.
A single placeholder would usually be called T. Multiple placeholders would usually be called T1, T2 etc. With multiple placeholders that mix argument and return types, the return type would often be called TResult. If multiple placeholders had a Key-Value relationship (e.g. Dictionary) then they would be called TKey and TValue.
When to use a more descriptive name than T or T#? If it makes sense go for it. However, if you find yourself asking ‘What is T3 again?’ then it’s probably time for a descriptive name.
Constraining Generics
Let’s remind ourselves of our generic interface:
public interface IFetchThings<T> {
T Fetch();
}
Currently, anything can be substituted for T. We can add some constraints to limit what can be used.
public interface IFetchThings<T> where T : class {
T Fetch();
}
Now T must be a class, so Client (assuming Client is a class) can still be used, but int wouldn’t be valid.
There are numerous constraints that can be applied:
Constraint | Type argument for T |
---|---|
where T : class | must be a reference type (class, interface, delegate or array) |
where T : struct | must be a value type (int, bool etc) |
where T : new() | must have a public parameterless constructor. When used together with other constraints, the new() constraint must be specified last. |
where T : <base class name> | must be or derive from the specified base class. |
where T : <interface name> | must be or implement the specified interface. Multiple interface constraints can be specified. The constraining interface can also be generic. |
where T : U | must be, or be derived from, type supplied for U |
where T : unmanaged | must not be a reference type and must not contain any reference type members at any level of nesting. |
Multiple constraints
A type parameter can have multiple constraints, and constraints can be generic types, as follows:
public interface IFetchThings<T> where T : Fetched, IFetchable, System.IComparable<T>, new() {
...
}
Inheriting from a Generic type
You can inherit Generic type without defining the generic type
public interface IFetchThings<T> {
T Fetch();
}
public interface ISecureFetchThings<T> : IFetchThings<T> {
T Fetch();
bool isLoggedOn();
void logOn();
}
public class ClientFetcher : ISecureFetchThings<Client> {
...
}
Conclusion
Generics are type-safe. They increase the reusability of code. They stop boxing and unboxing (converting to/from object) and so give a performance advantage. They are used often within the .net framework (e.g. Dictionary)
Next Steps
Use the buttons below to follow the Bold Programmer on social media, so you don’t miss any future posts, and please leave a comment below if you found this article useful, confusing (or found a bug)