Empower Your Code with Type Safety and Reusability
Generics in .NET provide a way to define type-safe collections and classes that can operate on any data type. They enhance code reusability, readability, and performance by allowing you to write algorithms and data structures that work with a variety of types without sacrificing type safety.
Before generics, developers often had to rely on the object
type or create separate classes for each data type, leading to potential runtime errors and verbose code. Generics solve these problems by introducing placeholders for types, which are then specified when the generic type or method is used.
Generics offer several key advantages:
object
.A generic type is a class, struct, interface, or delegate that is parameterized by one or more types. These type parameters are specified in angle brackets (<T>
).
Consider a simple generic stack implementation:
public class GenericStack<T>
{
private readonly List<T> _items = new List<T>();
public void Push(T item)
{
_items.Add(item);
}
public T Pop()
{
if (_items.Count == 0)
{
throw new InvalidOperationException("Stack is empty.");
}
T item = _items[_items.Count - 1];
_items.RemoveAt(_items.Count - 1);
return item;
}
public int Count => _items.Count;
}
You can then create instances of this generic type for specific data types:
// Generic stack for integers
var intStack = new GenericStack<int>();
intStack.Push(10);
intStack.Push(20);
Console.WriteLine(intStack.Pop()); // Output: 20
// Generic stack for strings
var stringStack = new GenericStack<string>();
stringStack.Push("Hello");
stringStack.Push("World");
Console.WriteLine(stringStack.Pop()); // Output: World
If you try to push an incompatible type, the compiler will flag it as an error:
// intStack.Push("This will cause a compile-time error.");
Generic methods are methods that have their own type parameters. They can be defined within generic or non-generic types.
Here's an example of a generic method to swap two values:
public static class Utility
{
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;
int y = 10;
Utility.Swap(ref x, ref y); // T is inferred as int
Console.WriteLine($"x: {x}, y: {y}"); // Output: x: 10, y: 5
string s1 = "First";
string s2 = "Second";
Utility.Swap(ref s1, ref s2); // T is inferred as string
Console.WriteLine($"s1: {s1}, s2: {s2}"); // Output: s1: Second, s2: First
Type constraints allow you to specify requirements for the types that can be used as arguments for generic type parameters. This enables you to call methods or access members of the constrained type within the generic code.
Common constraints include:
where T : struct
: The type argument must be a value type.where T : class
: The type argument must be a reference type.where T : new()
: The type argument must have a public, parameterless constructor.where T : BaseClassName
: The type argument must be or derive from BaseClassName
.where T : InterfaceName
: The type argument must implement InterfaceName
.Example with a constraint:
public class Calculator<T> where T : struct
{
public T Add(T a, T b)
{
// This requires T to be a type that supports addition.
// For primitive types, we can use dynamic or reflection,
// or a more specific constraint if available.
// For demonstration, assume it's a numeric type.
// A better approach might use specific numeric generic interfaces if available.
if (typeof(T) == typeof(int)) return (dynamic)a + (dynamic)b;
if (typeof(T) == typeof(double)) return (dynamic)a + (dynamic)b;
throw new NotSupportedException("Type not supported for addition.");
}
}
var intCalc = new Calculator<int>();
Console.WriteLine(intCalc.Add(5, 3)); // Output: 8
// var stringCalc = new Calculator<string>(); // Compile-time error because string is not a struct
Using dynamic
can bypass compile-time type checking for the operation itself, so use constraints carefully to ensure type safety where possible.
.NET provides a rich set of generic collection types in the System.Collections.Generic
namespace. These are highly recommended over their non-generic counterparts (e.g., ArrayList
).
List<T>
: A resizable array.Dictionary<TKey, TValue>
: A collection of key/value pairs.HashSet<T>
: A collection of unique elements.Queue<T>
: A First-In, First-Out (FIFO) collection.Stack<T>
: A Last-In, First-Out (LIFO) collection.Example using List<T>
and Dictionary<TKey, TValue>
:
// List of strings
var names = new List<string> { "Alice", "Bob", "Charlie" };
names.Add("David");
Console.WriteLine("Names:");
foreach (var name in names)
{
Console.WriteLine($"- {name}");
}
// Dictionary mapping product IDs to names
var products = new Dictionary<int, string>();
products.Add(101, "Laptop");
products.Add(102, "Mouse");
Console.WriteLine($"\nProduct 101: {products[101]}");
Covariance and contravariance are advanced features of generics that allow for more flexible type substitutions.
IEnumerable<string>
, you can assign it to a variable of type IEnumerable<object>
. This applies to output (return values). Generic interfaces and delegates can be covariant using the out
keyword.Action<object>
, you can assign it a delegate of type Action<string>
. This applies to input parameters. Generic interfaces and delegates can be contravariant using the in
keyword.Covariance Example:
IEnumerable<string> strings = new List<string> { "apple", "banana" };
IEnumerable<object> objects = strings; // Covariance: string is assignable to object
foreach (var obj in objects)
{
Console.WriteLine(obj);
}
Contravariance Example:
Action<object> printObject = Console.WriteLine;
Action<string> printString = printObject; // Contravariance: object is assignable to string for Action input
printString("This will be printed.");
Covariance and contravariance are supported for generic interfaces (e.g., IEnumerable<out T>
, IComparer<in T>
) and delegates. They are not directly supported for generic classes.
Generics are a fundamental feature of C# and the .NET platform. By understanding and utilizing generics, you can write more robust, efficient, and maintainable code. They provide a powerful mechanism for abstracting over types, enabling the creation of flexible and type-safe software components.
Embracing generics, especially with the built-in generic collections, is a key step towards writing modern, high-quality .NET applications.