What Are Generics in C# and Why Are They Needed
Imagine you are writing a method that should work with different data types: integers, strings, objects. Without Generics, you would have to either duplicate code for each type or use object with subsequent type casting. Both approaches lead to problems: the first leads to redundancy, the second leads to runtime errors.
Generics (generic types) are a mechanism in C# that allows you to create classes, methods, interfaces, and delegates that work with any data type while maintaining strong typing. Instead of writing code for a specific type, you use a type parameter (usually denoted by the letter T), which is replaced with the actual type when used.
Main advantages of Generics:
- Type safety — the compiler checks the correctness of type usage at compile time
- Performance — no boxing/unboxing for value types
- Code reuse — one class/method works with multiple types
- Readability — code becomes cleaner and more understandable
The Problem Without Generics: Example with object
Before the advent of Generics, developers often used the object type to create universal methods. Consider an example:
public class Stack{ private object[] _items = new object[10]; private int _count = 0; public void Push(object item) { _items[_count++] = item; } public object Pop() { return _items[--_count]; } // Usage Stack stack = new Stack(); stack.Push(123); // Boxing int to object stack.Push("hello"); // Any type can be added int number = (int)stack.Pop(); // Unboxing and casting}The problems are obvious:
- Explicit type casting is required when retrieving
- You can accidentally add a value of a different type
- For value types (int, double), boxing/unboxing occurs, which reduces performance
Basics of Working with Generics in C#
Creating a Generic Class
Consider the same stack example, but using Generics:
public class Stack<T>{ private T[] _items = new T[10]; private int _count = 0; public void Push(T item) { _items[_count++] = item; } public T Pop() { return _items[--_count]; }}
// UsageStack<int> intStack = new Stack<int>();intStack.Push(123);intStack.Push(456);int number = intStack.Pop(); // No unboxing needed, type is known
Stack<string> stringStack = new Stack<string>();stringStack.Push("Hello");// stringStack.Push(123); // Compilation error! Type safetyThe parameter <T> is specified in angle brackets. When creating an instance, you specify the concrete type, and the compiler generates a specialized version of the class for that type.
Generic Methods
Generics work not only with classes but also with individual methods:
public class Utils{ public static T Max<T>(T a, T b) where T : IComparable<T> { return a.CompareTo(b) > 0 ? a : b; }}
// Usageint maxInt = Utils.Max(5, 10); // 10string maxStr = Utils.Max("apple", "banana"); // "banana"Constraints in Generics
Sometimes you need to restrict the types that can be used with a generic. For this, constraints are used with the where keyword:
// Constraint: T must be a reference typepublic class Repository<T> where T : class{