Back to Blog
C#

C# Dictionary vs List: Choosing the Right Collection

Compare C# Dictionary and List collections by lookup, iteration, memory, and typical access patterns to decide when to use each.

DictionaryListC# CollectionsPerformanceData Structures
A visual comparison of a C# Dictionary and List, showing key-based lookup versus index-based access with a performance scale.

When you need to store and retrieve data in C#, the choice between Dictionary<TKey, TValue> and List<T> often comes down to how you access that data. The decision is not about which collection is better in absolute terms, but about matching the collection's internal behavior to your workload.

A List<T> is an ordered, index-based collection. You access elements by their position, and you can add or remove items at the end in constant time. A Dictionary<TKey, TValue> is a hash-based collection that stores key-value pairs. It offers O(1) average-case lookup by key, but its enumeration order is not guaranteed. Understanding these fundamental differences helps you avoid performance pitfalls and write clearer code.

The Core Difference: Keyed Lookup vs Indexed Access

The most immediate distinction is how you retrieve an element. With a List<T>, you use an integer index:

var fruits = new List<string> { "apple", "banana", "cherry" }; string second = fruits[1]; // "banana"

With a Dictionary<TKey, TValue>, you use a key of any type:

var fruitColors = new Dictionary<string, string> { ["apple"] = "red", ["banana"] = "yellow", ["cherry"] = "dark red" }; string appleColor = fruitColors["apple"]; // "red"

If your data naturally has a unique identifier—like an ID, a name, or a composite key—the dictionary eliminates the need to scan the list to find a match. That scan is what makes list lookup O(n) in the worst case, while dictionary lookup is O(1) on average.

Performance Characteristics: Lookup, Insert, Delete

Performance is the most common reason developers compare these two collections. The underlying data structures dictate the big-O complexity for basic operations.

| Operation | List<T> | Dictionary<TKey, TValue> | | Access by index | O(1) | Not applicable | | Lookup by key | O(n) linear scan | O(1) average, O(n) worst case | | Add at end / add new key | O(1) amortized | O(1) average | | Insert at an index / unordered insertion | O(n) shift | Not applicable (unordered) | | Remove by value | O(n) scan | Not applicable | | Remove by key | Not applicable | O(1) average | | Remove by index | O(n) shift | Not applicable |

A list's index-based access is unbeatable when you know the position. But if you need to find an element by a property, the list forces a linear search. The dictionary trades the ability to access by position for the ability to access by key without scanning.

It is important to note that dictionary lookups can degrade to O(n) if many keys collide in the same hash bucket. In practice, with a well-distributed hash function for the key type, collisions are rare, but you should be aware that the guarantee is average-case, not absolute.

Memory and Allocation Behavior

Both collections are generic, so they store elements directly when T is a value type and references when T is a reference type, but their internal layouts differ. A List<T> uses a contiguous array that grows as needed. When the capacity is exceeded, it allocates a new array and copies elements. This can cause memory spikes during growth, but it also provides excellent cache locality during iteration.

A Dictionary<TKey, TValue> uses an array of buckets plus an entry table. Each entry stores the key, the value, and the hash code, along with bucket-chain information. That layout uses more memory per element than a list, especially for small value types, but it is the price of O(1) key lookup.

If you are storing a large number of simple records and only need sequential access, a list will be more memory-efficient. If you need fast random access by a key, the dictionary's overhead is justified.

Iteration and Ordering Semantics

A List<T> preserves the order in which you add elements. Iterating with foreach or a for loop gives you a deterministic sequence. This is critical when order matters, such as when rendering a UI list or processing items in a specific sequence.

A Dictionary<TKey, TValue> does not guarantee any order. The enumeration order is implementation-defined and can change when you add or remove items. If you rely on the order of a dictionary, your code will be fragile. For example:

var scores = new Dictionary<string, int> { ["alice"] = 90, ["bob"] = 85, ["carol"] = 95 }; foreach (var kvp in scores) { Console.WriteLine($"{kvp.Key}: {kvp.Value}"); }

The output order is not guaranteed to be insertion order. If you need both keyed lookup and insertion order, you can use OrderedDictionary from System.Collections.Specialized, but that adds complexity. Alternatively, maintain a separate List<T> of keys if you need deterministic ordering.

When to Use a Dictionary Over a List

Use a dictionary when your primary operation is looking up a value by a unique key. Typical scenarios include:

  • Configuration settings where each key is a setting name.
  • Caching results keyed by a request ID or URL.
  • Mapping user IDs to user objects in a session store.
  • Counting occurrences where the key is the item and the value is the count.

In these cases, a dictionary avoids the O(n) scan that a list would require. For example, to count word frequencies in a text, a dictionary is the natural choice:

var wordCounts = new Dictionary<string, int>(); foreach (var word in words) { wordCounts.TryGetValue(word, out int count); wordCounts[word] = count + 1; }

This code would be much slower with a list because each increment would require a linear search for the word.

When a List Is the Better Choice

Choose a list when you need to preserve order, access elements by index, or when the collection size is small enough that linear search is negligible. Lists are also better when you need to iterate over all elements frequently and the order matters.

For example, if you are displaying a list of products in a fixed order, a list is straightforward:

var products = new List<Product>(); products.Add(new Product { Name = "Laptop", Price = 1200 }); products.Add(new Product { Name = "Mouse", Price = 25 }); for (int i = 0; i < products.Count; i++) { Console.WriteLine($"{i + 1}. {products[i].Name}"); }

If you only need to check whether an item exists and the collection is small, the linear scan may be fast enough. Prematurely introducing a dictionary for a small, order-sensitive collection adds complexity without measurable benefit.

Practical Example: Lookup-Intensive Code

Consider a method that processes a batch of orders and needs to find the customer for each order. If you have a list of customers and you search by ID inside a loop, the total cost becomes O(n*m), where n is the number of customers and m is the number of orders. With a dictionary, the cost drops to O(m) after the initial build.

// List approach: O(n*m) var customers = GetCustomers(); // List<Customer> foreach (var order in orders) { var customer = customers.FirstOrDefault(c => c.Id == order.CustomerId); // process order } // Dictionary approach: O(n + m) var customerDict = customers.ToDictionary(c => c.Id); foreach (var order in orders) { var customer = customerDict[order.CustomerId]; // process order }

Assuming customer IDs are unique so ToDictionary does not throw, the dictionary version builds the lookup structure once and then each order lookup is constant time. This pattern is common in data processing and is a clear win for the dictionary.

Common Pitfalls and Edge Cases

One common mistake is using a mutable type as a dictionary key. If a key type computes its hash code from fields that can change after insertion, the dictionary may not be able to find that entry later. Prefer immutable keys, or at least ensure the key's hash code is stable.

Another issue is the KeyNotFoundException when you access a dictionary with a missing key. Prefer TryGetValue over ContainsKey followed by an indexer to avoid exceptions and a second lookup:

if (dictionary.TryGetValue(key, out var value)) { // use value } else { // handle missing key }

For lists, a common pitfall is removing items while iterating. Modifying the list inside a foreach loop throws an InvalidOperationException. Use a for loop backwards or List<T>.RemoveAll to safely remove elements.

Finally, consider the impact of value types versus reference types. A dictionary that stores value types copies the values on each access, which can be a performance concern for large structs. A list also copies value-type elements when you read them, so the copying behavior is similar. The real difference is the internal storage: the dictionary's entry table adds per-entry overhead, which can matter when you have many small entries.

Understanding how these collections behave internally helps you make an informed choice. The decision is not about one collection being superior; it is about matching each collection's strengths to your access patterns. For keyed lookups, use a dictionary. For ordered, index-based access, use a list. When performance matters, measure your specific workload, but the algorithmic differences described here will guide you in the right direction.

C# Dictionary vs List: How to Choose | RYUSLOG DEV