.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:
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:
- No Type Safety: The compiler couldn't check if you were using the right types
- Performance Issues: Boxing/unboxing when storing value types
- 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#:
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:
// 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:
// 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:
// 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 elementsQueue<T>
: A first-in, first-out (FIFO) collectionStack<T>
: A last-in, first-out (LIFO) collectionLinkedList<T>
: A doubly linked list
Generic Methods
You can also create generic methods within non-generic classes:
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:
// 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:
// 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
// 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
- Create a generic
Stack<T>
class withPush()
,Pop()
, andPeek()
methods - Implement a generic
Pair<T, U>
class that holds two values of potentially different types - Write a generic method that finds the maximum element in an array of any comparable type
- 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! :)