Generics in C# allow you to define classes, methods, delegates, and interfaces with a placeholder for the type of data they store or use. This enables you to create more flexible and reusable code. Generics provide type safety without the need for boxing or unboxing, which can improve performance.

Key Concepts

  1. Generic Classes: Classes that can operate on any data type.
  2. Generic Methods: Methods that can operate on any data type.
  3. Generic Interfaces: Interfaces that can be implemented by any data type.
  4. Constraints: Restrictions that can be applied to the types that can be used with generics.

Generic Classes

A generic class is defined with a type parameter. This type parameter can be used within the class to define the type of its members.

Example

public class GenericList<T>
{
    private T[] elements;
    private int count = 0;

    public GenericList(int capacity)
    {
        elements = new T[capacity];
    }

    public void Add(T element)
    {
        if (count < elements.Length)
        {
            elements[count] = element;
            count++;
        }
    }

    public T GetElement(int index)
    {
        if (index < count)
        {
            return elements[index];
        }
        throw new IndexOutOfRangeException();
    }
}

Explanation

  • T is the type parameter.
  • elements is an array of type T.
  • The Add method adds an element of type T to the list.
  • The GetElement method returns an element of type T from the list.

Generic Methods

A generic method is defined with a type parameter. This type parameter can be used within the method to define the type of its parameters and return value.

Example

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

Explanation

  • T is the type parameter.
  • The Swap method swaps the values of two variables of type T.

Generic Interfaces

A generic interface is defined with a type parameter. This type parameter can be used within the interface to define the type of its members.

Example

public interface IRepository<T>
{
    void Add(T item);
    T Get(int id);
}

Explanation

  • T is the type parameter.
  • The Add method adds an item of type T to the repository.
  • The Get method returns an item of type T from the repository.

Constraints

Constraints can be applied to type parameters to restrict the types that can be used with generics.

Example

public class GenericList<T> where T : IComparable<T>
{
    private T[] elements;
    private int count = 0;

    public GenericList(int capacity)
    {
        elements = new T[capacity];
    }

    public void Add(T element)
    {
        if (count < elements.Length)
        {
            elements[count] = element;
            count++;
        }
    }

    public T GetElement(int index)
    {
        if (index < count)
        {
            return elements[index];
        }
        throw new IndexOutOfRangeException();
    }
}

Explanation

  • where T : IComparable<T> is a constraint that restricts T to types that implement the IComparable<T> interface.

Practical Exercises

Exercise 1: Create a Generic Stack

Create a generic stack class that supports the following operations:

  • Push: Adds an element to the top of the stack.
  • Pop: Removes and returns the element at the top of the stack.
  • Peek: Returns the element at the top of the stack without removing it.

Solution

public class GenericStack<T>
{
    private T[] elements;
    private int count = 0;

    public GenericStack(int capacity)
    {
        elements = new T[capacity];
    }

    public void Push(T element)
    {
        if (count < elements.Length)
        {
            elements[count] = element;
            count++;
        }
        else
        {
            throw new InvalidOperationException("Stack is full");
        }
    }

    public T Pop()
    {
        if (count > 0)
        {
            count--;
            return elements[count];
        }
        throw new InvalidOperationException("Stack is empty");
    }

    public T Peek()
    {
        if (count > 0)
        {
            return elements[count - 1];
        }
        throw new InvalidOperationException("Stack is empty");
    }
}

Exercise 2: Implement a Generic Repository

Create a generic repository class that supports the following operations:

  • Add: Adds an item to the repository.
  • Get: Returns an item from the repository by its ID.

Solution

public class GenericRepository<T> where T : IEntity
{
    private List<T> items = new List<T>();

    public void Add(T item)
    {
        items.Add(item);
    }

    public T Get(int id)
    {
        return items.FirstOrDefault(item => item.Id == id);
    }
}

public interface IEntity
{
    int Id { get; set; }
}

Common Mistakes and Tips

  • Type Safety: Always ensure that the type parameter constraints are correctly defined to avoid runtime errors.
  • Boxing and Unboxing: Avoid unnecessary boxing and unboxing by using generics instead of non-generic collections.
  • Performance: Generics can improve performance by reducing the need for type casting and boxing/unboxing.

Conclusion

Generics are a powerful feature in C# that allow you to create flexible, reusable, and type-safe code. By understanding and utilizing generic classes, methods, interfaces, and constraints, you can write more efficient and maintainable code. In the next module, we will explore collections in C#, which often make use of generics to provide type-safe data structures.

© Copyright 2024. All rights reserved