C# Generics Basics
Introduction
Generics are one of the most powerful features in C#, introduced in C# 2.0. They allow you to define type-safe data structures without committing to specific data types. This means you can create classes, interfaces, methods, and delegates that can work with any data type while maintaining type safety.
In this guide, we'll explore the basics of C# generics, why they're useful, and how to implement them in your code.
Why Generics?
Before diving into the syntax and usage, let's understand why generics exist in the first place:
- Type Safety: Generics provide compile-time type checking, reducing the risk of runtime errors.
- Code Reusability: Write code once that works with multiple data types.
- Performance: Generics avoid boxing/unboxing operations for value types, improving performance.
- Cleaner Code: Reduces the need for type casting and the potential for casting exceptions.
Generic Classes
Let's start by creating a simple generic class that can store an item of any type.
public class Box<T>
{
private T item;
public void SetItem(T newItem)
{
item = newItem;
}
public T GetItem()
{
return item;
}
}
In this example, T
is a type parameter that represents a placeholder for a specific type that will be provided later. Here's how you can use this generic class:
// Create a Box for integers
Box<int> intBox = new Box<int>();
intBox.SetItem(123);
int number = intBox.GetItem(); // Returns 123
Console.WriteLine($"Integer value: {number}");
// Create a Box for strings
Box<string> stringBox = new Box<string>();
stringBox.SetItem("Hello, Generics!");
string message = stringBox.GetItem(); // Returns "Hello, Generics!"
Console.WriteLine($"String value: {message}");
Output:
Integer value: 123
String value: Hello, Generics!
Generic Methods
You can also create generic methods within non-generic classes:
public class Utilities
{
public static void Swap<T>(ref T first, ref T second)
{
T temp = first;
first = second;
second = temp;
}
public static bool AreEqual<T>(T first, T second)
{
return first.Equals(second);
}
}
Here's how to use these generic methods:
int a = 5, b = 10;
Console.WriteLine($"Before swap: a = {a}, b = {b}");
Utilities.Swap<int>(ref a, ref b);
Console.WriteLine($"After swap: a = {a}, b = {b}");
string str1 = "Hello";
string str2 = "Hello";
bool areEqual = Utilities.AreEqual<string>(str1, str2);
Console.WriteLine($"Strings equal: {areEqual}");
// Type inference - the compiler can deduce the type
Utilities.Swap(ref a, ref b); // No need to specify <int>
Output:
Before swap: a = 5, b = 10
After swap: a = 10, b = 5
Strings equal: True
Generic Constraints
Sometimes you want to restrict the types that can be used with your generic class or method. C# allows you to apply constraints on type parameters using the where
keyword:
// T must be a class (reference type)
public class Repository<T> where T : class
{
// Implementation
}
// T must have a parameterless constructor
public class Factory<T> where T : new()
{
public T Create()
{
return new T();
}
}
// Multiple constraints: T must inherit from Animal and implement ICloneable
public class AnimalProcessor<T> where T : Animal, ICloneable
{
// Implementation
}
Common constraints include:
where T : struct
- T must be a value typewhere T : class
- T must be a reference typewhere T : new()
- T must have a parameterless constructorwhere T : BaseClass
- T must inherit from BaseClasswhere T : IInterface
- T must implement IInterface
Practical Example: Generic Collection
Let's create a simple generic list implementation to understand generics better:
public class SimpleList<T>
{
private T[] items;
private int count;
public SimpleList(int capacity = 4)
{
items = new T[capacity];
count = 0;
}
public void Add(T item)
{
if (count == items.Length)
{
// Resize array if needed
T[] newItems = new T[items.Length * 2];
Array.Copy(items, newItems, items.Length);
items = newItems;
}
items[count] = item;
count++;
}
public T GetAt(int index)
{
if (index < 0 || index >= count)
{
throw new IndexOutOfRangeException();
}
return items[index];
}
public int Count => count;
}
Using our generic list:
SimpleList<double> grades = new SimpleList<double>();
grades.Add(92.5);
grades.Add(89.0);
grades.Add(95.8);
Console.WriteLine($"Number of grades: {grades.Count}");
Console.WriteLine($"First grade: {grades.GetAt(0)}");
// Using with a different type
SimpleList<string> names = new SimpleList<string>();
names.Add("Alice");
names.Add("Bob");
names.Add("Charlie");
for (int i = 0; i < names.Count; i++)
{
Console.WriteLine($"Name {i+1}: {names.GetAt(i)}");
}
Output:
Number of grades: 3
First grade: 92.5
Name 1: Alice
Name 2: Bob
Name 3: Charlie
Real-World Application: Generic Repository Pattern
A common use of generics in real-world applications is the Repository Pattern in database operations. Here's a simplified example:
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 Repository<T> : IRepository<T> where T : class
{
private readonly List<T> _entities = new List<T>();
// In a real application, this would connect to a database
public T GetById(int id)
{
// Simplified implementation
return _entities.FirstOrDefault();
}
public IEnumerable<T> GetAll()
{
return _entities;
}
public void Add(T entity)
{
_entities.Add(entity);
}
public void Update(T entity)
{
// Implementation
}
public void Delete(int id)
{
// Implementation
}
}
Usage with different entity types:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
// Using the generic repository
IRepository<User> userRepository = new Repository<User>();
userRepository.Add(new User { Id = 1, Name = "John Doe" });
IRepository<Product> productRepository = new Repository<Product>();
productRepository.Add(new Product { Id = 1, Name = "Laptop", Price = 999.99m });
Generic Type Inference
C# can often infer the generic type arguments from the method parameters, which makes your code cleaner:
public class GenericHelper
{
public static void Display<T>(T item)
{
Console.WriteLine($"Type: {typeof(T).Name}, Value: {item}");
}
}
// These calls are equivalent:
GenericHelper.Display<int>(42);
GenericHelper.Display(42); // Type inference in action
Multiple Type Parameters
Generic classes and methods can have multiple type parameters:
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
public Pair(TFirst first, TSecond second)
{
First = first;
Second = second;
}
public override string ToString()
{
return $"({First}, {Second})";
}
}
// Usage
Pair<int, string> entry = new Pair<int, string>(1, "One");
Console.WriteLine(entry); // Output: (1, One)
Pair<string, bool> setting = new Pair<string, bool>("IsEnabled", true);
Console.WriteLine(setting); // Output: (IsEnabled, True)
Default Values in Generics
When you need a default value for a generic type, use the default
keyword:
public static T GetDefaultValue<T>()
{
return default(T); // For reference types: null, for value types: their default
}
int defaultInt = GetDefaultValue<int>(); // Returns 0
string defaultString = GetDefaultValue<string>(); // Returns null
Summary
In this guide, we've learned:
- What generics are and why they're useful
- How to create generic classes with type parameters
- How to write generic methods
- How to apply constraints to type parameters
- Real-world applications of generics like collections and repositories
- How type inference works with generics
- Creating generics with multiple type parameters
Generics are a cornerstone feature in C# that enables you to write flexible, reusable, and type-safe code. By mastering generics, you'll be able to create more elegant and efficient solutions to common programming problems.
Exercise Ideas
- Create a generic
Stack<T>
class withPush
,Pop
, andPeek
methods. - Implement a generic
Calculator<T>
class that can perform basic arithmetic on different numeric types. - Write a generic method that can find the maximum value in an array of any comparable type.
- Create a generic
KeyValuePair<TKey, TValue>
class similar to the one in .NET. - Implement a generic
Filter<T>
method that returns elements from a list that satisfy a given predicate.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)