Generics might sound like a fancy term, but they’re one of the most powerful and useful features in .NET. In this post, we’ll start with the basics – explaining what generics are and why we use them – and then step by step move into more advanced topics that even experienced developers will appreciate. So, grab a cup of ☕(I don’t drink coffee myself) and let’s dive in!

What Are Generics and Why Should You Care?

Generics allow us to create code templates that work with any data type while still being type-safe. In plain English, generics let you write a class or method once and reuse it for different types without sacrificing the safety and IntelliSense support of strong typing. This means fewer bugs (because the compiler catches type mismatches) and often better performance.

Why were generics introduced? Imagine the early days of .NET (pre-2.0) when we didn’t have generics. Back then, if you wanted a collection that could hold multiple objects (like today’s List<T>), you might use an ArrayList. An ArrayList holds items as object, which means you could put anything in it, but whenever you took an item out, you had to cast it to the expected type. This was error-prone (you might cast to the wrong type and get an exception at runtime) and not very efficient for value types because of boxing (converting a value type like int to an object) and unboxing. Generics solved these problems by allowing the creation of collections (and other structures) that are statically typed. For example, List<int> is a list that only holds integers – if you try to add a string to it, it won’t compile. You get type safety and also avoid the boxing overhead because a List<int> actually stores int values directly internally.

Key benefits of generics:

  • Type Safety: Errors are caught at compile time. If you try to use the wrong type in a generic class/method, the compiler will yell at you before you even run the code.
  • Code Reuse: Write once, use for any type. You don’t need one version of your class for int and another for string – generics handle that automatically.
  • Performance: No need for converting types back and forth (which is what happened with non-generic collections). Generic collections can store the actual data types efficiently, which is faster especially for value types.

Generic Classes

A generic class is like a blueprint that can create objects for any type. You define the class with a type parameter (usually we use <T> by convention, but it can be any valid identifier) and then you can substitute real types for that parameter when you use the class.

For example, the .NET framework provides many generic classes in the System.Collections.Generic namespace. A popular one is List<T>. The List<T> class is defined with a type parameter T, and when you create a list, you specify what T is (like int, string, or a custom class). The list then is strongly-typed to hold only that type. So you might have List<int> for a list of integers, or List<string> for a list of strings, etc. The magic is that List<T> is one class definition that works for any type T.

Let’s look at a simple example by creating our own generic class. We’ll create a generic Box that can hold a value of any type:

class Box<T>
{
    public T Value { get; set; }
    public Box(T value)
    {
        Value = value;
    }
}

Box<int> intBox = new Box<int>(123);
Box<string> strBox = new Box<string>("Hello Generics");

Console.WriteLine(intBox.Value);   // Output: 123
Console.WriteLine(strBox.Value);   // Output: Hello Generics

Generic Methods

Sometimes you don’t need a whole class to be generic – just a single method inside a class (or a standalone static method) can be made generic. A generic method is a method that has its own type parameter(s) in addition to the normal method parameters.

For example, let’s say we want a utility method to swap two variables’ values. We can write a generic method Swap<T> that swaps two variables of type T:

static void Swap<T>(ref T a, ref T b)
{
    T temp = a;
    a = b;
    b = temp;
}

int x = 5;
int y = 10;
Swap<int>(ref x, ref y);
Console.WriteLine($"x = {x}, y = {y}");   // Output: x = 10, y = 5

string first = "foo";
string second = "bar";
Swap<string>(ref first, ref second);
Console.WriteLine($"first = {first}, second = {second}"); // Output: first = bar, second = foo

We declared Swap<T> with <T> after the method name, meaning this method has a type parameter T. We use T for the types of the parameters a and b. When calling Swap, we can specify the type in angle brackets like Swap<int> or Swap<string>. In many cases, you don’t even need to explicitly specify the type because the C# compiler can infer it from the arguments (more on type inference later). For instance, we could just call Swap(ref x, ref y) without <int> and the compiler would know T is int because x and y are ints.

Generic methods are useful for utility functions like this, where the operation is the same for any type. Other examples might be a method to get the first element of a list, to check if two objects are equal, or to convert between types if certain conditions are met. The key is that the logic inside the method doesn’t depend on a specific concrete type.

You can have generic methods inside non-generic classes, and you can also have generic methods inside generic classes (in which case those methods might have their own type parameters in addition to the class’s type parameters).

Generic Interfaces

Just like classes, interfaces in .NET can also be generic. A generic interface defines a contract that includes one or more type parameters. This is extremely useful for creating flexible architectures. Many built-in interfaces in .NET are generic, such as IEnumerable<T>, IComparable<T>, IEquatable<T>, IList<T>, and so on. For example, IEnumerable<T> allows you to iterate over a collection of T (whatever T may be), and IComparable<T> allows an object to compare itself to another object of the same type T.

By making these interfaces generic, .NET ensures type safety. For instance, IComparable<string> has a method CompareTo(string other). This is much safer and clearer than the non-generic version from early .NET where IComparable‘s CompareTo took an object – with that, you could accidentally compare to an object of the wrong type and only find out at runtime. With IComparable<T>, the compiler makes sure you only compare like types (string to string, int to int, etc.).

You can define your own generic interfaces too. Suppose we want to define a simple storage/repository interface that can save and retrieve items of a certain type. We could do something like:

interface IStorage<T>
{
    void Save(T item);
    T Load(int id);
}

Here IStorage<T> is an interface with a type parameter T. It promises two things: a Save method to save an item of type T and a Load method to retrieve an item by an ID (in this case we used an int ID just for example). Any class that implements IStorage<T> will specify what T is (maybe T is Customer or Order or anything) and then implement those methods accordingly.

Generic interfaces often work hand-in-hand with generic classes. For example, you might have a generic class FileStorage<T> that implements IStorage<T> to save/load objects of type T to files, and another class DatabaseStorage<T> that implements IStorage<T> to handle database operations. The code using these classes can remain abstracted to the interface IStorage<T> without worrying about the underlying type T or the storage mechanism.

Generic Constraints (The where Clauses)

As powerful as generics are, sometimes you need to put some limits on the type parameters. For example, maybe your generic method wants to call a method that not every type has. Or you want to ensure a type parameter is a reference type (class) or value type (struct). This is where generic constraints come in, using the where keyword in C#.

A constraint tells the compiler “hey, T must be a kind of X”. If a type argument doesn’t meet the constraint, the code won’t compile. Here are some common constraints and what they mean:

  • where T : class – T must be a reference type (class, string, array, etc.).
  • where T : struct – T must be a value type (like int, bool, DateTime, etc., including nullable value types). Usually used to ensure no nulls.
  • where T : SomeBaseClass – T must be SomeBaseClass or derive from it. This lets you treat T as at least that base class inside your generic code.
  • where T : ISomeInterface – T must implement ISomeInterface. This lets you call interface methods on T within the generic code.
  • where T : new() – T must have a public parameterless constructor. This is often used if you need to create a new instance of T inside the generic class or method (using new T()).
  • You can also combine constraints, for example: where T : class, IComparable<T>, new() means T must be a reference type that implements IComparable<T> and has a parameterless constructor.

Why use constraints? Because they let you use the features of the constrained type safely. For instance, consider writing a generic function to find the larger of two values. You can use a constraint to ensure the types you compare have an ordering.

Let’s use a constraint in a generic method example:

// Generic method to return the larger of two values
// Constraint: T must implement IComparable<T> so we can call CompareTo on it
static T Max<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) >= 0 ? a : b;
}

// Usage:
Console.WriteLine(Max(5, 10));       // Output: 10  (T is inferred as int)
Console.WriteLine(Max("apple", "zoo")); // Output: zoo  (T is inferred as string)

Here Max<T> uses where T : IComparable<T> to make sure whatever type T is, we know how to compare two of them. Inside the method, we safely call a.CompareTo(b) because the constraint guarantees that method exists. We didn’t have to specify <int> or <string> when calling Max – the compiler infers it (since 5 and 10 are ints, and “apple”/”zoo” are strings, it figures out T).

Constraints are also crucial in larger designs. For example, if you create a generic class Repository<T>, you might want to constrain T to be an entity type that has an ID, so you could do where T : IEntity (assuming IEntity is an interface with, say, an Id property). This way you can use T.Id inside your repository code, and the compiler is happy.

Wrapping Up

Generics in .NET are all about creating reusable code without sacrificing type safety or performance. We started with the basics – understanding that generics let you parameterize types (like List<T> or Box<T>) – and saw why that’s useful for both avoiding code duplication and preventing runtime errors. We then explored how you can make not just classes, but also methods and interfaces generic, and how constraints let you add rules to those type parameters.

Now, just because you can make something generic, does not mean you should. Try to use them when it makes sense and not as a silver bullet.

Hope you learned something, thanks!

Affiliate promo

If you love learning new stuff and want to support me, consider buying a course from Dometrain using this link: Browse courses – Dometrain. Thank you!

Leave a comment

Trending