MSDN Documentation

Advanced C# Generics

This section dives deeper into the advanced features and patterns of C# generics, exploring how to leverage them for more sophisticated and type-safe code.

Generic Constraints - Beyond Basic Types

While generics allow you to write code that works with any type, sometimes you need to enforce specific capabilities on the type parameters. Generic constraints allow you to do this.

Reference Type Constraints

Ensures the type parameter is a reference type.

public class MyGenericClass<T> where T : class
{
    // T can be any reference type (class, interface, delegate, array, string, etc.)
}

Value Type Constraints

Ensures the type parameter is a non-nullable value type.

public class MyGenericClass<T> where T : struct
{
    // T can be any non-nullable value type (int, float, bool, struct, enum, etc.)
}

Specific Type Constraints

Ensures the type parameter is or derives from a specific class or implements a specific interface.

public interface IMyInterface { }

public class MyGenericClass<T> where T : MyBaseClass, IMyInterface
{
    // T must derive from MyBaseClass and implement IMyInterface
}

Constructor Constraints

Ensures the type parameter has a public, parameterless constructor. This is useful when you need to instantiate objects of the generic type.

public T CreateInstance<T>() where T : new()
{
    return new T();
}

Covariance and Contravariance

Covariance and contravariance allow for more flexible type relationships with generic interfaces and delegates.

Covariance (Output)

Allows you to use a more specific type than originally specified. Typically used with output positions (return types).

Consider an interface IEnumerable<out T>. This allows you to assign an IEnumerable<string> to an IEnumerable<object> because a string *is an* object.

IEnumerable<string> strings = new List<string> { "hello", "world" };
IEnumerable<object> objects = strings; // Covariant - Valid

Contravariance (Input)

Allows you to use a more general type than originally specified. Typically used with input positions (method parameters).

Consider a delegate type Action<in T>. This allows you to assign an Action<object> to an Action<string>. If a method can accept any object, it can certainly accept a string.

Action<object> logObject = obj => Console.WriteLine(obj);
Action<string> logString = logObject; // Contravariant - Valid

Generic Methods

Methods can also be generic, allowing them to operate on specific types without requiring the entire class to be generic.

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

Usage:

int x = 5, y = 10;
Utility.Swap<int>(ref x, ref y); // Explicitly specify type

string s1 = "abc", s2 = "def";
Utility.Swap(ref s1, ref s2); // Type inference - C# infers T as string

Generic Interfaces and Delegates

Interfaces and delegates can also be generic, providing powerful ways to define contracts and callbacks.

Generic Interfaces

As seen with IEnumerable<T>, generic interfaces define contracts that can be fulfilled by generic or non-generic classes.

Generic Delegates

Action<T> and Func<T, TResult> are built-in generic delegates that are widely used.

// Func<TInput, TOutput>
Func<int, string> intToString = number => number.ToString();
string result = intToString(123); // result = "123"

// Action<T>
Action<string> printString = Console.WriteLine;
printString("Hello, Generics!");

Common Generics Patterns and Use Cases

Performance Note: Generics provide performance benefits over non-generic collections or boxing/unboxing because they avoid the overhead of runtime type checking and data conversion.