Generics

Generics

The Crash Course

  • You can create your own class that uses generics by placing the generic type in angle brackets after the class definition: public class PracticeList<T> { /* ... */ }.
  • While you're not required, single capital letters for generic types are common, so T is a very common choice for a generic type.
  • You can have multiple generic types: public class PracticeDictionary<K, V> { /* ... */ }.
  • With a generic type defined, you can use that type anywhere throughout your class, such as for fields, method parameter types, and return types. public T GetItem(int index) { /* ... */ }
  • Generic type constraints limit what types can be used with the generic type but let you do more with the type within the class because you know more about it.

Introduction

In the previous tutorial, we saw some examples of generic classes. In this tutorial, we'll make a simple one so that you can see how they're made. We'll create our own version of List<T>, though we'll call it PracticeList<T> to make it clear that this is just for illustration, and we should keep using the "real" List<T> when we program.

Creating A Generic Class

To get the ball rolling, our first step is to add a generic type parameter to a class we're making. This is easy to do:

public class PracticeList<T>
{
}

The generic type parameter goes in angle brackets (< and >) and is given a name of your choosing. Generic type parameter names usually fit one of two patterns: Single capital letters (for example, K and V) or some short name, prefixed with a capital T, such as TKey and TValue. When you only have a single generic type parameter, it is nearly universal to just use T. But the name is arbitrary, and you can use whatever you want.

We now have a generic class!

But we want to make use of T or there's not much point in having it. So our next step is to use it within our class. We can use T within the class anywhere we'd use another type. That includes for field types, local variables, parameter types, and return types. I'm going to pull in the code we made in the previous tutorial to make that ListOfNumbers and just replace the usages of int with T:

public class PracticeList<T>
{
    private T[] _items; // Renamed from `_numbers` to `_items`.
 
    public PracticeList()
    {
        _numbers = new T[0];
    }
 
    public Add(T newItem)
    {
        // Make a new array, slightly larger than the current one.
        T[] newItems = new T[_items.Length + 1];
 
        // Copy everything over to this new array.
        Array.Copy(_items, newItems, _items.Length);
 
        // In the last slot, put the new number.
        newItems[newItems.Length - 1] = newItem;
 
        // Update the array this object is tracking with the new, longer array.
        // The old one will be cleaned up by the garbage collector eventually.
        _items = newItems.
    }
 
    public T GetItem(int index)
    {
        return _items[index];
    }
}

You can see the usages of T scattered throughout the class. We're using it for the type of array in the _items field, for the parameter type in Add, and for the return type of GetItem.

You'll use generic types way more than you'll define your own, but it is good to at least see how they are made so you have a rough idea of how to do it when the time comes.

Generic Constraints

Before we walk away from generic types, let's touch briefly on one final limitation of generic types and how to solve them.

The catch with using a generic type is that it could be anything. But that means we know nothing about the type being used. For a plain old container like our PracticeList<T> (or the real List<T>), that isn't too limiting. All we need to do with our T values is hang on to them. But if we wanted to do anything else with them, we're out of luck. (That's not entirely true. We do know that it must be an object, so we could always call ToString() on something of type T. But we are very limited.)

In the rare cases where we want a generic type but still need to expect something from it, we can provide a generic type constraint. These constraints limit what types can be filled in for T, but because of the limitations, we will know more about the type and be able to do more with them inside the generic class itself.

Generic type constraints are applied by appending them at the top of the class as shown below:

public class PracticeList<T> where T : IFileParser
{ 
   //...
}

The where indicates that there are constraints, the T indicates the type that the constraint is for, and then after the :, we name our constraint. In this case, where T : IFileParser indicates that T can be any type as long as it implements the IFileParser interface.

In exchange, now the compiler can be certain that T has all the capabilities of the IFileParser interface and could be used as one.

Listing a class or interface as a constraint is the most common, but there are other constraints as well. I'm not going to get into the full list here since this C# Crash Course is already quite long, and you won't make that many generic types and even fewer generic types with constraints. But you can constrain it to be only a class, only a struct, have a parameterless constructor, or even establish that two generic type parameters are somehow related to each other. So there's a lot of room to do stuff here when it becomes necessary. But it will probably be a while before you need to do this.

What's Next?

As we've seen in the last two tutorials, generics are very powerful and can greatly reduce the amount of code that we need to write. There are many times that we'll be able to put generics to use, by using generic classes that someone else has already created, like the List and Dictionary classes, but also by making our own generic classes as we have a need for it.

Up next, we'll start to take a look at more intermediate programming topics (going beyond the basics) and talk about the first of sort of a random collection of useful topics: reading from and writing to files.