MSDN Documentation

Introduction to Generics

Generics introduce the concept of type parameters to .NET, allowing you to design classes, interfaces, and methods that operate on elements of a specified type. This enables you to reuse code more effectively, create more robust and type-safe collections, and reduce the need for casting.

Before generics, collections like ArrayList stored elements as object. This required explicit casting when retrieving elements, which could lead to runtime errors if the cast was incorrect. Generics solve this by allowing you to define collections that hold a specific type, like List<int> or List<string>.

Without Generics (Type Safety Issues):

System.Collections.ArrayList myList = new System.Collections.ArrayList();
myList.Add(10);
myList.Add("hello"); // No compile-time error, but a runtime error awaits!
int firstItem = (int)myList[0]; // Works
// string secondItem = (string)myList[1]; // This would throw an InvalidCastException at runtime if myList[1] was an int.

Benefits of Generics

  • Type Safety: Generics provide compile-time type checking. If you try to add an incompatible type to a generic collection, you'll get a compile-time error, preventing runtime issues.
  • Code Reusability: You can write a single generic class or method that can be used with any data type, eliminating the need to write multiple versions of the same logic for different types.
  • Performance: Generic collections avoid boxing and unboxing of value types, leading to improved performance compared to non-generic collections that store elements as object.
  • Readability: Code becomes cleaner and more expressive when the intent of data types is explicitly stated through generics.

Generic Type Syntax

Generic types are declared using angle brackets (<>) to specify one or more type parameters. The type parameter is a placeholder for an actual type that will be supplied when the generic type is instantiated.

For example, the generic `List<T>` class in C# uses `T` as its type parameter. When you create a list of integers, you instantiate it as List<int>, where int replaces the T placeholder.

Key elements:

  • Type Parameter: Usually represented by a single uppercase letter, commonly `T`.
  • Generic Type Declaration: `ClassName<T1, T2, ...>`
  • Generic Type Instantiation: `new ClassName<ActualType1, ActualType2, ...>()`

Generic Classes

A generic class is a class that is parameterized by one or more types. This allows you to define a class structure that can work with any data type without sacrificing type safety.

Consider a generic `Box<T>` class that can hold any type of item:

public class Box<T>
{
    private T _content;

    public Box(T content)
    {
        _content = content;
    }

    public T GetContent()
    {
        return _content;
    }

    public void SetContent(T newContent)
    {
        _content = newContent;
    }
}

Using the generic `Box` class:

Box<int> intBox = new Box<int>(123);
int value = intBox.GetContent(); // value is 123

Box<string> stringBox = new Box<string>("Hello Generics");
string message = stringBox.GetContent(); // message is "Hello Generics"

// intBox.SetContent("This is wrong!"); // Compile-time error: Cannot convert from string to int.

Generic Interfaces

Generic interfaces define a contract that classes can implement. Similar to generic classes, they use type parameters to specify the types they operate on.

The .NET Framework provides many generic interfaces, such as IEnumerable<T>, ICollection<T>, and IDictionary<TKey, TValue>.

Example of a custom generic interface:

public interface IRepository<T> where T : class
{
    T GetById(int id);
    void Add(T entity);
    void Update(T entity);
    void Delete(int id);
}

An implementation of this interface:

public class UserRepository : IRepository<User>
{
    // ... implementation details ...
    public User GetById(int id) { /* ... */ return null; }
    public void Add(User user) { /* ... */ }
    public void Update(User user) { /* ... */ }
    public void Delete(int id) { /* ... */ }
}

Generic Methods

Generic methods are methods that are parameterized by one or more types. They can be defined within non-generic classes or generic classes.

This allows you to create standalone methods that can operate on different types without requiring the entire class to be generic.

Example of a generic static method to swap two values:

public static class Swapper
{
    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, y = 10;
Swapper.Swap(ref x, ref y); // x is now 10, y is now 5

string s1 = "hello", s2 = "world";
Swapper.Swap(ref s1, ref s2); // s1 is now "world", s2 is now "hello"

Constraints on Generic Types

Constraints are used to enforce rules on the types that can be used as type arguments for a generic type parameter. They limit the kinds of types that a client can supply.

Common constraints include:

  • where T : struct: `T` must be a value type (excluding nullable value types).
  • where T : class: `T` must be a reference type.
  • where T : new(): `T` must have a public, parameterless constructor.
  • where T : BaseClassName: `T` must be or derive from `BaseClassName`.
  • where T : InterfaceName: `T` must implement `InterfaceName`.

A generic class with constraints:

public class DataProcessor<T> where T : class, new()
{
    public void Process(T data)
    {
        // T must be a reference type and have a parameterless constructor
        T defaultInstance = new T();
        // ... process data ...
    }
}

In this example, `DataProcessor<string>` is valid, but `DataProcessor<int>` would result in a compile-time error because `int` is a value type and does not satisfy the `class` constraint.

Covariance and Contravariance

Covariance and contravariance are advanced concepts in generics that deal with how generic types can be substituted for one another.

  • Covariance: Allows you to use a more derived type than originally specified. It applies to output positions (e.g., return types of methods, read-only properties). Marked with the out keyword.
  • Contravariance: Allows you to use a less derived type than originally specified. It applies to input positions (e.g., method parameters). Marked with the in keyword.

Covariance Example:

If you have a generic interface `IEnumerable<out T>`, you can assign an `IEnumerable<string>` to an `IEnumerable<object>` because `string` is derived from `object`.

IEnumerable<string> strings = new List<string>() { "a", "b" };
IEnumerable<object> objects = strings; // Covariance allows this.

Contravariance Example:

If you have a generic delegate `Action<in T>`, you can assign an `Action<object>` to an `Action<string>` because a method that accepts `object` can also accept a `string`.

Action<object> printObject = Console.WriteLine;
Action<string> printString = printObject; // Contravariance allows this.

Practical Uses

Generics are fundamental to modern C# development. Some common practical uses include:

  • Collections: List<T>, Dictionary<TKey, TValue>, HashSet<T>, Queue<T>, Stack<T> are the go-to for efficient and type-safe data storage.
  • Asynchronous Programming: Task<TResult> represents an asynchronous operation that returns a result of type `TResult`.
  • LINQ: Language Integrated Query heavily relies on generics, particularly IEnumerable<T> and IQueryable<T>.
  • Dependency Injection: Generic interfaces are often used to register and resolve services.
  • Custom Data Structures: Building your own linked lists, trees, or other data structures that can be parameterized by type.

Embracing generics leads to more robust, maintainable, and performant C# applications.