;

C# Generics


Generics in C# are a powerful feature that lets you design classes, interfaces, and methods with a placeholder for the type of data they store or operate on. Instead of specifying a specific data type, generics allow you to use a type parameter as a placeholder, which you can replace with any type at runtime. This flexibility promotes code reuse, type safety, and performance.

What Are Generics?

Generics in C# allow you to define classes, interfaces, and methods with a type placeholder. By doing so, you don’t have to commit to a specific data type and can instead substitute any type at runtime. This is useful when creating classes and methods that are meant to work across different data types.

Example

For instance, rather than creating multiple versions of a class for different data types, a single generic class can handle all data types.

Why Use Generics?

Using generics in C# offers several benefits:

  • Type Safety: Generics enforce type safety at compile time, reducing runtime errors by catching issues early.
  • Code Reuse: You can write a single generic class or method that works with any data type, making your code more modular and reusable.
  • Performance: Generics avoid the need for boxing and unboxing operations, which improves performance by minimizing memory usage.

Generic Classes

A generic class allows you to define a class with one or more type parameters. These type parameters act as placeholders that can be replaced with any data type when an instance of the class is created.

Syntax for Generic Classes

The basic syntax for defining a generic class looks like this:

public class GenericClass<T>
{
    private T data;

    public GenericClass(T value)
    {
        data = value;
    }

    public void Display()
    {
        Console.WriteLine($"Stored value: {data}");
    }
}

Example: A Generic Container

Here is an example of a simple generic container class.

public class Container<T>
{
    private T item;

    public Container(T item)
    {
        this.item = item;
    }

    public T GetItem()
    {
        return item;
    }
}

Usage

You can now create Container instances for any data type:

Container<int> intContainer = new Container<int>(42);
Container<string> stringContainer = new Container<string>("Hello, Generics!");

Console.WriteLine(intContainer.GetItem());       // Output: 42
Console.WriteLine(stringContainer.GetItem());    // Output: Hello, Generics!

In this example, Container<int> is used to store an integer, while Container<string> is used for a string. This flexibility is what makes generics so valuable.

Generic Methods

Generic methods work similarly to generic classes, allowing you to define methods with type parameters. Generic methods can be defined within both generic and non-generic classes.

Syntax for Generic Methods

Here’s the basic syntax for a generic method:

public void GenericMethod<T>(T parameter)
{
    Console.WriteLine($"Parameter type: {typeof(T)}, Value: {parameter}");
}

Example: A Method for Swapping Values

Consider this example of a method that swaps two values, regardless of their type:

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

Usage

You can now use this method to swap integers, strings, or any other types:

int x = 5, y = 10;
Utilities.Swap(ref x, ref y);
Console.WriteLine($"x: {x}, y: {y}");  // Output: x: 10, y: 5

string str1 = "Hello", str2 = "World";
Utilities.Swap(ref str1, ref str2);
Console.WriteLine($"str1: {str1}, str2: {str2}");  // Output: str1: World, str2: Hello

Generic Constraints

Generic constraints allow you to limit the types that can be used with a generic class or method. Constraints make generics more flexible by restricting the types to only those that meet specific requirements.

Types of Constraints

  • where T : struct - T must be a value type.
  • where T : class - T must be a reference type.
  • where T : new() - T must have a parameterless constructor.
  • where T : <base class> - T must inherit from a specific base class.
  • where T : <interface> - T must implement a specific interface.

Example: Using Constraints

Here’s a generic class that works only with objects that implement IDisposable.

public class ResourceHandler<T> where T : IDisposable
{
    private T resource;

    public ResourceHandler(T resource)
    {
        this.resource = resource;
    }

    public void DisposeResource()
    {
        resource.Dispose();
    }
}

Real-World Example: A Generic Repository

A common use case for generics in real-world applications is creating a repository pattern in a data-driven application. A generic repository can handle operations for any data type, such as adding, deleting, and retrieving records from a database.

Code Example

Consider a repository class for managing different entities in an application.

public interface IRepository<T>
{
    void Add(T entity);
    void Delete(T entity);
    IEnumerable<T> GetAll();
}

public class Repository<T> : IRepository<T> where T : class
{
    private List<T> entities = new List<T>();

    public void Add(T entity)
    {
        entities.Add(entity);
    }

    public void Delete(T entity)
    {
        entities.Remove(entity);
    }

    public IEnumerable<T> GetAll()
    {
        return entities;
    }
}

Usage

Suppose we have Customer and Order classes. Using the generic repository, we can create repositories for each type.

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Order
{
    public int OrderId { get; set; }
    public string ProductName { get; set; }
}

public class Program
{
    public static void Main()
    {
        Repository<Customer> customerRepo = new Repository<Customer>();
        customerRepo.Add(new Customer { Id = 1, Name = "John Doe" });

        Repository<Order> orderRepo = new Repository<Order>();
        orderRepo.Add(new Order { OrderId = 101, ProductName = "Laptop" });

        foreach (var customer in customerRepo.GetAll())
        {
            Console.WriteLine($"Customer ID: {customer.Id}, Name: {customer.Name}");
        }

        foreach (var order in orderRepo.GetAll())
        {
            Console.WriteLine($"Order ID: {order.OrderId}, Product: {order.ProductName}");
        }
    }
}

Explanation

This generic repository can now manage any type of entity (e.g., Customer, Order) without rewriting separate repository classes for each type.

Key Takeaways

  • Type Safety: Generics provide compile-time type checking, reducing runtime errors.
  • Code Reusability: Generic classes and methods eliminate the need to write redundant code for multiple types.
  • Performance: Generics minimize boxing and unboxing, improving performance.
  • Flexibility with Constraints: Constraints enable control over which types can be used with generics, enhancing the functionality of generic classes and methods.

Summary

Generics in C# allow for the creation of reusable, type-safe, and performance-optimized classes and methods that can work with any data type. With type safety and flexibility, generics help streamline code and support a wide variety of use cases, especially in applications involving collections, repositories, and other data structures. Adopting generics is a best practice for writing cleaner, more efficient, and adaptable code, enabling you to build robust, modular applications that are easy to extend and maintain.