Generics are a powerful feature in C# that allows you to write code that can work with different data types without being specific about those types at compile time. This leads to increased code reusability, type safety, and performance. By using generics, you can create classes, interfaces, methods, and delegates that operate on a variety of types while maintaining strong type checking. This lesson will explore the core concepts of generics, how to implement them, and their benefits in building robust and maintainable applications.

Understanding Generics

Generics enable you to define type-safe data structures and algorithms without committing to a specific data type. Type parameters are used as placeholders for the actual types that will be used when the generic type is instantiated or the generic method is called.

Type Parameters

Type parameters are specified within angle brackets <> after the name of the class, interface, method, or delegate. By convention, single uppercase letters are used as type parameter names, with T being the most common. You can use multiple type parameters, separated by commas, such as <T, U>.

Example:

public class GenericClass<T>
{
    public T Data { get; set; }

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

    public T GetData()
    {
        return Data;
    }
}

// Usage
GenericClass<int> intObject = new GenericClass<int>(10);
Console.WriteLine(intObject.GetData()); // Output: 10

GenericClass<string> stringObject = new GenericClass<string>("Hello");
Console.WriteLine(stringObject.GetData()); // Output: Hello

In this example, T is the type parameter. When you create an instance of GenericClass, you specify the actual type to use in place of T. This allows the same class definition to work with different data types.

Benefits of Generics

Generic Classes

A generic class is a class that can operate on different types. You define a generic class by including one or more type parameters in the class definition.

Example:

public class DataStore<T>
{
    private T[] data;
    private int index = 0;

    public DataStore(int size)
    {
        data = new T[size];
    }

    public void Add(T item)
    {
        if (index < data.Length)
        {
            data[index] = item;
            index++;
        }
        else
        {
            Console.WriteLine("Data store is full.");
        }
    }

    public T Get(int i)
    {
        if (i >= 0 && i < index)
        {
            return data[i];
        }
        else
        {
            throw new IndexOutOfRangeException("Index is out of range.");
        }
    }
}

// Usage
DataStore<int> intDataStore = new DataStore<int>(10);
intDataStore.Add(5);
intDataStore.Add(10);
Console.WriteLine(intDataStore.Get(0)); // Output: 5

DataStore<string> stringDataStore = new DataStore<string>(5);
stringDataStore.Add("Apple");
stringDataStore.Add("Banana");
Console.WriteLine(stringDataStore.Get(1)); // Output: Banana

In this example, DataStore<T> is a generic class that can store an array of any type T. The Add and Get methods work with the specified type.

Generic Methods

Generic methods are methods that can operate on different types. You define a generic method by including one or more type parameters in the method signature.

Example: