Generics allow developers to create classes, methods, interfaces, and other types using type parameters instead of fixed data types. They make code reusable while providing compile-time type safety.
- Allow the same code to work with different data types without duplication.
- Detect type mismatches at compile time and reduce explicit casting.
- Help avoid boxing and unboxing when working with value types.
Generics
Generics provide a way to define reusable components that can operate on different data types. Instead of writing separate classes or methods for integers, strings, and other types, developers can use a type parameter such as T.
For example, a generic class Box<T> can store an integer using Box<int> or a string using Box<string> without changing its implementation.

Syntax
class ClassName<T>{
// Members using type T
}
Here:
- T is a type parameter that acts as a placeholder for a data type.
- The actual type, such as int or string, is provided when creating an object.
- The compiler checks whether the supplied values match the specified type.
Generic Classes
A generic class is a class that uses one or more type parameters. It allows the same class definition to work with different data types without duplicating code.
Example: Program demonstrates a generic class that stores and displays values of different types.
using System;
class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public void Display()
{
Console.WriteLine("Value: " + value);
}
}
class Geeks {
static void Main()
{
Box<int> intBox = new Box<int>(100);
intBox.Display();
Box<string> stringBox
= new Box<string>("GeeksforGeeks");
stringBox.Display();
}
}
Output
Value: 100 Value: GeeksForGeeks
Explanation:
- Box<T> defines a generic class with a type parameter T.
- Box<int> creates an instance that stores an integer.
- Box<string> creates an instance that stores a string.
- The same class implementation works with both data types without changing its code.
Generic Classes with Multiple Type Parameters
A generic class can have multiple type parameters when it needs to work with more than one data type.
Syntax:
class ClassName<T1, T2>
{
// Members using T1 and T2
}
Example: The following program stores a student ID and name using two different type parameters.
using System;
class Student<T1, T2>
{
public T1 Id { get; set; }
public T2 Name { get; set; }
public Student(T1 id, T2 name)
{
Id = id;
Name = name;
}
public void Display()
{
Console.WriteLine($"ID: {Id}, Name: {Name}");
}
}
class Geeks
{
static void Main()
{
Student<int, string> student =
new Student<int, string>(101, "Rahul");
student.Display();
}
}
Output
ID: 101, Name: Rahul
Here, T1 represents the integer ID, while T2 represents the string name.
Generic Methods
A generic method uses a type parameter to perform the same operation on different data types. It can be defined inside either a generic or a non-generic class.
using System;
class Geeks
{
static void Display<T>(T value)
{
Console.WriteLine(value);
}
static void Main()
{
Display<int>(10);
Display<string>("Hello");
Display<double>(25.5);
}
}
Output
10 Hello 25.5
The Display<T>() method accepts integers, strings, and doubles using the same implementation. The compiler can also infer the type argument, allowing calls such as Display(10).
Constraints in Generics
Generic constraints restrict the types that can be passed as type arguments. They are defined using the where keyword.
Common generic constraints include:
Constraint | Description |
|---|---|
where T : class | Requires a reference type. |
where T : struct | Requires a non-nullable value type. |
where T : new() | Requires a public parameterless constructor. |
where T : BaseClass | Requires inheritance from a specified base class. |
where T : IInterface | Requires implementation of a specified interface. |
where T : notnull | Requires a non-nullable type, with nullable warnings in an enabled nullable context. |
Example: The following generic class accepts only types implementing IComparable<T>.
using System;
class Calculator<T> where T : IComparable<T>
{
public T GetMaximum(T a, T b)
{
return a.CompareTo(b) >= 0 ? a : b;
}
}
class Geeks
{
static void Main()
{
Calculator<int> calc = new Calculator<int>();
Console.WriteLine(calc.GetMaximum(10, 20));
}
}
Output
20
The IComparable<T> constraint allows the method to compare two values of type T using CompareTo().
Generic Collections
C# provides several built-in generic collection classes in the System.Collections.Generic namespace. These collections store elements using specified data types.
Collection | Description |
|---|---|
List<T> | Stores a dynamically sized list of elements. |
Dictionary<TKey, TValue> | Stores key-value pairs with unique keys. |
Queue<T> | Processes elements in FIFO order. |
Stack<T> | Processes elements in LIFO order. |
HashSet<T> | Stores unique elements. |
SortedSet<T> | Stores unique elements in sorted order. |
Example: Using a generic List<T>.
using System;
using System.Collections.Generic;
class Geeks
{
static void Main()
{
List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
numbers.Add(30);
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}
}
Output
10 20 30
Here, List<int> stores only integers. Attempting to add a string results in a compile-time error.
Advantages of Generics
Generics provide several benefits when developing reusable and type-safe applications.
- Code Reusability: A single class or method can work with multiple data types, reducing duplicate implementations.
- Type Safety: Type mismatches are detected at compile time, helping prevent invalid assignments.
- Better Performance: Generic collections can avoid unnecessary boxing and unboxing of value types.
- Reduced Casting: Strongly typed values generally eliminate the need for explicit casts when retrieving data.
- Maintainability: Shared implementations make code easier to update and maintain.
Generics Vs Non-Generics
Generics | Non-Generics |
|---|---|
Use type parameters such as T. | Use fixed types or general types such as object. |
Provide compile-time type safety. | Object-based implementations may require runtime type checks. |
Usually avoid explicit casting. | May require explicit casting. |
Avoid unnecessary boxing for value types. | Object-based storage may involve boxing and unboxing. |
Example: List<int> | Example: ArrayList |
When to Use Generics?
Generics are useful when the same logic must work with different data types while maintaining type safety.
Common use cases include:
- Data Collections: Store strongly typed elements using List<T>, Queue<T>, or Dictionary<TKey, TValue>.
- Reusable Components: Create generic classes for data storage, processing, and result handling.
- Common Algorithms: Implement searching, sorting, or comparison logic for different data types, using constraints where necessary.