C# Generics
Writing reusable code that works with any data type
🔧 What are Generics?
Generics allow you to write flexible, reusable code that works with any data type. Instead of creating separate methods for integers, strings, and other types, you write one generic version.
// Generic method that works with any type
public T GetFirst<T>(List<T> items)
{
return items[0];
}
// Works with integers
int firstNumber = GetFirst(new List<int> { 1, 2, 3 });
// Works with strings
string firstName = GetFirst(new List<string> { "A", "B" });
Output:
firstNumber = 1
firstName = "A"
Generic Concepts
Generic Classes
Classes that work with any type
class Box<T>
{ public T Value; }
Generic Methods
Methods with type parameters
void Swap<T>(ref T a,
ref T b)
Constraints
Limit types that can be used
where T : class
where T : struct
Type Safety
Compile-time type checking
List<int> numbers;
// Only integers allowed
🔹 Generic Classes
Generic classes in C# use type parameters (like T) to create flexible, type-safe data structures. By defining a class such as Box<T>, you can instantiate it with any data type (int, string, double). This eliminates code duplication for different types while maintaining compile-time type checking. For instance, Box<int> stores 123, Box<string> holds "Hello", and Box<double> contains 45.67, all using the same class definition.
using System;
// Generic class definition
class Box<T>
{
public T Value { get; set; }
public void Display()
{
Console.WriteLine("Box contains: " + Value);
}
}
class Program
{
static void Main()
{
// Box for integers
Box<int> intBox = new Box<int>();
intBox.Value = 123;
intBox.Display();
// Box for strings
Box<string> stringBox = new Box<string>();
stringBox.Value = "Hello";
stringBox.Display();
// Box for doubles
Box<double> doubleBox = new Box<double>();
doubleBox.Value = 45.67;
doubleBox.Display();
}
}
Output:
Box contains: 123
Box contains: Hello
Box contains: 45.67
🔹 Generic Methods
Generic methods allow a single method to operate on arguments of various types without sacrificing type safety. Defined with a type parameter (e.g., Swap<T>(ref T x, ref T y)), they can swap integers (x=5, y=10 becomes x=10, y=5), strings, or any other type. They are also used with LINQ-style operations to print sequences like "Alice Bob Charlie" or 1 2 3 4 5. This promotes code reusability and reduces redundancy across different data type implementations.
using System;
class Program
{
// Generic method to swap two values
static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
// Generic method to display array
static void DisplayArray<T>(T[] array)
{
foreach (T item in array)
{
Console.Write(item + " ");
}
Console.WriteLine();
}
static void Main()
{
// Swap integers
int x = 5, y = 10;
Console.WriteLine($"Before: x={x}, y={y}");
Swap(ref x, ref y);
Console.WriteLine($"After: x={x}, y={y}");
// Display string array
string[] names = { "Alice", "Bob", "Charlie" };
DisplayArray(names);
// Display int array
int[] numbers = { 1, 2, 3, 4, 5 };
DisplayArray(numbers);
}
}
Output:
Before: x=5, y=10
After: x=10, y=5
Alice Bob Charlie
1 2 3 4 5
🔹 Generic Constraints
Constraints in generics restrict the types that can be used as arguments, ensuring they possess required capabilities. Using the where clause, you can specify that a type must be a class (where T : class), have a parameterless constructor (where T : new()), or implement an interface. For example, a Repository<T> where T : IEntity guarantees that only entities with a Save method (like saving "Alice") or a Create method (creating "Bob") can be used, enhancing safety and design clarity.
using System;
// Constraint: T must be a class (reference type)
class Repository<T> where T : class
{
public void Save(T item)
{
Console.WriteLine("Saving: " + item);
}
}
// Constraint: T must have a parameterless constructor
class Factory<T> where T : new()
{
public T Create()
{
return new T();
}
}
class Person
{
public string Name { get; set; }
public override string ToString() => Name;
}
class Program
{
static void Main()
{
// Works with class types
Repository<Person> repo = new Repository<Person>();
Person person = new Person { Name = "Alice" };
repo.Save(person);
// Creates new instance
Factory<Person> factory = new Factory<Person>();
Person newPerson = factory.Create();
newPerson.Name = "Bob";
Console.WriteLine("Created: " + newPerson);
}
}
Output:
Saving: Alice
Created: Bob
🔹 Multiple Type Parameters
Generic classes and methods can define multiple type parameters, enabling them to handle several distinct types simultaneously. A class like Pair<TFirst, TSecond> can store combinations such as a string and an integer (First: Alice, Second: 25), an integer and a double (First: 101, Second: 95.5), or two strings (First: Username, Second: john_doe). This provides a strongly-typed way to group heterogeneous data without resorting to non-generic containers like object, improving performance and readability.
using System;
// Generic class with two type parameters
class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
public void Display()
{
Console.WriteLine($"First: {First}, Second: {Second}");
}
}
class Program
{
static void Main()
{
// Pair of string and int
Pair<string, int> nameAge = new Pair<string, int>();
nameAge.First = "Alice";
nameAge.Second = 25;
nameAge.Display();
// Pair of int and double
Pair<int, double> idScore = new Pair<int, double>();
idScore.First = 101;
idScore.Second = 95.5;
idScore.Display();
// Pair of strings
Pair<string, string> keyValue = new Pair<string, string>();
keyValue.First = "Username";
keyValue.Second = "john_doe";
keyValue.Display();
}
}
Output:
First: Alice, Second: 25
First: 101, Second: 95.5
First: Username, Second: john_doe
🔹 Common Generic Constraints
C# provides several constraint types to control what types can be used with your generics:
Constraint Types:
- where T : class - T must be a reference type
- where T : struct - T must be a value type
- where T : new() - T must have a parameterless constructor
- where T : BaseClass - T must inherit from BaseClass
- where T : IInterface - T must implement IInterface
// Multiple constraints example
class DataStore<T> where T : class, new()
{
public T CreateNew()
{
return new T(); // Can create instance
}
}