Working with Generics in C#

What are Generics in C#?

Generics in C# allow developers to define classes, methods, and interfaces with a placeholder for data types. This enables type safety, code reusability, and better performance by avoiding unnecessary type casting.

Key Features of Generics

  • Provides **type safety** by allowing specific data types.
  • Improves **code reusability** by working with multiple data types.
  • Enhances **performance** by reducing boxing and unboxing operations.
  • Works with **generic collections** like List<T>, Dictionary<TKey, TValue>.

Generic Classes in C#

A **generic class** allows the same class to work with different data types.

Example: Defining and Using a Generic Class

using System;

class GenericContainer
{
    private T item;

    public void Add(T value)
    {
        item = value;
    }

    public T Get()
    {
        return item;
    }
}

// Usage
class Program
{
    static void Main()
    {
        GenericContainer intContainer = new GenericContainer();
        intContainer.Add(10);
        Console.WriteLine($"Integer Value: {intContainer.Get()}");

        GenericContainer stringContainer = new GenericContainer();
        stringContainer.Add("Hello Generics");
        Console.WriteLine($"String Value: {stringContainer.Get()}");
    }
}

// Output:
// Integer Value: 10
// String Value: Hello Generics
        

The GenericContainer<T> class allows storing different data types while maintaining type safety.

Generic Methods in C#

A **generic method** allows a method to work with different data types without code duplication.

Example: Implementing a Generic Method

using System;

class Program
{
    static void Print(T value)
    {
        Console.WriteLine($"Value: {value}");
    }

    static void Main()
    {
        Print(100);
        Print("Generics in C#");
        Print(3.14);
    }
}

// Output:
// Value: 100
// Value: Generics in C#
// Value: 3.14
        

The Print<T> method can accept any data type, improving code flexibility.

Generic Interfaces in C#

A **generic interface** defines common functionality that can be implemented for multiple data types.

Example: Implementing a Generic Interface

using System;

interface IRepository
{
    void Save(T item);
}

class UserRepository : IRepository
{
    public void Save(string user)
    {
        Console.WriteLine($"User '{user}' saved to database.");
    }
}

// Usage
class Program
{
    static void Main()
    {
        IRepository userRepo = new UserRepository();
        userRepo.Save("Alice");
    }
}

// Output:
// User 'Alice' saved to database.
        

The IRepository<T> interface allows different classes to implement their own version while maintaining type flexibility.

Generic Constraints

**Constraints** in generics restrict the allowed data types that can be used with a generic class or method.

Example: Using Generic Constraints

using System;

class DataProcessor where T : class
{
    public void Process(T data)
    {
        Console.WriteLine($"Processing: {data}");
    }
}

// Usage
class Program
{
    static void Main()
    {
        DataProcessor processor = new DataProcessor();
        processor.Process("Generic Constraints in C#");

        // DataProcessor invalid = new DataProcessor(); // Error: 'int' is not a reference type
    }
}

// Output:
// Processing: Generic Constraints in C#
        

The constraint where T : class ensures that only reference types can be used.

Best Practices for Using Generics

  • Use generics to **avoid type casting** and improve performance.
  • Apply **generic constraints** to enforce type rules.
  • Prefer **generic collections** (e.g., List<T>) over non-generic collections.
  • Use generics in **interfaces and methods** for reusable, flexible code.
  • Use **multiple type parameters** if needed (e.g., Dictionary<TKey, TValue>).