Generic Collections Deep Dive
Vaibhav • September 11, 2025
In previous articles, we explored how generics allow us to write reusable and type-safe code, and how constraints help us enforce rules on generic types. Now it’s time to dive into one of the most practical applications of generics: generic collections. These are the backbone of modern C# programming, offering flexible, efficient, and strongly typed data structures that adapt to your needs.
Why Generic Collections Matter
Before generics, collections like ArrayList
stored elements as object
, which meant you had to cast items manually and risk runtime errors.
Generic collections solve this by letting you specify the type of elements they store. This improves type
safety, performance, and developer productivity.
// Non-generic collection
ArrayList list = new ArrayList();
list.Add("Hello");
string s = (string)list[0]; // Risky: requires casting
// Generic collection
List<string> list = new List<string>();
list.Add("Hello");
string s = list[0]; // Safe: no casting needed
The second example is safer and cleaner. The compiler ensures that only strings can be added to the list, preventing accidental misuse.
List<T> - The Workhorse
The List<T>
class is the most commonly used generic collection. It
behaves like a dynamic array that resizes automatically and provides powerful built-in methods.
List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
numbers.Add(30);
Console.WriteLine(numbers[1]); // Output: 20
You can add, remove, insert, and access elements using intuitive methods. The list maintains order and supports indexing, making it ideal for most everyday scenarios.
List<T>
belongs to the System.Collections.Generic
namespace. It uses a backing array that doubles in
size when capacity is exceeded.
Dictionary<K,V> - Key-Value Storage
The Dictionary<K,V>
class stores data as key-value pairs. It’s optimized
for fast lookups, insertions, and deletions using hashing.
Dictionary<string, int> scores = new Dictionary<string, int>();
scores["Alice"] = 95;
scores["Bob"] = 87;
Console.WriteLine(scores["Alice"]); // Output: 95
Keys must be unique, and the dictionary throws an exception if you try to access a missing key. You can use
ContainsKey
to check safely.
if (scores.ContainsKey("Charlie"))
{
Console.WriteLine(scores["Charlie"]);
}
Dictionaries are perfect for mapping identifiers to values, such as usernames to profiles or product IDs to prices.
HashSet<T> - Unique Elements
The HashSet<T>
class stores unique elements and provides fast lookup and
set operations. It’s ideal when you care about existence but not order.
HashSet<string> tags = new HashSet<string>();
tags.Add("csharp");
tags.Add("generics");
tags.Add("csharp"); // Duplicate ignored
Console.WriteLine(tags.Count); // Output: 2
HashSet automatically ignores duplicates and supports operations like union, intersection, and difference.
HashSet<T>
uses hashing internally, just like Dictionary<K,V>
, but only stores values-not key-value pairs.
Queue<T> and Stack<T> - Ordered Access
These two collections provide specialized access patterns:
Queue<T> follows First-In-First-Out (FIFO) ordering. You enqueue items at the end and dequeue from the front.
Queue<string> queue = new Queue<string>();
queue.Enqueue("First");
queue.Enqueue("Second");
Console.WriteLine(queue.Dequeue()); // Output: First
Stack<T> follows Last-In-First-Out (LIFO) ordering. You push items onto the top and pop them off.
Stack<string> stack = new Stack<string>();
stack.Push("Bottom");
stack.Push("Top");
Console.WriteLine(stack.Pop()); // Output: Top
These collections are useful for modeling workflows, undo operations, or buffering tasks.
Collection Initialization Syntax
You can initialize collections using collection initializer syntax, which makes code cleaner and more readable.
List<int> numbers = new List<int> { 1, 2, 3 };
Dictionary<string, int> ages = new Dictionary<string, int>
{
["Alice"] = 30,
["Bob"] = 25
};
This syntax is especially useful when setting up test data or configuration values.
Iterating Through Collections
All generic collections implement IEnumerable<T>
, so you can use foreach
to iterate over them.
foreach (var name in names)
{
Console.WriteLine(name);
}
This works for List<T>
, HashSet<T>
, Queue<T>
, and more. The
iteration order depends on the collection type.
Performance Considerations
Generic collections are optimized for performance, but each has trade-offs:
List<T>
offers fast indexing and appending, but inserting/removing in the
middle is slower due to shifting elements.
Dictionary<K,V>
and HashSet<T>
offer fast lookups, but require good hash functions and more memory.
Queue<T>
and Stack<T>
are fast for
their specific access patterns but not suitable for random access.
Choose the right collection based on your access pattern. Use List<T>
for ordered data, Dictionary<K,V>
for key-based lookup, and HashSet<T>
for uniqueness.
Collection Interfaces
Most generic collections implement interfaces like ICollection<T>
, IEnumerable<T>
, and IDictionary<K,V>
. These interfaces define common behaviors and allow
polymorphism.
ICollection<string> items = new List<string>();
items.Add("Item");
items.Remove("Item");
This lets you write code that works with any collection type, as long as it implements the expected interface.
Collections and LINQ
Generic collections work seamlessly with LINQ. You can filter, project, and aggregate data using expressive queries.
var highScores = scores.Where(s => s.Value > 90)
.Select(s => s.Key);
LINQ makes it easy to work with collections in a declarative style, improving readability and reducing boilerplate.
Common Mistakes with Collections
While generic collections are powerful, they can be misused:
Avoid modifying a collection while iterating over it-this causes runtime exceptions. Instead, collect changes in a separate list and apply them after iteration.
Don’t assume order in Dictionary<K,V>
or HashSet<T>
-they are unordered by default.
Be cautious with Contains
and Remove
-they rely
on Equals
and GetHashCode
, so override these
methods in custom types.
Summary
Generic collections are essential tools in C#. They provide type-safe, efficient, and flexible data structures
for storing and manipulating data. You’ve learned how to use List<T>
,
Dictionary<K,V>
, HashSet<T>
, Queue<T>
, and Stack<T>
, and how to
choose the right one for your needs. You’ve also seen how to initialize, iterate, and optimize collections, and
how they integrate with LINQ and interfaces. In the next article, we’ll explore Generic
Interfaces-how to define and implement interfaces that work across types, and how they enable
powerful abstractions in your codebase.