C# Advanced Generics
Generics in C# provide a way to define type-safe collections and methods that can operate on any data type without sacrificing type safety or performance. This document explores advanced concepts and practical applications of generics.
Introduction to Generics
Before generics, collections like `ArrayList` stored elements as objects, requiring casting and leading to potential runtime errors and performance overhead. Generics solve this by allowing you to specify the type of elements a collection or method can work with.
Generic Classes
You can define your own generic classes. These classes have type parameters that are specified when an instance of the class is created.
public class Stack<T>
{
private List<T> elements = new List<T>();
public void Push(T item)
{
elements.Add(item);
}
public T Pop()
{
if (elements.Count == 0)
{
throw new InvalidOperationException("Stack is empty.");
}
T item = elements[elements.Count - 1];
elements.RemoveAt(elements.Count - 1);
return item;
}
}
Usage:
Stack<int> intStack = new Stack<int>();
intStack.Push(10);
int num = intStack.Pop(); // num is int, no cast needed
Stack<string> stringStack = new Stack<string>();
stringStack.Push("Hello");
string str = stringStack.Pop(); // str is string
Generic Interfaces
Similar to classes, interfaces can also be generic. This is common for defining contracts for generic types.
public interface IRepository<TEntity> where TEntity : class
{
TEntity GetById(int id);
void Add(TEntity entity);
void Update(TEntity entity);
void Delete(int id);
}
Implementation:
public class UserRepository : IRepository<User>
{
// ... implementation details ...
}
Generic Methods
Methods can also be generic, independent of their containing class (if any).
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 = "apple", s2 = "banana";
Utility.Swap(ref s1, ref s2); // Type inference can often be used
Constraints on Generic Types
Constraints allow you to restrict the types that can be used as type arguments for a generic type parameter. This enables you to call methods or access properties specific to certain types.
where T : struct: T must be a value type.where T : class: T must be a reference type.where T : new(): T must have a public, parameterless constructor.where T : BaseClass: T must inherit fromBaseClass.where T : InterfaceName: T must implementInterfaceName.where T : U: T must be or derive from the type U.
public class Factory<TProduct> where TProduct : new()
{
public TProduct CreateInstance()
{
return new TProduct(); // Possible because of the new() constraint
}
}
Covariance and Contravariance
These advanced features allow for more flexible use of generic types, particularly with interfaces and delegates.
- Covariance (out keyword): Allows using a more derived type than originally specified. Useful for outputting values. E.g., assigning an `IEnumerable<string>` to an `IEnumerable<object>`.
- Contravariance (in keyword): Allows using a less derived type than originally specified. Useful for inputting values. E.g., assigning an `Action<object>` to an `Action<string>`.
// Covariance example
IEnumerable<string> strings = new List<string> { "a", "b" };
IEnumerable<object> objects = strings; // Covariant assignment
// Contravariance example
Action<object> printObject = obj => Console.WriteLine(obj);
Action<string> printString = printObject; // Contravariant assignment
printString("Hello"); // Outputs "Hello" to the console
Generic Collections
The .NET Framework provides a rich set of generic collection types in the System.Collections.Generic namespace:
List<T>: Dynamically sized array.Dictionary<TKey, TValue>: Key-value pairs.HashSet<T>: Unique elements.Queue<T>: First-In, First-Out (FIFO).Stack<T>: Last-In, First-Out (LIFO).SortedList<TKey, TValue>: Sorted key-value pairs.
Using these avoids the overhead and type-safety issues of older non-generic collections.
Common Use Cases
- Type-safe collections (e.g.,
List<Customer>). - Reusable algorithms that operate on different data types (e.g., sorting, searching).
- Data access layers (e.g., generic repositories).
- Caching mechanisms where the type of cached item can vary.
- Asynchronous programming with generic
Task<TResult>.