Generics in C#: Why They Are Needed and How to Use Them in Practice

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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 type


With 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 = string


Main 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 type
  • where T : struct — value type
  • where T : new() — must have a parameterless constructor
  • where T : BaseClass — must inherit from a specific base class
  • where 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 unboxing


Benchmarks 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

Blogs

Book Recommendations