C# Collections Explained: Lists, Dictionaries, and Queues

If you’ve been working in C# for a while, you’ve probably used collections like List, Dictionary, or maybe even Queue. They’re everywhere — in APIs, data processing, and everyday programming tasks. But have you ever stopped to ask why there are so many collection types, or when one is better than another?

Choosing the right collection can make your code not only faster but also clearer and easier to reason about. In this post, we’ll break down three of the most commonly used collection types in C#: List, Dictionary, and Queue. We’ll look at how they work, what problems they solve, and how to decide which one to use in different situations.

Why We Need Collections

Before collections, there were arrays. Arrays let you hold multiple items of the same type, but they’re rigid — once you create one, its size is fixed. If you need to add, remove, or reorder elements frequently, arrays can get messy fast. You either have to allocate a new one and copy elements over, or resort to unsafe code patterns.

Collections solve that by managing storage dynamically. They grow or shrink automatically as you add or remove elements, and they provide higher-level operations like searching, sorting, and iteration. In short, collections are data structures with flexibility built in.

The .NET Framework includes dozens of collection types, but List<T>, Dictionary<TKey, TValue>, and Queue<T> are among the most useful and commonly encountered.

List<T>: Your Go-To for Ordered Data

If you’ve ever needed a resizable array, List<T> is your best friend. It’s part of System.Collections.Generic, and it behaves just like an array — except it automatically adjusts its capacity behind the scenes.

A List<T> stores elements in a specific order and allows access by index. You can think of it as a dynamic sequence of items where insertion order matters.

Here’s a simple example:

Lists are similar to arrays but can change in size

You can add, remove, insert, or retrieve items by position, and under the hood the list manages an array that resizes when necessary. When it runs out of space, it allocates a larger array and copies existing elements into it. That’s why lists feel flexible — but also why adding thousands of items can sometimes be slower than you expect.

When to Use a List

Use a List<T> whenever you need:

  • To maintain order — the order you add items in is preserved.

  • To access elements by index.

  • To iterate or sort elements frequently.

Lists are perfect for things like user lists, task queues (when order is important but you don’t strictly enforce FIFO), or storing results before transforming them.

Performance Notes

List<T> provides O(1) access by index, meaning it retrieves elements instantly based on position. However, adding or removing elements in the middle of the list can be expensive because everything after that point has to shift. Inserting at the end, though, is typically O(1) — the list just appends and resizes occasionally.

If you find yourself removing or inserting frequently at random positions, you might want to look at LinkedList<T> instead. But for most general use cases, List<T> offers an excellent balance of performance and simplicity.

Dictionary<TKey, TValue>: Fast Lookups by Key

While lists store elements in order, dictionaries store elements by association. A Dictionary<TKey, TValue> is a collection of key–value pairs, where each key must be unique. This makes it ideal when you need to look up values quickly without scanning an entire list.

Imagine you’re managing user data by username. A list would require searching every element to find the right one. A dictionary lets you go straight to it:

Dictionaries are like phone books

You can add or remove entries dynamically:

It’s easy to add and remove entries

If you try to access a key that doesn’t exist, the dictionary throws an exception. To avoid that, use TryGetValue():

A safer way to retrieve entries from a dictionary

When to Use a Dictionary

Use a dictionary when:

  • You need fast lookup of values by key.

  • Keys are unique identifiers (like IDs, usernames, or GUIDs).

  • You don’t care about order — dictionaries don’t maintain insertion order (though OrderedDictionary and SortedDictionary do).

Dictionaries are perfect for caching, configuration storage, or mapping IDs to data objects.

Performance Notes

Dictionary lookups are typically O(1) on average — extremely fast. Internally, they use a hash table, where the key’s hash code determines where the value is stored in memory. Collisions (when two keys share the same hash) are handled automatically.

However, performance depends on the quality of your keys. If your key type doesn’t have a well-distributed GetHashCode() implementation, your dictionary can slow down. When creating custom key types, always override both Equals() and GetHashCode().

Queue<T>: Managing Sequential Data Flow

Unlike List<T> or Dictionary<TKey, TValue>, a queue focuses on how data flows. It follows the First-In, First-Out (FIFO) principle — the first item you add is the first one that comes out.

Think of a queue like a line at a coffee shop. Whoever gets in line first gets served first. That’s exactly how the Queue<T> class behaves.

Queues are like lines at the store- first come first served

When you call Enqueue(), the item goes to the back of the queue. When you call Dequeue(), the item at the front is removed and returned. If you just want to inspect the next item without removing it, Peek() does that.

When to Use a Queue

Use a queue when:

  • Order of processing matters.

  • You need to handle items in the same order they arrive.

  • You’re managing asynchronous or background tasks.

Queues are commonly used in scenarios like:

  • Job scheduling systems (process tasks in the order they were received).

  • Messaging systems (send and receive messages in sequence).

  • Game development (managing player input or event sequences).

Performance Notes

Both Enqueue() and Dequeue() are O(1) operations — very efficient. The queue internally uses a circular buffer to minimize memory movement. However, a queue doesn’t allow random access. You can’t retrieve items by index or key; you can only process them in sequence.

Comparing Lists, Dictionaries, and Queues

Comparing the main collections

Going Beyond the Basics

The .NET ecosystem includes many other specialized collections that build on the same principles as List, Dictionary, and Queue. Each one targets a specific access pattern or performance requirement:

  • Stack<T> — similar to Queue<T>, but follows LIFO (Last-In, First-Out) order. You push items onto the top and pop them off in reverse order.

  • LinkedList<T> — ideal for frequent insertions and deletions, especially in the middle of a collection. It trades random-access speed for structural flexibility.

  • HashSet<T> — a collection that stores unique elements with very fast lookup, insertion, and removal. Unlike List<T>, a HashSet<T> automatically ignores duplicates and doesn’t preserve order. Internally, it uses the same hash-based principles as Dictionary<TKey, TValue>, but only stores keys.

  • SortedList<TKey, TValue> and SortedDictionary<TKey, TValue> — these maintain items in sorted key order, which is useful when you need predictable iteration or binary search capabilities.

  • ConcurrentQueue<T> and ConcurrentDictionary<TKey, TValue> — thread-safe versions of common collections, designed for use in multi-threaded or asynchronous applications.

A HashSet<T> is particularly useful when you care about membership rather than sequence — for example, checking if a value has already been processed or ensuring no duplicates appear in a dataset. Its lookup time is O(1) on average, and it’s often used under the hood in algorithms like graph traversals, where tracking visited nodes efficiently is crucial.

Once you understand List, Dictionary, Queue, and HashSet, the rest of the .NET collection ecosystem starts to make intuitive sense — each new type just optimizes for a specific trade-off between ordering, uniqueness, and access speed.

Wrapping Up

Collections are the backbone of almost every C# program. Understanding how they work isn’t just about syntax — it’s about picking the right tool for the job.

A List<T> gives you flexible, ordered storage.
A Dictionary<TKey, TValue> gives you lightning-fast lookups.
A Queue<T> (or Stack<T> for LIFO) gives you a natural way to handle sequential processing.
And a HashSet<T> ensures uniqueness at blazing speed.

Once you grasp these four, you’ll have a strong foundation for mastering every other collection in .NET. Knowing which one to reach for can make your code cleaner, faster, and easier to maintain — and that’s the kind of knowledge that separates good developers from great ones.

Previous
Previous

Understanding Regular Expressions in C#: A Practical Guide to Powerful Text Matching

Next
Next

Extension Methods in C#: Adding Power Without Changing Code