14.4. Generics#

You have been using generics since Chapter 7: List<T>, Dictionary<TKey, TValue>, Stack<T>. Here we look at what <T> actually means and how to write your own generic types and methods.

14.4.1. Why Generics Exist#

Without generics, a List that holds int would be a different class from one that holds string. Generics let one implementation work for any type:

// Non-generic — stores object, loses type safety
var list = new ArrayList();
list.Add(1);
list.Add("oops");  // compiles — bug at runtime

// Generic — type-safe at compile time
var nums = new List<int>();
nums.Add(1);
// nums.Add("oops");  // compile error

14.4.2. Generic Methods#

// A generic method works for any type T
static T[] Repeat<T>(T value, int count)
{
    var arr = new T[count];
    for (int i = 0; i < count; i++) arr[i] = value;
    return arr;
}

int[]    ints    = Repeat(0, 5);       // [0, 0, 0, 0, 0]
string[] strings = Repeat("hi", 3);   // ["hi", "hi", "hi"]

Console.WriteLine(string.Join(", ", ints));     // 0, 0, 0, 0, 0
Console.WriteLine(string.Join(", ", strings));  // hi, hi, hi

14.4.3. Generic Classes#

// A minimal generic pair
class Pair<T1, T2>
{
    public T1 First  { get; }
    public T2 Second { get; }

    public Pair(T1 first, T2 second) { First = first; Second = second; }

    public override string ToString() => $"({First}, {Second})";
}

var p1 = new Pair<string, int>("Alice", 30);
var p2 = new Pair<double, bool>(3.14, true);

Console.WriteLine(p1);  // (Alice, 30)
Console.WriteLine(p2);  // (3.14, True)

14.4.4. Type Constraints#

Use where T : ... to restrict what types T can be:

Constraint

Meaning

where T : class

T must be a reference type

where T : struct

T must be a value type

where T : new()

T must have a parameterless constructor

where T : ISomeInterface

T must implement the interface

where T : BaseClass

T must inherit from BaseClass

// Constraint: T must implement IComparable<T> so we can compare
static T Max<T>(T a, T b) where T : IComparable<T>
    => a.CompareTo(b) >= 0 ? a : b;

Console.WriteLine(Max(3, 7));          // 7
Console.WriteLine(Max("apple", "banana")); // banana

14.4.5. Generic Interfaces and LINQ#

All LINQ extension methods are generic — Where<T>, Select<T, TResult>, etc. You have been calling them without writing the type arguments because C# infers them:

// These two are identical:
nums.Where<int>(x => x > 0);
nums.Where(x => x > 0);  // T inferred as int

Type inference makes generics feel like ordinary method calls at the use site.

14.4.6. Practice#

  1. Write a generic static T First<T>(List<T> list) that returns the first element or throws InvalidOperationException if the list is empty.

  2. Write a generic static void Swap<T>(ref T a, ref T b) and test it with int and string.

  3. Write a generic Stack<T> class backed by a List<T> with Push, Pop, and Peek methods. Add a constraint so T must be non-nullable.

Footnotes