Now that we've seen LINQ in action and discussed generic collections, it's time to explain what all that strange<T> syntax was about. Let's learn about generics!

A generic in C# is a type that uses objects of a different type. Said different type is not specified until an instance of the generic object is created. We can identify a generic type by looking for the syntax <T>, where T is a placeholder that represents the type being used by the generic.

public class MyGeneric<T> { /*...*/ }

//Elsewhere
var myGeneric = new MyGeneric<int>();
var myOtherGeneric = new MyGeneric<OtherClass>();

We have already seen generics in use with LINQ and collections. Here, we will dive deeper into what these types can do for us developers, how to create both generic classes and generic methods, and how to create constraints.

If you take the label off, does it become the generic brand? Photo by Crystal de Passillé-Chabot / Unsplash

The Sample Solution

exceptionnotfound/CSharpInSimpleTerms
Contribute to exceptionnotfound/CSharpInSimpleTerms development by creating an account on GitHub.
Project for this post: 15Generics

Creating a Generic

The most common use for generic types is to create collection classes. For example, let's say we wanted to implement a custom collection class called StackQueue which implements methods for both the Stack class and the Queue class provided by C#.

public class StackQueue<T> { /*...*/ }

T is the common placeholder for a type that is not defined yet.

A Queue has methods for Enqueue() and Dequeue(), and a Stack has methods for Push() and Pop(). Since Dequeue() and Pop() are the same operation (remove the element at the front of the list) our StackQueue class will implement Enqueue(), Push(), and Pop().

public class QueueStack<T>
{
    private List<T> elements = new List<T>();

    //Insert at "back of line" or bottom of list
    public void Enqueue(T item)
    {
        Console.WriteLine("Queueing " + item.ToString());
        elements.Insert(elements.Count, item);
    }

    //Insert at "front of line" or top of list
    public void Push(T item)
    {
        Console.WriteLine("Pushing " + item.ToString());
        elements.Insert(0, item);
    }

    //Remove the element at the top of the list
    public T Pop()
    {
        var element = elements[0];
        Console.WriteLine("Popping " + element.ToString());
        elements.RemoveAt(0);
        return element;
    }
}

For the Enqueue() and Push() methods, the placeholder T is used as the type of a parameter. For the Pop() method, T is used as the return type.

Because StackQueue is a generic class, we can put off specifying the type that it will operate on until an instance of it is created:

var myStackQueue = new StackQueue<int>(); //T is now int

myStackQueue.Enqueue(1);
myStackQueue.Push(2);
myStackQueue.Push(3);
myStackQueue.Enqueue(4); //At this point, the collection is { 3, 2, 1, 4 }

var firstValue = myStackQueue.Pop(); //3
var secondValue = myStackQueue.Pop(); //2

We will get a build error if we attempt to Push() any object which is not of the type specified by our StackQueue instance:

myStackQueue.Push("a string");
Error: Argument 1: Cannot convert from 'string' to 'int'.

Generic Methods

Instead of creating an entire generic class, we can implement a generic method for situations where a generic class might be more than what we need.

public static class SwapMethods
{
    public static void Swap<T>(ref T first, ref T second)
    {
        T temp;
        temp = first;
        first = second;
        second = temp;
    }
}

public static void Test()
{
    int a = 5;
    int b = 3;
    Swap<int>(ref a, ref b);
    Console.WriteLine(a + " " + b); //Output: 3 5
}

Constraints

It is possible to tell the C# compiler to only allow certain types in a generic type; this is called a constraint. For example, we could restrict our StackQueue class to only be usable on reference types:

public class StackQueue<T> where T : class { /*...*/ }

If we then tried to create an instance of StackQueue with a value type we get an error.

var myStackQueue = new StackQueue<int>();
Error: The type 'int' must be a reference type in order to use it as parameter T in the generic type or method 'StackQueue<T>'.

There are many kinds of constraints we can use, and they work for all generic types including generic classes and generic methods. Let's see a few of them.

Specific Class or Interface

We can constrain a generic class to use a specific class:

public class StackQueue<T> where T : OtherClass { /*...*/ }

Due to inheritance, this instance of StackQueue will also accept instances of any classes which, in turn, inherit from OtherClass.

We can also constrain generics to use types which implement a specific interface:

public class StackQueue<T> where T : ISomeOtherInterface { /*...*/ }

Public Parameterless Constructor

We can constrain a generic class to only use types which have a public parameterless constructor.

public class StackQueue<T> where T : new() { /*...*/ }

You may remember from the class basics post that any class which does not implement a custom constructor will automatically have a public parameterless constructor.

Nullables

We can constrain generic types into using either a specific type or a null instance using the nullable syntax:

public class StackQueue<T> where T : class? { /*...*/ } //Will accept 
                                                        //a class or null

Why Use Constraints?

Constraints are about expectations. By creating a constraint, we are creating an expectation on any instance of the generic type that said instance must follow the rules of the constraint.

The primary reason we use constraints is to force compilation errors when we accidentally use a generic type for something other than what it is meant for. That way, we get these errors immediately, as opposed to waiting for some unknown condition at runtime to cause them.

Glossary

  • Generic - a C# type that uses other types. The type being used will be defined when an instance of the generic type is created. Uses the syntax <T>.
  • Constraint - Used to restrict the kinds of types that can be used in a generic type.

Summary

Generics are types in C# which can use other types in their implementation. The type being used is not specified until an instance of the generic type is created. Generic types are often used for collections. We most often create generic classes or generic methods.

We can place constraints on generic types to restrict the kinds of types they can use. Restrictions might include using a specific class or any classes which inherit from it, using types with a public parameterless constructor, or using nullable types. Constraints work on all generic types.

In the next post in this series, we will discuss two advanced types that C# allows us to use: tuples and anonymous types. Check that out here:

C# in Simple Terms - Tuples and Anonymous Types
Two different ways to group values without needing an entire class.

Happy Coding!