C# List vs Dictionary: Choosing the Right Collection
c# list vs dictionary: Compare C# List and Dictionary for lookups, iteration, and memory. Learn when each collection fits best with code examples.
When you need to store a sequence of items and access them by position, a List<T> is the natural choice. When you need to retrieve values by a unique key, a Dictionary<TKey, TValue> provides direct lookup. The decision between C# List vs Dictionary comes down to how you access data.
What Each Collection Is Optimized For
List<T> is a dynamically sized array. It stores elements contiguously in memory and gives you index-based access in constant time. You can add, remove, and insert elements, but those operations may shift other elements. The collection is optimized for scenarios where you iterate over all elements or access them by an integer index.
Dictionary<TKey, TValue> is a hash table. It stores key-value pairs and uses the key's hash code to determine where the value lives. Lookup by key is typically O(1) on average, but the collection does not preserve insertion order. It is optimized for scenarios where you need to find a value quickly given a unique key.
The two collections serve different access patterns. Choosing one without considering how you will query the data leads to unnecessary complexity or poor performance.
Lookup Performance: Index vs Key
The most significant difference between List<T> and Dictionary<TKey, TValue> is how lookup behaves.
With a List<T>, accessing an element by index is O(1):
var numbers = new List<int> { 10, 20, 30, 40 }; int third = numbers[2]; // O(1)
But finding an element by value requires a linear scan unless you use a helper like IndexOf, which is O(n):
int index = numbers.IndexOf(30); // O(n) worst case
With a Dictionary<TKey, TValue>, retrieving a value by key is O(1) on average:
var map = new Dictionary<string, int> { ["apple"] = 1, ["banana"] = 2 }; int value = map["apple"]; // O(1)
If you need to check whether a key exists without throwing, use TryGetValue:
if (map.TryGetValue("banana", out int count)) { Console.WriteLine(count); }
This distinction matters when you repeatedly search for items. A List<T> is efficient when you know the index or when the collection is small enough that a linear scan is acceptable. A Dictionary<TKey, TValue> is the right tool when you frequently look up values by a non-numeric key.
Iteration and Ordering
List<T> preserves the order in which elements are added. Iterating with foreach or a for loop returns elements in that order. This is essential when the sequence has semantic meaning, such as a queue of tasks or a sorted list of names.
Dictionary<TKey, TValue> does not guarantee any order. The underlying hash table arranges entries based on hash codes, and that arrangement can change when the dictionary is resized. In practice, the current .NET implementation often preserves insertion order until a removal occurs, but that behavior is not part of the contract. Relying on it is fragile.
If you need both key-based lookup and deterministic ordering, you have two options. You can keep a separate List<TKey> that tracks the insertion order, or you can use a SortedDictionary<TKey, TValue> which orders by key comparison. The latter gives you O(log n) lookup and ordered iteration, but at a higher cost than a plain dictionary.
Memory and Allocation Considerations
A List<T> stores elements in a single contiguous array. When the array is full, adding a new element triggers a resize that allocates a new array and copies the existing elements. This overhead is amortized, but it can cause temporary memory spikes for large lists.
A Dictionary<TKey, TValue> stores entries in an array of buckets, each entry holding the key, value, and a hash code. The per-entry overhead is larger than a list because of the hash code and the bucket structure. Additionally, the key itself is stored, so if you use a reference type as a key, you keep a reference to that object.
For small collections, the difference is negligible. For large collections, a List<T> uses less memory per element and is more cache-friendly because of its contiguous layout. A Dictionary<TKey, TValue> uses more memory but gives you fast key-based access.
If memory is a primary concern and you only need index-based access, a List<T> is the better choice. If you need to map keys to values, the dictionary's overhead is the price you pay for that capability.
When to Use a List
Use a List<T> when:
- You need to access elements by an integer index.
- You want to preserve insertion order.
- You primarily iterate over the entire collection.
- The collection size is small enough that linear search is acceptable.
- You need to insert or remove elements at arbitrary positions (though this is O(n) for a list).
A list is also the natural choice when you are building a collection from a stream of data and later need to process it sequentially.
var logEntries = new List<LogEntry>(); foreach (var line in ReadLogLines()) { logEntries.Add(Parse(line)); } // Later, iterate in order foreach (var entry in logEntries) { Process(entry); }
When to Use a Dictionary
Use a Dictionary<TKey, TValue> when:
- You need to look up values by a unique key.
- You want to enforce key uniqueness.
- The number of lookups is large relative to the number of insertions.
- You do not care about the order of elements.
A dictionary is ideal for caching, configuration maps, or any scenario where you have a natural identifier for each value.
var userCache = new Dictionary<int, User>(); if (userCache.TryGetValue(userId, out var user)) { return user; } var loaded = LoadUserFromDatabase(userId); userCache[userId] = loaded; return loaded;
Here the key is the user ID, and the value is the full user object. The dictionary gives you O(1) access without scanning the entire cache.
Choosing Between List and Dictionary in Practice
| Criterion | List<T> | Dictionary<TKey, TValue> |
|---|---|---|
| Access by index | O(1) | Not supported |
| Lookup by value/key | O(n) linear scan | O(1) average for key lookup |
| Ordering | Preserves insertion order | No guaranteed order |
| Memory per element | Lower (contiguous array) | Higher (hash table overhead) |
| Typical use case | Sequences, ordered data | Key-value mapping, caching |
Start by asking what you need to do with the collection. If you know the index, use a list. If you know a key, use a dictionary. If you need both, consider whether you can maintain a list of keys alongside a dictionary, or whether a SortedDictionary fits your ordering requirements.
Common Pitfalls and Edge Cases
One common mistake is using a List<T> when the collection is large and you frequently search by a non-numeric property. A linear scan becomes expensive as the list grows. Convert to a dictionary when the lookup key is stable and unique.
Another pitfall is using a mutable object as a dictionary key. If the object's hash code changes after it is added, the dictionary will no longer find it. Keys should be immutable or at least have a stable hash code.
Also note that Dictionary<TKey, TValue> does not allow duplicate keys. If you need to store multiple values under the same key, use a List<TValue> as the value or use a different structure like Lookup<TKey, TValue>.
When you add to a List<T> and the capacity is exceeded, the list reallocates. If you know the final size in advance, pass the capacity to the constructor to avoid repeated resizes:
var items = new List<Item>(expectedCount);
For a dictionary, you can similarly provide an initial capacity to reduce the number of resizes when you know the approximate number of entries.
Finally, remember that Dictionary<TKey, TValue> uses the default equality comparer unless you provide one. If you need case-insensitive string keys, use StringComparer.OrdinalIgnoreCase in the constructor:
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
This avoids subtle bugs where "Name" and "name" are treated as different keys.
The choice between C# List vs Dictionary is not about which is better overall, but which matches the access pattern you need. Evaluate how you will query the data, whether order matters, and what memory constraints you have. That analysis leads to the right collection for your specific scenario.