C#  

Generics in C#: Introduction and Usage

Generics in C# are a feature that allows the design of classes, interfaces, and methods where the specification of one or more data types is deferred until the code is declared and instantiated. This provides type safety and performance without requiring the manual duplication of identical logic across data types.

What Are Generics?

Generics allow defining classes, methods, and interfaces with a placeholder type instead of a fixed type. Think of generics as a container that works with any type, while still giving type safety.

Imagine a ShippingBox class used to ship various items.

Without Generics: Defining a separate box class for every item type would require IntBox, StringBox, LaptopBox, and so on. This approach creates redundant code.

With Generics: Generics enable the creation of a single versatile class, ShippingBox<T>, where T serves as a placeholder. When used, the exact content type is specified, such as ShippingBox<Laptop> or ShippingBox<Book>. The compiler enforces that only an item of the specified type can be placed inside, like a book in ShippingBox<Book>, ensuring type safety and eliminating redundancy.

This design improves code reusability and enforces compile-time type checking, preventing errors and making the codebase cleaner and easier to maintain.

Core Concepts of Generics

1. Type Parameters

Type parameters are the placeholders for types. They are typically represented by a single capital letter, most commonly T (for Type). They are declared inside angle brackets (<T>) immediately following the class, interface, or method name.

2. Generic Classes and Interfaces

These are classes or interfaces defined with one or more type parameters.

// The type parameter T is a placeholder for any type
public class Repository<T>
{
    private List<T> _items = new List<T>();

    public void Add(T item)
    {
        _items.Add(item);
    }
    
    // ... other methods that work with T
}

3. Generic Methods

A method can be generic even if it belongs to a non-generic class, or it can be a generic method inside a generic class.

public static void Swap<T>(ref T firstValue, ref T secondValue)
{
    T temp = firstValue;
    firstValue = secondValue;
    secondValue = temp;
}

// Usage example:
string studentName1 = "Alice";
string studentName2 = "Bob";
Swap(ref studentName1, ref studentName2);  // Swaps student names

int studentId1 = 101;
int studentId2 = 202;
Swap(ref studentId1, ref studentId2);  // Swaps student IDs

4. Type Constraints

Type constraints restrict the types that can be substituted for the type parameter T. They enforce rules that allow the generic code to safely call methods or use properties of the placeholder type.

ConstraintDescriptionExample
where T : structThe type argument must be a value type (e.g., int, bool).class Box<T> where T : struct
where T : classThe type argument must be a reference type (e.g., string, any class).class Box<T> where T : class
where T : new()The type argument must have a parameterless constructor.class Factory<T> where T : new()
where T : BaseClassThe type argument must be, or inherit from, BaseClass.class Processor<T> where T : IProcessable

Uses of generics in C#

Type Safety: Generics enforce that only the specified data type can be used with a class or method, preventing runtime errors by catching type mismatches at compile time.

  • Code Reusability: They enable writing a single class or method that can handle multiple types, avoiding code duplication and making maintenance easier.

  • Performance Improvement: Generics eliminate boxing and unboxing for value types, which improves performance by reducing CPU and memory overhead compared to non-generic collections.

  • Better Readability and Maintainability: Using generics makes the purpose of collections or methods clearer (e.g., List<string> vs ArrayList) because the types are explicitly stated.

  • Widely Used in Collections: Many .NET Framework collections such as List<T>, Dictionary<TKey, TValue>, HashSet<T>, and others are implemented as generics for type safety and efficiency.

  • Support for Generic Methods and Interfaces: Besides classes, methods and interfaces can be generic, allowing utility functions and contracts to work generically across types.

  • Constraints for Flexible Control: With constraints (using where), generics can require a type to implement an interface or inherit a class, ensuring the generic code can safely use specific members of that type.

Conclusion

Generics in C# enable the creation of flexible, reusable, and type-safe code components by allowing classes, methods, and interfaces to operate with any data type. They enhance code quality by catching type errors at compile time instead of runtime, reducing bugs and improving reliability. Generics avoid the overhead of boxing and unboxing for value types, leading to better performance and memory efficiency. Widely used in collections like List<T> and Dictionary<TKey, TValue>, generics provide clarity and safety by explicitly defining the type of data they manage.