Generics in C#: Why They Are Needed and How to Use Them in Practice
Generics are one of the key features of the C# language, allowing you to write flexible, reusable, and type-safe code. In this article, we will break down why generics are needed, how they work under the hood, and show practical examples of their use in real projects.
What are Generics and Why Are They Needed?
Imagine you need to implement a method that swaps two values. Without generics, you would have to write separate versions for each data type:
// Without Generics — code duplicationvoid SwapInt(ref int a, ref int b) { int temp = a; a = b; b = temp; }void SwapString(ref string a, ref string b) { string temp = a; a = b; b = temp; }// ... and so on for each typeWith Generics, this problem is solved elegantly:
// With Generics — one method for all typesvoid Swap<T>(ref T a, ref T b){ T temp = a; a = b; b = temp;}
// Usageint x = 1, y = 2;Swap(ref x, ref y); // T = int
string s1 = "Hello", s2 = "World";Swap(ref s1, ref s2); // T = stringMain advantages of Generics:
- Type safety — type errors are caught at compile time
- Performance — no boxing/unboxing for value types
- Code reuse — one class/method works with any type
- Readability — code becomes clearer and more concise
Generic Classes and Structures
The most common example of using Generics is collections. Instead of writing your own class for each type, we create a generic one:
public class MyList<T>{ private T[] _items; private int _count;
public MyList(int capacity = 4) { _items = new T[capacity]; _count = 0; }
public void Add(T item) { if (_count == _items.Length) { Array.Resize(ref _items, _items.Length * 2); } _items[_count++] = item; }
public T Get(int index) { if (index < 0 || index >= _count) throw new IndexOutOfRangeException(); return _items[index]; }
public int Count => _count;}
// Usagevar numbers = new MyList<int>();numbers.Add(10);numbers.Add(20);
var names = new MyList<string>();names.Add("Alice");Note: when creating an instance, we specify the specific type in angle brackets <T>. The compiler substitutes this type everywhere T is used.
Generic Methods and Constraints
Sometimes you need to impose constraints on type T. For example, to guarantee that the type implements a certain interface or has a parameterless constructor:
// Constraint: T must implement IComparable<T>public T GetMax<T>(T a, T b) where T : IComparable<T>{ return a.CompareTo(b) > 0 ? a : b;}
// Constraint: T must have a parameterless constructorpublic class Factory<T> where T : new(){ public T Create() { return new T(); }}
// Constraint: T must be a value type (struct)public struct Nullable<T> where T : struct{ private T _value; private bool _hasValue;
public Nullable(T value) { _value = value; _hasValue = true; }
public T Value => _hasValue ? _value : throw new InvalidOperationException(); public bool HasValue => _hasValue;}Main types of constraints:
where T : class— reference typewhere T : struct— value typewhere T : new()— must have a parameterless constructorwhere T : BaseClass— must inherit from a specific base classwhere T : IInterface— must implement a specific interface
Generics in Real Projects
In real development, generics are used everywhere. Here are a few examples:
1. Repositories in the Repository pattern
public interface IRepository<T> where T : class{ T GetById(int id); IEnumerable<T> GetAll(); void Add(T entity); void Update(T entity); void Delete(int id);}
public class UserRepository : IRepository<User>{ // Implementation for User}2. Event handlers and messaging
public interface IEventHandler<TEvent> where TEvent : IEvent{ Task Handle(TEvent eventData);}
public class OrderCreatedHandler : IEventHandler<OrderCreatedEvent>{ public async Task Handle(OrderCreatedEvent eventData) { // Logic for handling order creation }}3. Configuration and settings
public class ConfigurationManager<T> where T : class, new(){ private T _settings;
public T Load() { _settings = new T(); // Load settings from file or database return _settings; }
public void Save() { // Save settings }}Performance: Generics vs. Boxing
One of the key advantages of generics is performance. Without generics, when working with value types (int, double, struct), boxing occurs — converting a value type to the object type, which is resource-intensive:
// Without generics — boxingArrayList list = new ArrayList();list.Add(42); // Boxing: int -> objectint value = (int)list[0]; // Unboxing: object -> int
// With generics — no boxingList<int> list = new List<int>();list.Add(42); // No boxingint value = list[0]; // No unboxingBenchmarks show that using generic collections can be 10-20 times faster than non-generic ones when working with value types.
Conclusion
Generics in C# are a powerful tool that every developer should master. They help write safer, faster, and more maintainable code. Start using generics in your projects today — and you will notice how much cleaner and more professional your code becomes.
Key takeaways:
- Generics provide type safety at compile time
- They improve performance by eliminating boxing/unboxing
- They allow creating universal classes and methods
- Constraints help control what types can be used
- Generics are actively used in real projects: repositories, event handlers, configuration