C# Generics

Writing reusable code that works with any data type

🔧 What are Generics?

Generics allow you to write flexible, reusable code that works with any data type. Instead of creating separate methods for integers, strings, and other types, you write one generic version.


// Generic method that works with any type
public T GetFirst<T>(List<T> items)
{
    return items[0];
}

// Works with integers
int firstNumber = GetFirst(new List<int> { 1, 2, 3 });
// Works with strings
string firstName = GetFirst(new List<string> { "A", "B" });
                                    

Output:

firstNumber = 1

firstName = "A"

Generic Concepts

🎯

Generic Classes

Classes that work with any type

class Box<T>
{ public T Value; }
⚙️

Generic Methods

Methods with type parameters

void Swap<T>(ref T a, 
ref T b)
🔒

Constraints

Limit types that can be used

where T : class
where T : struct
🔄

Type Safety

Compile-time type checking

List<int> numbers;
// Only integers allowed

🔹 Generic Classes

Generic classes in C# use type parameters (like T) to create flexible, type-safe data structures. By defining a class such as Box<T>, you can instantiate it with any data type (int, string, double). This eliminates code duplication for different types while maintaining compile-time type checking. For instance, Box<int> stores 123, Box<string> holds "Hello", and Box<double> contains 45.67, all using the same class definition.

using System;

// Generic class definition
class Box<T>
{
    public T Value { get; set; }
    
    public void Display()
    {
        Console.WriteLine("Box contains: " + Value);
    }
}

class Program
{
    static void Main()
    {
        // Box for integers
        Box<int> intBox = new Box<int>();
        intBox.Value = 123;
        intBox.Display();
        
        // Box for strings
        Box<string> stringBox = new Box<string>();
        stringBox.Value = "Hello";
        stringBox.Display();
        
        // Box for doubles
        Box<double> doubleBox = new Box<double>();
        doubleBox.Value = 45.67;
        doubleBox.Display();
    }
}

Output:

Box contains: 123

Box contains: Hello

Box contains: 45.67

🔹 Generic Methods

Generic methods allow a single method to operate on arguments of various types without sacrificing type safety. Defined with a type parameter (e.g., Swap<T>(ref T x, ref T y)), they can swap integers (x=5, y=10 becomes x=10, y=5), strings, or any other type. They are also used with LINQ-style operations to print sequences like "Alice Bob Charlie" or 1 2 3 4 5. This promotes code reusability and reduces redundancy across different data type implementations.

using System;

class Program
{
    // Generic method to swap two values
    static void Swap<T>(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }
    
    // Generic method to display array
    static void DisplayArray<T>(T[] array)
    {
        foreach (T item in array)
        {
            Console.Write(item + " ");
        }
        Console.WriteLine();
    }
    
    static void Main()
    {
        // Swap integers
        int x = 5, y = 10;
        Console.WriteLine($"Before: x={x}, y={y}");
        Swap(ref x, ref y);
        Console.WriteLine($"After: x={x}, y={y}");
        
        // Display string array
        string[] names = { "Alice", "Bob", "Charlie" };
        DisplayArray(names);
        
        // Display int array
        int[] numbers = { 1, 2, 3, 4, 5 };
        DisplayArray(numbers);
    }
}

Output:

Before: x=5, y=10

After: x=10, y=5

Alice Bob Charlie

1 2 3 4 5

🔹 Generic Constraints

Constraints in generics restrict the types that can be used as arguments, ensuring they possess required capabilities. Using the where clause, you can specify that a type must be a class (where T : class), have a parameterless constructor (where T : new()), or implement an interface. For example, a Repository<T> where T : IEntity guarantees that only entities with a Save method (like saving "Alice") or a Create method (creating "Bob") can be used, enhancing safety and design clarity.

using System;

// Constraint: T must be a class (reference type)
class Repository<T> where T : class
{
    public void Save(T item)
    {
        Console.WriteLine("Saving: " + item);
    }
}

// Constraint: T must have a parameterless constructor
class Factory<T> where T : new()
{
    public T Create()
    {
        return new T();
    }
}

class Person
{
    public string Name { get; set; }
    public override string ToString() => Name;
}

class Program
{
    static void Main()
    {
        // Works with class types
        Repository<Person> repo = new Repository<Person>();
        Person person = new Person { Name = "Alice" };
        repo.Save(person);
        
        // Creates new instance
        Factory<Person> factory = new Factory<Person>();
        Person newPerson = factory.Create();
        newPerson.Name = "Bob";
        Console.WriteLine("Created: " + newPerson);
    }
}

Output:

Saving: Alice

Created: Bob

🔹 Multiple Type Parameters

Generic classes and methods can define multiple type parameters, enabling them to handle several distinct types simultaneously. A class like Pair<TFirst, TSecond> can store combinations such as a string and an integer (First: Alice, Second: 25), an integer and a double (First: 101, Second: 95.5), or two strings (First: Username, Second: john_doe). This provides a strongly-typed way to group heterogeneous data without resorting to non-generic containers like object, improving performance and readability.

using System;

// Generic class with two type parameters
class Pair<TFirst, TSecond>
{
    public TFirst First { get; set; }
    public TSecond Second { get; set; }
    
    public void Display()
    {
        Console.WriteLine($"First: {First}, Second: {Second}");
    }
}

class Program
{
    static void Main()
    {
        // Pair of string and int
        Pair<string, int> nameAge = new Pair<string, int>();
        nameAge.First = "Alice";
        nameAge.Second = 25;
        nameAge.Display();
        
        // Pair of int and double
        Pair<int, double> idScore = new Pair<int, double>();
        idScore.First = 101;
        idScore.Second = 95.5;
        idScore.Display();
        
        // Pair of strings
        Pair<string, string> keyValue = new Pair<string, string>();
        keyValue.First = "Username";
        keyValue.Second = "john_doe";
        keyValue.Display();
    }
}

Output:

First: Alice, Second: 25

First: 101, Second: 95.5

First: Username, Second: john_doe

🔹 Common Generic Constraints

C# provides several constraint types to control what types can be used with your generics:

Constraint Types:

  • 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 : BaseClass - T must inherit from BaseClass
  • where T : IInterface - T must implement IInterface
// Multiple constraints example
class DataStore<T> where T : class, new()
{
    public T CreateNew()
    {
        return new T(); // Can create instance
    }
}

🧠 Test Your Knowledge

What is the main benefit of using generics?