How generics work
At the beginning of my programming journey, I had some issues with understanding generics. In this article, I want to dive in on some knowledge that I hope will help you know generics better. So without further ado let’s jump in.
What are generics?
Microsoft documentation defines generics as
“Generics are classes, structures, interfaces, and methods that have placeholders (type parameters) for one or more of the types that they store or use.”
but let’s try and see what that means by simplifying it a little bit.
Generics are classes, structures, interfaces, and methods that have placeholders (type parameters)
Why “T” and why not something else? You could name the placeholder something else like “U” but “T” stands for type parameters and it is easy to identify that this is a generic. More information can be found below in the naming guidelines.
for one or more of the types that they store or use.
How to use generics
We’ve created a generic class, so how to use it? The same way you use List<> where you can add what type of objects your list should contain.
This is the output of the code above, where the type and property value are listed in the console. As we can see, the first generic shows a string type, the second one is an int type, and the third is a List<string> type.
Now that we’ve seen how to use it after we’ve created a generic, and we’ve seen the output, let’s also take a look at what happens at the Low-Level C# after compilation.
As we can see, there are not that many differences from the High-Level C# before compilation, but we can see that the object of our generic is created in generic1, and then assigned to “stringGeneric”.
Why and when to use generics
When you want to maximize code reuse. In other words, we should use generics when we have a code that is general and could be used with multiple types. Examples of this are Lists.
For type safety. The data type is enforced at compile time. As well an easy to understand example would be Lists. If we have a List<string> like in the example above, we can only add string objects, and if we add something else we will get a compile error.
Performance. The following example is just a general one, and I know I’m reusing lists, but it is always easier to explain by comparing the performance between ArrayList and List, where you will know the type added at compile time. Below we can see that when we use List<Generic<string>> it is faster than adding the same object in an ArrayList.
Naming guidelines
Microsoft documentation defines the following guidelines for generics:
Do name generic type parameters with descriptive names, unless a single letter name is completely self explanatory and a descriptive name would not add value.
Consider using T as the type parameter name for types with one single letter type parameter.
Do prefix descriptive type parameter names with "T".
Consider indicating constraints placed on a type parameter in the name of parameter. For example, a parameter constrained to
ISession
may be calledTSession
.
These guidelines look standalone, but usually, when we work with generics, we combine two of them.
What do I mean by that? Well, let’s take a look at the above examples again:
1. Generic<T>
In this case, the part of the definitions I used is as follows:
Doname generic type parameters with descriptive names, unlessa single letter name is completely self explanatory and a descriptive name would not add value.Consider using T as the type parameter name for types with one single letter type parameter.
2. public TOutput? Converter<TInput, TOutput>(TInput from)
In the following one, I’ve used two type parameters, so in this case, I couldn’t use single-letter ones as above, and this one transformed into:
Do name generic type parameters with descriptive names,
unless a single letter name is completely self explanatory and a descriptive name would not add value.Do prefix descriptive type parameter names with "T".
3. Consider TSession
Do name generic type parameters with descriptive names,
unless a single letter name is completely self explanatory and a descriptive name would not add value.Consider indicating constraints placed on a type parameter in the name of parameter. For example, a parameter constrained to
ISession
may be calledTSession
.
The definition above says to consider constraints placed on a type parameter in the name of the parameter. What does this mean exactly? This is the perfect moment to go and talk about constraints on type parameters, where I will also explain what the last guideline means.
Constraints on Type Parameters
As mentioned above, generics can also have constraints. So what are these constraints?
Let’s consider we create a generic class, which does some math calculations for us. We want our type parameter to be a number. This is where constraints come into place. We can constrain what type of parameter we want to have. Let’s take a look.
The generic constraint can be recognized by the word “where”. In this case, the type argument must be, or implement, the specified interface, meaning “INumber”.
Here we have two types of “TNumber”, int, and float. As we can see, we can’t assign a float to an int and neither double to a float.
Until next time, I will leave you with a question that I recommend you explore on your own. Can we assign, in this case, an int to a float?
The code in this article is simplified for demonstration purposes and it was written in .NET7 with C#11.
The entire code used for this article can be found here - Github profile.