Generics in .NET Core

Empower Your Code with Type Safety and Reusability

Table of Contents

Introduction

Generics in .NET provide a way to define type-safe collections and classes that can operate on any data type. They enhance code reusability, readability, and performance by allowing you to write algorithms and data structures that work with a variety of types without sacrificing type safety.

Before generics, developers often had to rely on the object type or create separate classes for each data type, leading to potential runtime errors and verbose code. Generics solve these problems by introducing placeholders for types, which are then specified when the generic type or method is used.

Why Use Generics?

Generics offer several key advantages:

Generic Types

A generic type is a class, struct, interface, or delegate that is parameterized by one or more types. These type parameters are specified in angle brackets (<T>).

Consider a simple generic stack implementation:

                
public class GenericStack<T>
{
    private readonly List<T> _items = new List<T>();

    public void Push(T item)
    {
        _items.Add(item);
    }

    public T Pop()
    {
        if (_items.Count == 0)
        {
            throw new InvalidOperationException("Stack is empty.");
        }
        T item = _items[_items.Count - 1];
        _items.RemoveAt(_items.Count - 1);
        return item;
    }

    public int Count => _items.Count;
}
                
            

You can then create instances of this generic type for specific data types:

                
// Generic stack for integers
var intStack = new GenericStack<int>();
intStack.Push(10);
intStack.Push(20);
Console.WriteLine(intStack.Pop()); // Output: 20

// Generic stack for strings
var stringStack = new GenericStack<string>();
stringStack.Push("Hello");
stringStack.Push("World");
Console.WriteLine(stringStack.Pop()); // Output: World
                
            

If you try to push an incompatible type, the compiler will flag it as an error:

                
// intStack.Push("This will cause a compile-time error.");
                
            

Generic Methods

Generic methods are methods that have their own type parameters. They can be defined within generic or non-generic types.

Here's an example of a generic method to swap two values:

                
public static class Utility
{
    public static void Swap<T>(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }
}
                
            

Using the generic swap method:

                
int x = 5;
int y = 10;
Utility.Swap(ref x, ref y); // T is inferred as int
Console.WriteLine($"x: {x}, y: {y}"); // Output: x: 10, y: 5

string s1 = "First";
string s2 = "Second";
Utility.Swap(ref s1, ref s2); // T is inferred as string
Console.WriteLine($"s1: {s1}, s2: {s2}"); // Output: s1: Second, s2: First
                
            

Type Constraints

Type constraints allow you to specify requirements for the types that can be used as arguments for generic type parameters. This enables you to call methods or access members of the constrained type within the generic code.

Common constraints include:

Example with a constraint:

                
public class Calculator<T> where T : struct
{
    public T Add(T a, T b)
    {
        // This requires T to be a type that supports addition.
        // For primitive types, we can use dynamic or reflection,
        // or a more specific constraint if available.
        // For demonstration, assume it's a numeric type.
        // A better approach might use specific numeric generic interfaces if available.
        if (typeof(T) == typeof(int)) return (dynamic)a + (dynamic)b;
        if (typeof(T) == typeof(double)) return (dynamic)a + (dynamic)b;
        throw new NotSupportedException("Type not supported for addition.");
    }
}

var intCalc = new Calculator<int>();
Console.WriteLine(intCalc.Add(5, 3)); // Output: 8

// var stringCalc = new Calculator<string>(); // Compile-time error because string is not a struct
                
            

Using dynamic can bypass compile-time type checking for the operation itself, so use constraints carefully to ensure type safety where possible.

Generic Collections

.NET provides a rich set of generic collection types in the System.Collections.Generic namespace. These are highly recommended over their non-generic counterparts (e.g., ArrayList).

Example using List<T> and Dictionary<TKey, TValue>:

                
// List of strings
var names = new List<string> { "Alice", "Bob", "Charlie" };
names.Add("David");

Console.WriteLine("Names:");
foreach (var name in names)
{
    Console.WriteLine($"- {name}");
}

// Dictionary mapping product IDs to names
var products = new Dictionary<int, string>();
products.Add(101, "Laptop");
products.Add(102, "Mouse");

Console.WriteLine($"\nProduct 101: {products[101]}");
                
            

Covariance and Contravariance

Covariance and contravariance are advanced features of generics that allow for more flexible type substitutions.

Covariance Example:

                
IEnumerable<string> strings = new List<string> { "apple", "banana" };
IEnumerable<object> objects = strings; // Covariance: string is assignable to object

foreach (var obj in objects)
{
    Console.WriteLine(obj);
}
                
            

Contravariance Example:

                
Action<object> printObject = Console.WriteLine;
Action<string> printString = printObject; // Contravariance: object is assignable to string for Action input

printString("This will be printed.");
                
            

Covariance and contravariance are supported for generic interfaces (e.g., IEnumerable<out T>, IComparer<in T>) and delegates. They are not directly supported for generic classes.

Conclusion

Generics are a fundamental feature of C# and the .NET platform. By understanding and utilizing generics, you can write more robust, efficient, and maintainable code. They provide a powerful mechanism for abstracting over types, enabling the creation of flexible and type-safe software components.

Embracing generics, especially with the built-in generic collections, is a key step towards writing modern, high-quality .NET applications.