Generic Types

Learn about classes that accept type parameters.

Introduction

Consider the following Holder class:

public class Holder
{
    public string[] Items { get; private set; }

    public Holder(int holderSize)
    {
        Items = new string[holderSize];
    }

    public override string ToString()
    {
        string result = "Items inside: ";

        foreach (var item in Items)
        {
            result = result + item + " ";
        }

        return result;
    }
}

It has the Items property, which is string type. The Holder class holds string items. What if we need a similar class that holds integers? The functionality is the same, but the type is different.

We could suggest something like the following:

public class IntHolder
{
    // Now, the Items property holds integers
    public int[] Items { get; private set; }

    public IntHolder(int holderSize)
    {
        Items = new int[holderSize];
    }

    public override string ToString()
    {
        string result = "Items inside: ";

        foreach (var item in Items)
        {
            result = result + item + " ";
        }

        return result;
    }
}

This works. Is this feasible if we want to create such a class for many more types, though? We’d be copying the same code and changing the type of the internal array used to hold the items.

A better approach uses generics.

Generic types

Generics are classes with members whose types are specified during variable declaration and object creation. In other words, instead of having IntHolder or StringHolder, we can have a single class that instantiates as follows:

Holder<int> intHolder = new Holder<int>(); // Holder class holds integers
Holder<string> stringHolder = new Holder<string>(); // Holder class holds strings

With a generic Holder class, the type of the Items property is specified during variable declaration instead of being hard-coded inside the class:

C#
namespace GenericTypes
{
// <T> means the class will accept a type parameter
public class Holder<T>
{
// This type parameter is then injected into code
public T[] Items { get; private set; }
public Holder(int holderSize)
{
Items = new T[holderSize];
}
public override string ToString()
{
string result = "Items inside: ";
foreach (var item in Items)
{
result = result + item + " ";
}
return result;
}
}
}

We declare Items to be a type T array. This placeholder is replaced by a concrete type when we create a Holder<T> object, replacing the T with a type we want to use.

In our example, we create two holders, one of type Holder<string> and one of type Holder<int>. The type that goes inside <> is called a type parameter, and it replaces the placeholder, T:

C#
using System;
namespace GenericTypes
{
class Program
{
static void Main(string[] args)
{
Holder<int> intHolder = new Holder<int>(3);
Holder<string> stringHolder = new Holder<string>(3);
// Let's see the type of the Items array
Console.WriteLine(intHolder.Items.GetType());
Console.WriteLine(stringHolder.Items.GetType());
}
}
}

Default values

Remember that the default value for a numeric value-type variable is zero, while a reference-type variable doesn’t point to anything (it’s null). To assign a default value to a T member, we can use the default operator:

public T Name { get; set; } = null; // Incorrect
public T JobPosition { get; set; } = default(T); // Depending on the actual type that will be used, default() will return a default value for this type

Accept multiple type parameters

A generic class can accept multiple type parameters. We don’t have to limit ourselves to the letter T when choosing a placeholder:

public class SystemBootstrapper<A, B>
{
	public A FirstParameter { get; set; }
	public B SecondParameter { get; set; }
}

Generic methods

Besides generic types, we can create generic methods that accept type parameters:

C#
using System;
namespace GenericTypes
{
class Program
{
static void Main(string[] args)
{
string defaultString = GetDefault<string>(); // Returns null
int defaultInt = GetDefault<int>();
if (defaultString == null)
{
Console.WriteLine("Default string: null");
}
Console.WriteLine($"Default integer: {defaultInt}");
}
// Generic method
private static T GetDefault<T>()
{
return default(T);
}
}
}