11
JulGenerics in C#: A Beginner's Guide
Generics in C# allows you to write flexible, reusable code that works with any data type while maintaining type safety. They are unable to create a single method, class, or interface that can work with multiple types, such as integers, strings, or custom objects, without rewriting the logic. Whether you're a beginner or an experienced C# developer, mastering Generics will help you write cleaner and more efficient applications.
What Are Generics in C#?
Generics in C# allow you to define classes, interfaces, methods, or delegates with a placeholder for the data type. Generics allow you to handle different data types in a more organized, efficient way, reducing code duplication and enhancing maintainability.
Generics let you write code that can work with any data type — whether it’s a custom object, a string, or an int — without needing to rewrite the same logic for each type. This makes your code more adaptable and reusable. They also help to ensure type safety, meaning your code is checked at compile time to prevent errors caused by using incorrect types.
You define generics using angle brackets < > and specify the actual type when you use the generic class or method. For example, you can create a generic list that can store any type of data, making it easy to manage values without duplicating code.
Features of Generics in C#

Type Safety:
- Generics ensure that only a specific type can be used, catching errors at compile time.
Code Reusability
- You can write generic code once and reuse it for different types without duplicating logic.
Performance:
- Generics improve performance by avoiding unnecessary boxing and unboxing, which enhances execution speed and memory usage.
Flexibility
- Generics allow you to create data structures and algorithms that can work with any type, making your code more flexible.
Elimination of Type Casting:
- When using generics, there’s no need to manually cast objects from one type to another (unlike non-generic collections such as ArrayList).
Maintainability
- Code that uses generics is easier to read and maintain because the type relationships are clear and enforced.
Support for Custom Generic Classes and Methods
- You can create your own generic classes, generic methods, generic interfaces, and even generic delegates.
Works with Collections and LINQ
- Generics power most of the modern collection classes (List<T>, Dictionary<TKey, TValue>, etc.) and make LINQ queries type-safe.
Why Are Generics Important?
So, why are generics necessary? Let’s say you need a collection that holds integers, strings, and custom objects. You could use older non-generic collections like ArrayList, but they come with a major drawback: they aren’t type-safe.Feature | Non-Generics (ArrayList, etc.) | Generics (List, Dictionary<TKey, TValue>, etc.) |
Type Safety | No, allows mixed types | Yes, enforces compile-time type checking |
Performance | Slower (boxing/unboxing for value types) | Faster (no boxing/unboxing) |
Code Reusability | Limited | High — reusable with any type |
Type Casting | Manual casting required | No casting needed |
Readability& Maintainability | Low-more prone to errors. | High — clear and type-safe |
Compile-time Checking | Less reliable (runtime errors possible) | Strong compile-time checks |
Usage LINQ | Not supported | Fully supported |
Examples | ArrayList, Hashtable | List<T>,Dictionary<TKey,TValue>, Queue<T> |
Core Concepts of Generic in C#
1. Generic Class
- A generic class in C# lets you define a class with a placeholder (called a type parameter) that works with multiple data types. Think of it like a cookie cutter that can make different shapes while using the same cutter.
Here’s a simple example
using System;
public class Box<T>
{
public T Item { get; set; }
}
// Usage
class Program
{
static void Main(string[] args)
{
var amitBox = new Box<int> { Item = 10 };
Console.WriteLine("${amitBox.Item} is stored in Amit's box.");
var priyaBox = new Box<string> { Item = "Hello, World!" };
Console.WriteLine("${priyaBox.Item} is stored in Priya's box.");
}
}
Output
10 is stored in Amit's box.
Hello, World! is stored in Priya's box.
Explanation
- The Box<T> class allows us to store any type in it. The <T> is a placeholder for the type we define later.
- We create instances of Box<int> and Box<string>, which means Box will only accept integers and strings, respectively.
2. Generic Method
- You can also create generic methods that can handle different data types without repeating code.
using System;
public class Program
{
public void Display<T>(T message)
{
Console.WriteLine(message);
}
static void Main(string[] args)
{
var program = new Program();
program.Display(123);
program.Display("Hello!");
program.Display(45.67);
}
}
Output
123
Hello!
45.67
Explanation
- The Display<T> method can handle any data type by specifying the type parameter <T>.
- In the Main method, the same Display method is used to print an integer, a string, and a double.
3. Generic Interfaces
- Generic interfaces allow you to create flexible and reusable contracts that can be implemented by classes using different data types.
- Let’s explore an example where we define a generic repository interface.
using System;
using System.Collections.Generic;
public interface IRepository<T>
{
void Add(T item);
List<T> GetAll();
}
public class Repository<T> : IRepository<T>
{
private List<T> items = new List<T>();
public void Add(T item) => items.Add(item);
public List<T> GetAll() => items;
}
// Usage
class Program
{
static void Main(string[] args)
{
IRepository<int> intRepo = new Repository<int>();
intRepo.Add(5);
Console.WriteLine("Repository contains:");
foreach (int item in intRepo.GetAll())
{
Console.WriteLine(item);
}
}
}
Output
Repository contains:
5
Explanation
- The IRepository<T> interface defines a contract for adding items and getting all items from a collection.
- The Repository<T> class implements this interface and provides concrete functionality for adding and retrieving items.
- In the Main method, we create a repository for integers and add a value to it, then print the value stored in the repository.
4. Constraints in Generics
Constraints help you limit the types that can be used as type parameters. For instance, you can enforce that a type must be a class or implement a specific interface.using System;
public class Manager<T> where T : class
{
public T Entity { get; set; }
}
Explanation
- In this example, the Manager<T> class uses the where T: class constraint, ensuring that the type parameter T must be a reference type (class).
- This makes sure that only reference types can be used, adding a layer of type safety to the generic class.
Real-Life Examples of Generics
Generics are commonly used in different scenarios:1. Collections and Data Structures
- Generics make collections like List<T>, Dictionary<TKey, TValue>, Queue<T>, and Stack<T> more efficient and type-safe. Let’s take a look:
using System;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
List<int> numbers = new List<int> { 1, 2, 3, 4 };
Dictionary<int, string> students = new Dictionary<int, string>
{
{ 1, "Amit" },
{ 2, "Priya" }
};
Console.WriteLine(numbers[0]);
Console.WriteLine(students[1]);
}
}
Output
1
Amit
Explanation
- The List<int> stores a collection of integers, and the Dictionary<int, string> stores key-value pairs where the keys are integers, and the values are strings.
- We print the first item in the list and the value associated with the key 1 in the dictionary.
2. Database Access
- You can use generics to create reusable methods for accessing databases:
using System;
using System.Collections.Generic;
public class Repository<T>
{
private List<T> items = new List<T>();
public void Add(T item) => items.Add(item);
public List<T> GetAll() => items;
}
// Usage
class Program
{
static void Main(string[] args)
{
Repository<int> intRepository = new Repository<int>();
intRepository.Add(1);
intRepository.Add(2);
List<int> allInts = intRepository.GetAll();
Console.WriteLine("All integers in the repository:");
foreach (int i in allInts)
{
Console.WriteLine(i);
}
}
}
Output
All integers in the repository:
1
2
Explanation
- The Repository<T> class can store and retrieve data of any type <T>.
- In the Main method, we create a repository for integers, add two integers, and retrieve all the integers stored in the repository.
3. Constraints in Generics
- Sometimes, you want to restrict the types that can be used in a generic class or method.
- This is where constraints come into play.
- You can specify that the type parameter must implement a specific interface, inherit a particular class, or have a default constructor.
Let’s look at an example:
using System;
public class Person
{
public string Name { get; set; }
}
public class Employee : Person
{
public int EmployeeId { get; set; }
}
public class EmployeeRepository<T> where T : Person
{
public void Add(T person)
{
Console.WriteLine("${person.Name} added!");
}
}
// Usage
class Program
{
static void Main(string[] args)
{
var employeeRepo = new EmployeeRepository<Employee>();
employeeRepo.Add(new Employee { Name = "John", EmployeeId = 101 });
}
}
Output
John added!
Explanation
- The EmployeeRepository<T> class is constrained by the where T: Person constraint, meaning it only accepts types that inherit from the Person class.
- In the Main method, we add an Employee object to the EmployeeRepository class.
Code Example of Feature of Generic :
1. Type Safety
- Generics provide type safety, ensuring that only a specific type can be used.
- This reduces runtime errors because incorrect types can’t be accidentally added to a generic collection or passed to a method.
Example: Type Safety in Generic List
using System;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
List<int> numberList = new List<int> { 1, 2, 3 };
// numberList.Add("Hello"); // This line will cause a compile-time error
Console.WriteLine(numberList[0]); // Output: 1
}
}
Output
1
Explanation
- You can’t accidentally add a string to numberList because it only accepts integers.
- C# enforces this rule at compile time, catching the error early and preventing potential runtime issues.
2. Performance
- Generics improve performance by avoiding unnecessary boxing and unboxing of value types.
- Boxing occurs when a value type is converted to an object, and unboxing occurs when an object is converted back to a value type.
- Generics eliminate this need, making your code faster.
Example: Avoiding Boxing with Generics
using System;
using System.Collections;
class Program
{
static void Main(string[] args)
{
// Without Generics
ArrayList list = new ArrayList();
list.Add(10); // Boxing happens
int number = (int)list[0]; // Unboxing happens
// With Generics
List<int> genericList = new List<int> { 10 };
int genericNumber = genericList[0]; // No boxing or unboxing
}
}
Explanation
- Using a List<int> avoids boxing and unboxing, which improves memory usage and execution speed.
- Boxing occurs when a value type is converted to an object, and unboxing occurs when an object is converted back to a value type.
3. Binary Code Reuse
- Generics allow you to write code that works with different types without repeating yourself.
- This means less code duplication and easier maintenance.
Example: Reusable Generic Class
using System;
public class Pair<T1, T2>
{
public T1 First { get; set; }
public T2 Second { get; set; }
}
// Usage
class Program
{
static void Main(string[] args)
{
var intStringPair = new Pair<int, string> { First = 1, Second = "One" };
Console.WriteLine(${intStringPair.First}, {intStringPair.Second}); // Output: 1, One
}
}
Output
1, One
Explanation
- You create a generic class Pair<T1, T2> that can hold two related values of different types.
- This allows you to avoid writing separate classes for each possible combination of types.
Role of Collections in Generics: Dictionary, Queue, and Stack
- In C#, collections like Dictionary, Queue, and Stack utilize generics to enhance code flexibility and type safety.
- These data structures allow developers to create type-safe collections that efficiently manage data while minimizing runtime errors.
1. Dictionary in C#
- A Dictionary in C# is a collection that stores key-value pairs.
- It allows fast lookups by key and enforces uniqueness for the keys.
Example: Dictionary Usage
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Dictionary students = new Dictionary
{
{ 1, "Amit" },
{ 2, "Priya" }
};
Console.WriteLine(students[1]); // Output: Amit
Console.WriteLine(students[2]); // Output: Priya
}
}
Output
Amit
Priya
Explanation
- A Dictionary allows you to store key-value pairs. The keys must be unique, and in this example, the key is the student's ID, and the value is their name.
Queues in C#
- A Queue in C# is a first-in, first-out (FIFO) collection that processes items in the order they were added.
Example: Queue Usage
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Queue queue = new Queue();
queue.Enqueue("First");
queue.Enqueue("Second");
queue.Enqueue("Third");
Console.WriteLine(queue.Dequeue()); // Output: First
Console.WriteLine(queue.Dequeue()); // Output: Second
}
}
Output
First
Second
Explanation
- A Queue processes elements in the order they were added.
- Here, items are added using Enqueue() and removed using Dequeue().
3. Stacks in C#
- A Stack in C# is a last-in, first-out (LIFO) collection.
- The last element added is the first one to be removed.
Example: Stack Usage
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Stack stack = new Stack();
stack.Push("First");
stack.Push("Second");
stack.Push("Third");
Console.WriteLine(stack.Pop()); // Output: Third
Console.WriteLine(stack.Pop()); // Output: Second
}
}
Output
Third
Second
Explanation
- A Stack follows a last-in, first-out order.
- Items are added using Push() and removed using Pop().
Difference between Generic and Collection
- Generics: Feature to create reusable and type-safe code.
- Collections: Structures to store and manage groups of data — often built using Generics
Point | Generics in C# | Collections in C# |
Definition | Language feature — allows defining type-safe, reusable code (classes, methods, interfaces) with placeholders for types | Data structures used to store and manage groups of objects (like lists, queues, dictionaries) |
Purpose | To write flexible and type-safe code | To organize and manipulate multiple data elements. |
Examples | T, TKey, TValue, <T> | List, ArrayList, Dictionary, Queue, Stack |
Relation | Can be used within collections (ex: List<T>, Dictionary<TKey, TValue>) | Collections use Generics to provide type-safe storage. |
Type Parameterization | Yes — placeholders like <T> used | Non-generic collections don’t use type parameters |
Use Case | Building reusable libraries, algorithms, utility classes | Managing and storing groups of objects |
Advantages of Generics in C#
- Reusability: You write a generic class or method once, and it works with multiple data types.
- Type Safety: You prevent errors by enforcing type checks during compile time.
- Performance: Generics reduce the need for type conversions, improving speed and efficiency.
Common Pitfalls and Best Practices of Generic in C#
- Overusing Generics: Not every problem requires a generic solution. Use them only where necessary.
- Ignoring Constraints: Applying constraints makes your code safer and easier to use.
Read More: Top 50 C# Interview Questions & Answers To Get Hired |
Conclusion
Generics in C# are an essential feature that empowers you to write flexible, reusable, and type-safe code. Whether you're working with collections, building custom data structures, or implementing methods and classes that function with different data types. Generics help reduce code duplication and improve maintainability. By mastering Generics, you’ll produce cleaner, more efficient, and scalable applications.
FAQs
Take our Csharp skill challenge to evaluate yourself!

In less than 5 minutes, with our skill challenge, you can identify your knowledge gaps and strengths in a given skill.