Collections and Generics in .NET
refrence
Contents
4. Why generics are important 4
8. Dictionary<TKey, TValue> 21
What each collection is doing 42
18. Quick reference: important methods and properties 43
19. Choosing the correct collection 46
Collections and Generics in .NET
1. What is a collection?
A collection is used to store multiple values or objects in one place.
Simple analogy
A normal variable is like one box.
A collection is like a ** cupboard containing many boxes**.
Each type of collection organizes those boxes differently.
For example:
List<T> is like a shopping list.
Dictionary<TKey, TValue> is like a phone book.
HashSet<T> is like a guest list that does not allow duplicate names.
Queue<T> is like a waiting line.
Stack<T> is like a pile of plates.
Without a collection
csharp
string employee1 = "Anita";
string employee2 = "Rahul";
string employee3 = "John";
This becomes difficult when there are hundreds of employees.
With a collection
csharp
List<string> employees = new List<string>
{
"Anita",
"Rahul",
"John"
};
Now you can easily add, remove, search and loop through employees.
csharp
employees.Add("Priya");
employees.Remove("John");
foreach (string employee in employees)
{
Console.WriteLine(employee);
}
2. Why collections are used
Collections are used when an application needs to manage groups of data.
Common industry examples include:
Employees returned from a database
Products displayed on an e-commerce page
Orders waiting for processing
User roles and permissions
Cached records
API response data
Background jobs
Unique IDs that have already been processed
3. What are generics?
Generics allow you to specify the type of data that a class, method or collection can work with.
Simple analogy
A generic collection is like a container with a label:
List<int> means: this list stores integers.
List<string> means: this list stores strings.
List<Employee> means: this list stores Employee objects.
csharp
List<int> numbers = new List<int>();
List<string> names = new List<string>();
List<Employee> employees = new List<Employee>();
The type inside < > is called the type parameter.
4. Why generics are important
Generics provide:
Type safety: only the correct type can be added.
Less casting: values are returned in their actual type.
Reusable code: the same code works with different types.
Better maintainability: the intended data type is clear.
Better performance: avoids unnecessary boxing and unboxing.
Without generics
csharp
ArrayList values = new ArrayList();
values.Add(100);
values.Add("Hello");
// This compiles, but fails at runtime
int number = (int)values[1];
The collection allowed both integers and strings, which caused a runtime error.
With generics
csharp
List<int> values = new List<int>();
values.Add(100);
// values.Add("Hello"); // Compile-time error
The compiler prevents invalid data before the application runs.
In modern .NET applications, prefer generic collections such as List<T> and Dictionary<TKey, TValue> over older collections such as ArrayList and Hashtable.
5. Main collection types
6. Array
An array stores items of the same type and has a fixed size.
Analogy
An array is like a row of fixed seats. You can change who sits in a seat, but you cannot add more seats after creating the array.
csharp
int[] marks = { 70, 40, 90, 60 };
Important array property and members
Add and access values
Arrays cannot grow using Add(). Values are assigned using an index.
csharp
int[] marks = new int[3];
marks[0] = 80;
marks[1] = 90;
marks[2] = 70;
Console.WriteLine(marks[0]); // 80
Console.WriteLine(marks.Length); // 3
Sort and search
csharp
int[] marks = { 70, 40, 90, 60 };
Array.Sort(marks);
// 40, 60, 70, 90
int position = Array.IndexOf(marks, 70);
// position is 2
bool hasFailedMark = Array.Exists(
marks,
mark => mark < 50);
// True
When to use an array
Use an array when:
The size is fixed.
You need simple index-based access.
You are working with fixed configuration values.
You are processing numeric or low-level data.
7. List<T>
List<T> is a dynamic, ordered collection.
It is one of the most commonly used collections in .NET applications.
Analogy
A List<T> is like a shopping bag. You can add and remove items whenever required.
csharp
List<string> employees = new List<string>
{
"Anita",
"Rahul",
"John"
};
Important List<T> properties
Count
Use Count to know how many items are currently in the list.
csharp
List<string> employees = new List<string>
{
"Anita",
"Rahul",
"John"
};
Console.WriteLine(employees.Count); // 3
Count changes when items are added or removed.
Capacity
Capacity is the amount of internal storage currently available.
csharp
Console.WriteLine(employees.Capacity);
Normally, you do not need to manage Capacity manually. It becomes useful when optimizing very large lists.
Indexer
Use an index to read or update an item.
csharp
Console.WriteLine(employees[0]); // Anita
employees[1] = "Priya";
Console.WriteLine(employees[1]); // Priya
Indexes start at 0.
Important List<T> methods
Add()
Adds one item to the end of the list.
csharp
List<string> skills = new List<string>();
skills.Add("C#");
skills.Add("SQL");
Console.WriteLine(skills.Count); // 2
Application
Use Add() when a new product, employee, order or item is created.
csharp
List<string> products = new List<string>();
products.Add("Laptop");
products.Add("Mouse");
AddRange()
Adds multiple items at once.
csharp
List<string> skills = new List<string>();
skills.AddRange(new List<string>
{
"C#",
"SQL",
"ASP.NET Core"
});
You can also use an array:
csharp
skills.AddRange(new[]
{
"Azure",
"Docker"
});
Application
Use AddRange() when adding:
Database results
API response items
Multiple products
Multiple permissions
Insert()
Adds an item at a specific position.
csharp
List<string> priorities = new List<string>
{
"Medium",
"Low"
};
priorities.Insert(0, "High");
foreach (string priority in priorities)
{
Console.WriteLine(priority);
}
Output:
text
High
Medium
Low
Application
Use Insert() when the position of the new item matters.
Remove()
Removes the first matching value.
csharp
List<string> users = new List<string>
{
"Anita",
"Rahul",
"John",
"Rahul"
};
users.Remove("Rahul");
Only the first matching "Rahul" is removed.
Important point
Remove() returns a Boolean:
csharp
bool removed = users.Remove("John");
Console.WriteLine(removed); // True
If the item does not exist:
csharp
bool removed = users.Remove("Meera");
Console.WriteLine(removed); // False
RemoveAt()
Removes an item using its index.
csharp
List<string> users = new List<string>
{
"Anita",
"Rahul",
"John"
};
users.RemoveAt(1);
foreach (string user in users)
{
Console.WriteLine(user);
}
Output:
text
Anita
John
Important warning
The index must be valid. Otherwise, an exception occurs.
csharp
// users.RemoveAt(10); // ArgumentOutOfRangeException
RemoveAll()
Removes every item that matches a condition.
csharp
List<int> numbers = new List<int>
{
10, 15, 20, 25, 30
};
numbers.RemoveAll(number => number > 20);
Remaining values:
text
10
15
20
Application
Use RemoveAll() to:
Remove inactive users
Remove cancelled orders
Remove expired records
Remove values matching a business rule
csharp
employees.RemoveAll(employee => !employee.IsActive);
Contains()
Checks whether a value exists.
csharp
List<string> roles = new List<string>
{
"Admin",
"Manager",
"Employee"
};
bool hasAdminRole = roles.Contains("Admin");
Console.WriteLine(hasAdminRole); // True
Application
Use Contains() to check:
Whether a role exists
Whether a product is in a cart
Whether a permission is assigned
Whether an ID has already been added
IndexOf()
Returns the position of the first matching value.
csharp
List<string> roles = new List<string>
{
"Admin",
"Manager",
"Employee"
};
int position = roles.IndexOf("Manager");
Console.WriteLine(position); // 1
If the value does not exist, it returns -1.
csharp
int position = roles.IndexOf("Guest");
Console.WriteLine(position); // -1
Find()
Returns the first object that matches a condition.
csharp
List<Employee> employees = new List<Employee>
{
new Employee
{
Id = 101,
Name = "Anita",
IsActive = true
},
new Employee
{
Id = 102,
Name = "Rahul",
IsActive = false
}
};
Employee? employee = employees.Find(
employee => employee.Id == 102);
Console.WriteLine(employee?.Name); // Rahul
If no item matches, Find() returns null for a reference type.
FindAll()
Returns all objects that match a condition.
csharp
List<Employee> activeEmployees = employees.FindAll(
employee => employee.IsActive);
foreach (Employee employee in activeEmployees)
{
Console.WriteLine(employee.Name);
}
Application
Use FindAll() for:
Active employees
Products in a category
Orders with a status
Users with a specific role
Sort()
Sorts the list in ascending order.
csharp
List<int> scores = new List<int>
{
80, 50, 95, 60
};
scores.Sort();
Result:
text
50
60
80
95
For objects, provide a comparison rule:
csharp
employees.Sort(
(first, second) =>
first.Name.CompareTo(second.Name));
Clear()
Removes all items.
csharp
List<string> cart = new List<string>
{
"Laptop",
"Mouse"
};
cart.Clear();
Console.WriteLine(cart.Count); // 0
Application
Use Clear() to:
Empty a shopping cart
Reset a temporary list
Clear a batch after processing
Remove all selected items
ToArray()
Converts a list into an array.
csharp
List<int> numbers = new List<int>
{
10, 20, 30
};
int[] numberArray = numbers.ToArray();
Application
Use ToArray() when another API, library or method requires an array.
Practical List<T> example
csharp
List<Employee> employees = new List<Employee>();
employees.Add(new Employee
{
Id = 101,
Name = "Anita",
IsActive = true
});
employees.AddRange(new List<Employee>
{
new Employee
{
Id = 102,
Name = "Rahul",
IsActive = false
},
new Employee
{
Id = 103,
Name = "John",
IsActive = true
}
});
Employee? selectedEmployee = employees.Find(
employee => employee.Id == 102);
List<Employee> activeEmployees = employees.FindAll(
employee => employee.IsActive);
employees.RemoveAll(
employee => !employee.IsActive);
This example demonstrates:
Adding one item
Adding multiple items
Finding an item
Finding multiple items
Removing items based on a condition
8. Dictionary<TKey, TValue>
A dictionary stores data as key-value pairs.
Analogy
A phone book:
Name or phone number = key
Contact details = value
csharp
Dictionary<int, string> employees =
new Dictionary<int, string>
{
{ 101, "Anita" },
{ 102, "Rahul" },
{ 103, "John" }
};
In this example:
int is the key type.
string is the value type.
Each key must be unique.
Important dictionary properties
Access using a key
csharp
string employeeName = employees[101];
Console.WriteLine(employeeName); // Anita
Important warning
If the key does not exist, direct access throws an exception.
csharp
// string name = employees[999];
// KeyNotFoundException
Important dictionary methods
Add()
Adds a new key-value pair.
csharp
Dictionary<int, string> products =
new Dictionary<int, string>();
products.Add(1, "Laptop");
products.Add(2, "Mouse");
If the key already exists, Add() throws an exception.
csharp
// products.Add(1, "Keyboard");
// ArgumentException: duplicate key
TryAdd()
Adds a pair only if the key does not already exist.
csharp
bool added = products.TryAdd(1, "Keyboard");
Console.WriteLine(added); // False
Use TryAdd() when duplicate keys are possible and you do not want an exception.
ContainsKey()
Checks whether a key exists.
csharp
if (products.ContainsKey(2))
{
Console.WriteLine("Product exists");
}
Application
Use it before accessing a key when you want to check existence explicitly.
TryGetValue()
Safely gets a value using a key.
csharp
if (products.TryGetValue(2, out string? productName))
{
Console.WriteLine(productName);
}
else
{
Console.WriteLine("Product not found");
}
Why it is useful
TryGetValue():
Avoids KeyNotFoundException
Performs lookup and retrieval together
Is commonly used in production code
Remove()
Removes a key-value pair using its key.
csharp
bool removed = products.Remove(2);
Console.WriteLine(removed); // True
If the key does not exist:
csharp
bool removed = products.Remove(999);
Console.WriteLine(removed); // False
Clear()
Removes all key-value pairs.
csharp
products.Clear();
Console.WriteLine(products.Count); // 0
Updating a value
You can update a value using the indexer.
csharp
products[1] = "Gaming Laptop";
If key 1 exists, its value is updated.
If key 5 does not exist, this syntax creates a new pair:
csharp
products[5] = "Keyboard";
Reading keys and values
csharp
foreach (int productId in products.Keys)
{
Console.WriteLine(productId);
}
foreach (string productName in products.Values)
{
Console.WriteLine(productName);
}
Read both key and value:
csharp
foreach (KeyValuePair<int, string> product in products)
{
Console.WriteLine(
$"Id: {product.Key}, Name: {product.Value}");
}
Practical dictionary example
csharp
Dictionary<int, Employee> employeeCache =
new Dictionary<int, Employee>();
employeeCache.Add(101, new Employee
{
Id = 101,
Name = "Anita",
IsActive = true
});
if (employeeCache.TryGetValue(
101,
out Employee? employee))
{
Console.WriteLine(employee.Name);
}
employeeCache[101].IsActive = false;
employeeCache.Remove(101);
Industry use
Dictionaries are commonly used for:
Caching users by ID
Finding products by SKU
Mapping country codes
Storing configuration settings
Mapping error codes to messages
Counting occurrences
9. HashSet<T>
A HashSet<T> stores unique values.
Analogy
A guest list allows each person to appear only once.
csharp
HashSet<string> skills = new HashSet<string>();
skills.Add("C#");
skills.Add("SQL");
skills.Add("C#");
Console.WriteLine(skills.Count); // 2
The duplicate "C#" is ignored.
Important HashSet<T> members
Duplicate check using Add()
csharp
HashSet<int> processedOrderIds =
new HashSet<int>();
if (processedOrderIds.Add(5001))
{
Console.WriteLine("Process order");
}
else
{
Console.WriteLine("Order already processed");
}
Add() returns:
true if the value was added
false if it already existed
Set comparison
csharp
HashSet<string> userRoles =
new HashSet<string>
{
"Employee",
"Manager"
};
HashSet<string> requiredRoles =
new HashSet<string>
{
"Manager",
"Admin"
};
userRoles.IntersectWith(requiredRoles);
Console.WriteLine(
userRoles.Contains("Manager")); // True
Industry use
Use HashSet<T> for:
Unique roles
Unique permissions
Processed message IDs
Unique email addresses
Tags
Duplicate prevention
10. Queue<T>
A queue follows FIFO:
First In, First Out
Analogy
A queue at a ticket counter. The first person who joins is served first.
csharp
Queue<string> supportTickets =
new Queue<string>();
supportTickets.Enqueue("Ticket-101");
supportTickets.Enqueue("Ticket-102");
supportTickets.Enqueue("Ticket-103");
Important queue members
Enqueue(), Peek() and Dequeue()
csharp
Queue<string> jobs = new Queue<string>();
jobs.Enqueue("Job-101");
jobs.Enqueue("Job-102");
Console.WriteLine(jobs.Peek());
// Job-101; still remains in the queue
string nextJob = jobs.Dequeue();
Console.WriteLine(nextJob);
// Job-101; removed from the queue
Safe dequeue
Dequeue() throws an exception if the queue is empty.
csharp
if (jobs.Count > 0)
{
string job = jobs.Dequeue();
Console.WriteLine(job);
}
Industry use
Use queues for:
Background jobs
Email processing
Notification processing
Support tickets
Print requests
Order processing
11. Stack<T>
A stack follows LIFO:
Last In, First Out
Analogy
A stack of plates. The last plate placed on top is removed first.
csharp
Stack<string> undoActions =
new Stack<string>();
undoActions.Push("Add product");
undoActions.Push("Update quantity");
undoActions.Push("Apply discount");
Important stack members
Push(), Peek() and Pop()
csharp
Console.WriteLine(undoActions.Peek());
// Apply discount
string lastAction = undoActions.Pop();
Console.WriteLine(lastAction);
// Apply discount
Safe pop
csharp
if (undoActions.Count > 0)
{
string action = undoActions.Pop();
Console.WriteLine(action);
}
Industry use
Use stacks for:
Undo and redo
Browser history
Backtracking
Expression evaluation
Navigation history
12. LinkedList<T>
A linked list stores data in connected nodes.
Analogy
A train where each coach is connected to the next coach.
csharp
LinkedList<string> workflow =
new LinkedList<string>();
workflow.AddLast("Development");
workflow.AddLast("Testing");
Important members
Insert before an existing node
csharp
LinkedListNode<string>? testingNode =
workflow.Find("Testing");
if (testingNode != null)
{
workflow.AddBefore(
testingNode,
"Code Review");
}
The workflow becomes:
text
Development
Code Review
Testing
When to use
Use LinkedList<T> when:
Frequent insertion or removal in the middle is required.
You already have a reference to a node.
Index-based access is not important.
For most business applications, List<T> is simpler and more common.
13. Generic interfaces
Interfaces describe what a collection can do.
IEnumerable<T>
Use IEnumerable<T> when a method only needs to read or loop through data.
csharp
public void PrintNames(
IEnumerable<string> names)
{
foreach (string name in names)
{
Console.WriteLine(name);
}
}
This method can accept:
List<string>
string[]
HashSet<string>
Other enumerable collections
Key point
IEnumerable<T> does not guarantee that you can add or remove items.
ICollection<T>
Use ICollection<T> when basic add, remove and count operations are required.
csharp
public void AddDepartment(
ICollection<string> departments)
{
departments.Add("Finance");
}
It supports commonly used members such as:
Add()
Remove()
Contains()
Count
Clear()
IList<T>
Use IList<T> when order and index-based access are required.
csharp
public void UpdateCity(
IList<string> cities)
{
cities[0] = "Mumbai";
}
It supports:
Index access
Add()
Remove()
Insert()
IndexOf()
IDictionary<TKey, TValue>
Use this interface when the data has keys and values.
csharp
public string? FindEmployee(
IDictionary<int, string> employees,
int employeeId)
{
employees.TryGetValue(
employeeId,
out string? name);
return name;
}
Using interfaces makes methods more flexible because the caller can provide different implementations.
14. Generic classes
A generic class can work with different types.
csharp
public class Box<T>
{
public T Value { get; set; } = default!;
}
Usage:
csharp
Box<int> numberBox = new Box<int>
{
Value = 100
};
Box<string> textBox = new Box<string>
{
Value = "Hello"
};
The same class works for integers, strings, employees or products.
15. Generic methods
A generic method can work with different types.
csharp
public static void PrintValue<T>(T value)
{
Console.WriteLine(value);
}
Usage:
csharp
PrintValue(100);
PrintValue("Hello");
PrintValue(true);
C# identifies the type automatically in most cases.
16. Generic constraints
Constraints restrict which types can be used with a generic class or method.
class constraint
csharp
public class Service<T>
where T : class
{
}
new() constraint
csharp
public class Factory<T>
where T : new()
{
public T Create()
{
return new T();
}
}
This works only when T has a public parameterless constructor.
17. Industry example
Consider an e-commerce order system.
csharp
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; } = "";
public decimal Amount { get; set; }
}
Different collections can solve different requirements:
csharp
List<Order> allOrders =
new List<Order>();
Dictionary<int, Order> ordersById =
new Dictionary<int, Order>();
Queue<Order> pendingOrders =
new Queue<Order>();
HashSet<int> processedOrderIds =
new HashSet<int>();
Add an order:
csharp
Order order = new Order
{
Id = 5001,
CustomerName = "Anita",
Amount = 2500
};
allOrders.Add(order);
ordersById[order.Id] = order;
pendingOrders.Enqueue(order);
Process the next order:
csharp
if (pendingOrders.Count > 0)
{
Order nextOrder =
pendingOrders.Dequeue();
if (processedOrderIds.Add(nextOrder.Id))
{
Console.WriteLine(
$"Processing order {nextOrder.Id}");
}
else
{
Console.WriteLine(
"Order was already processed");
}
}
Find an order quickly:
csharp
if (ordersById.TryGetValue(
5001,
out Order? foundOrder))
{
Console.WriteLine(
foundOrder.CustomerName);
}
What each collection is doing
18. Quick reference: important methods and properties
Array
List<T>
Dictionary<TKey, TValue>
HashSet<T>
Queue<T>
Stack<T>
19. Choosing the correct collection
20. Final summary
A collection stores multiple values or objects.
Generics make collections and classes type-safe and reusable.
Use an array when the size is fixed.
Use List<T> for normal dynamic and ordered data.
Use Dictionary<TKey, TValue> for fast lookup by a unique key.
Use HashSet<T> when duplicate values are not allowed.
Use Queue<T> when the oldest item must be processed first.
Use Stack<T> when the newest item must be processed first.
Use LinkedList<T> when frequent insertion or removal between nodes is required.
Use IEnumerable<T> when a method only needs to read or iterate.
Use TryGetValue() for safe dictionary lookup.
Use Any() or Contains() for existence checks.
Use Find() or FindAll() to search a List<T>.
Use Count to know the number of items in most collections.
Use Length for arrays.
Use Add() to insert one item.
Use AddRange() to insert several items.
Use Remove() to remove a matching value.
Use RemoveAt() to remove by position.
Use RemoveAll() to remove items matching a condition.
Use Clear() to empty a collection.
Comments
Post a Comment