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
- Data Structures: Lists, dictionaries, stacks, queues that can hold any type.
- LINQ: Most LINQ extension methods operate on generic collections like
IEnumerable<T>. - Dependency Injection: Frameworks often use generics to register and resolve services.
- Factory Patterns: Creating generic factory methods or classes.
- Event Handling: Generic event arguments.