Skip to main content

.NET Generics

Introduction

Generics are one of the most powerful features in .NET, introduced in C# 2.0. They allow you to define classes, interfaces, methods, and delegates where the type is specified when the code is used rather than when it's written. Generics provide type safety, reduce the need for casting, improve performance, and enable code reuse across different data types.

Think of generics as "templates" that let you write code that can work with any data type while maintaining type safety. Instead of writing the same logic multiple times for different types, you write it once using a type parameter.

Understanding Generics

What Problems Do Generics Solve?

Before generics, if you wanted to create a collection that could store any type of object, you'd use ArrayList or similar non-generic collections:

csharp
ArrayList list = new ArrayList();
list.Add(42); // Adding an integer
list.Add("Hello"); // Adding a string
list.Add(true); // Adding a boolean

// To use the items, we need casting:
int firstItem = (int)list.GetValue(0); // Explicit casting required

This approach had significant drawbacks:

  1. No Type Safety: The compiler couldn't check if you were using the right types
  2. Performance Issues: Boxing/unboxing when storing value types
  3. Runtime Errors: Invalid casts could crash your application

Generics solve these problems by providing compile-time type checking while maintaining flexibility.

Basic Generic Syntax

To create a generic class in C#:

csharp
public class Box<T>
{
private T item;

public void Add(T item)
{
this.item = item;
}

public T Get()
{
return item;
}
}

Here, T is a type parameter that will be specified when the class is used:

csharp
// Box for storing integers
Box<int> intBox = new Box<int>();
intBox.Add(123);
int number = intBox.Get(); // No casting needed

// Box for storing strings
Box<string> stringBox = new Box<string>();
stringBox.Add("Hello Generics");
string message = stringBox.Get(); // Type-safe and no casting

Generic Collections

The most common use of generics is with collections. .NET provides a rich set of generic collections in the System.Collections.Generic namespace:

List<T>

List<T> is the generic version of ArrayList and is one of the most widely used generic collections:

csharp
// Create a list of integers
List<int> numbers = new List<int>();

// Add items
numbers.Add(10);
numbers.Add(20);
numbers.Add(30);

// Access items by index
Console.WriteLine($"First number: {numbers[0]}");

// Iterate through the list
foreach (int num in numbers)
{
Console.WriteLine(num);
}

// Output:
// First number: 10
// 10
// 20
// 30

Dictionary<TKey, TValue>

Dictionary<TKey, TValue> is a generic collection that stores key-value pairs:

csharp
// Create a dictionary with string keys and int values
Dictionary<string, int> ages = new Dictionary<string, int>();

// Add key-value pairs
ages.Add("Alice", 28);
ages.Add("Bob", 32);
ages["Charlie"] = 25; // Alternative syntax

// Access values by key
Console.WriteLine($"Bob's age: {ages["Bob"]}");

// Check if a key exists before accessing
if (ages.ContainsKey("David"))
{
Console.WriteLine($"David's age: {ages["David"]}");
}
else
{
Console.WriteLine("David not found");
}

// Iterate through entries
foreach (KeyValuePair<string, int> entry in ages)
{
Console.WriteLine($"{entry.Key} is {entry.Value} years old");
}

// Output:
// Bob's age: 32
// David not found
// Alice is 28 years old
// Bob is 32 years old
// Charlie is 25 years old

Other Generic Collections

  • 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
  • LinkedList<T>: A doubly linked list

Generic Methods

You can also create generic methods within non-generic classes:

csharp
public class Utilities
{
// A generic method that swaps two values of any type
public void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}

// Usage:
Utilities utils = new Utilities();
int x = 10, y = 20;
utils.Swap<int>(ref x, ref y); // Type argument can be explicit
Console.WriteLine($"x = {x}, y = {y}"); // Output: x = 20, y = 10

string s1 = "hello", s2 = "world";
utils.Swap(ref s1, ref s2); // Type argument can be inferred
Console.WriteLine($"s1 = {s1}, s2 = {s2}"); // Output: s1 = world, s2 = hello

Generic Constraints

Sometimes you need to restrict what types can be used with your generic code. Generic constraints allow you to specify requirements for the type parameters:

csharp
// T must be a class (reference type)
public class Container<T> where T : class
{
// Implementation
}

// T must have a parameterless constructor
public class Factory<T> where T : new()
{
public T Create()
{
return new T();
}
}

// T must implement the IComparable interface
public class Sorter<T> where T : IComparable<T>
{
public void Sort(T[] items)
{
// Implementation that uses CompareTo method
}
}

// Multiple constraints: T must be a class that implements IDisposable and has a parameterless constructor
public class ResourceManager<T> where T : class, IDisposable, new()
{
// Implementation
}

Real-World Example: Generic Repository Pattern

The Repository Pattern is commonly used in applications to abstract the data access layer. Here's how you might implement it using generics:

csharp
// Define an interface for our entities
public interface IEntity
{
int Id { get; set; }
}

// A basic entity class
public class Customer : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}

// Generic repository interface
public interface IRepository<T> where T : IEntity
{
T GetById(int id);
IEnumerable<T> GetAll();
void Add(T entity);
void Update(T entity);
void Delete(int id);
}

// Generic repository implementation
public class Repository<T> : IRepository<T> where T : IEntity
{
private readonly List<T> _entities = new List<T>();
private int _nextId = 1;

public T GetById(int id)
{
return _entities.FirstOrDefault(e => e.Id == id);
}

public IEnumerable<T> GetAll()
{
return _entities.ToList();
}

public void Add(T entity)
{
if (entity.Id == 0)
{
entity.Id = _nextId++;
}
_entities.Add(entity);
}

public void Update(T entity)
{
var existingEntity = GetById(entity.Id);
if (existingEntity != null)
{
_entities.Remove(existingEntity);
_entities.Add(entity);
}
}

public void Delete(int id)
{
var entity = GetById(id);
if (entity != null)
{
_entities.Remove(entity);
}
}
}

// Usage example
public class Program
{
public static void Main()
{
// Create a repository for customers
IRepository<Customer> customerRepo = new Repository<Customer>();

// Add customers
customerRepo.Add(new Customer { Name = "John Doe", Email = "[email protected]" });
customerRepo.Add(new Customer { Name = "Jane Smith", Email = "[email protected]" });

// Get all customers
var customers = customerRepo.GetAll();
foreach (var customer in customers)
{
Console.WriteLine($"ID: {customer.Id}, Name: {customer.Name}, Email: {customer.Email}");
}

// Get a specific customer
var customer1 = customerRepo.GetById(1);
if (customer1 != null)
{
Console.WriteLine($"Found: {customer1.Name}");
}
}
}

// Output:
// ID: 1, Name: John Doe, Email: [email protected]
// ID: 2, Name: Jane Smith, Email: [email protected]
// Found: John Doe

This example shows how generics enable us to write a single repository implementation that works with any entity type, providing type safety and code reuse.

Generic Covariance and Contravariance

C# 4.0 introduced covariance and contravariance for generic type parameters, which allow for more flexible assignments of generic types:

  • Covariance: Allows a method to return a more derived type than specified by the generic parameter
  • Contravariance: Allows a method to accept a less derived type than specified by the generic parameter
csharp
// Covariance with generic interfaces
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // This works because IEnumerable<T> is covariant

// Contravariance with action delegates
Action<object> objectAction = obj => Console.WriteLine(obj);
Action<string> stringAction = objectAction; // This works because Action<T> is contravariant

Summary

Generics in .NET provide a powerful way to create reusable, type-safe code. They offer several advantages:

  • Type Safety: Compile-time type checking that prevents errors
  • Reusability: Write code once that works with multiple types
  • Performance: Avoid boxing/unboxing operations with value types
  • Code Clarity: Less casting and clearer intentions

As you continue to develop with .NET, generics will become an essential tool in your programming toolkit. They form the foundation of many framework libraries and are crucial for writing efficient, maintainable code.

Further Learning

Exercises

  1. Create a generic Stack<T> class with Push(), Pop(), and Peek() methods
  2. Implement a generic Pair<T, U> class that holds two values of potentially different types
  3. Write a generic method that finds the maximum element in an array of any comparable type
  4. Create a generic repository that stores data in a file instead of memory

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)