Collections
Use TryGetValue instead of ContainsKey + indexer, and TryAdd to avoid exceptions for duplicate keys.
Concept
Growable array, indexable Index O(1), append amortized O(1), insert/remove O(n) Hash table key → value Lookup/add/remove O(1) average Unique items, set operations Contains/add O(1) average First in, first out Enqueue/dequeue O(1) Last in, first out Doubly linked nodes Insert/remove at a node O(1), find O(n)
Unique items kept sorted (red-black tree) Keys kept sorted (tree)
Sorted arrays of keys and values; index access Lookup O(log n), insert O(n)
Use TryGetValue instead of ContainsKey + indexer, and TryAdd to avoid exceptions for duplicate keys.
Iteration (foreach, LINQ) — the most general Count, Add, Remove, Contains Index access and insertion
IReadOnlyCollection<T> / IReadOnlyList<T> Count / index access without mutation
IDictionary<TKey,TValue> / IReadOnlyDictionary<TKey,TValue> Keyed access with / without mutation
Accept the most general interface you need as a parameter, and return read-only interfaces from APIs.
C# 12 unifies initialization: int[] a = [1, 2, 3];, List<int> l = [.. a, 4]; (spread ..), [] for empty. They work for arrays, lists, spans, immutable collections and your own types (with a CollectionBuilder).
System.Collections.Immutable provides ImmutableArray<T>, ImmutableList<T>, ImmutableDictionary<TKey,TValue>, ImmutableHashSet<T> and more. Every "modification" returns a new collection that shares structure with the old one. They are thread-safe by construction and ideal for configuration, snapshots and functional-style code. ReadOnlyCollection<T> (a read-only *view*) is different: the underlying list can still change.
Why It Matters
Choosing the right collection determines both correctness (uniqueness, ordering) and performance (O(1) lookups instead of O(n) scans). Almost every program stores data in a list or dictionary.
Syntax
List<T> list = [a, b]; var dict = new Dictionary<K, V>();
dict[key] = value; if (dict.TryGetValue(key, out var v)) { }
var set = new HashSet<T>(); set.UnionWith(other);
queue.Enqueue(x); queue.Dequeue(); stack.Push(x); stack.Pop();Example
using System.Collections.Immutable;
// List<T>: growable array
List<string> ecus = ["VCU", "BMS"];
ecus.Add("ABS");
ecus.Insert(0, "GW");
ecus.Remove("BMS");
Console.WriteLine($"List: {string.Join(", ", ecus)} (Count {ecus.Count}), contains ABS: {ecus.Contains("ABS")}");
// Dictionary<TKey, TValue>: fast lookup by key
var dtcCounts = new Dictionary<string, int> { ["P0A80"] = 2, ["U0100"] = 1 };
dtcCounts["P0A80"]++;
dtcCounts.TryAdd("C1234", 5);
Console.WriteLine($"Dictionary: {string.Join(", ", dtcCounts.Select(kv => $"{kv.Key}={kv.Value}"))}");
Console.WriteLine($"TryGetValue(\"B0001\"): {dtcCounts.TryGetValue("B0001", out int n)} (n = {n})");
// HashSet<T>: unique items, set algebra
var seenIds = new HashSet<uint> { 0x100, 0x200, 0x100 };
var expected = new HashSet<uint> { 0x200, 0x300 };
Console.WriteLine($"HashSet count {seenIds.Count}; missing: {string.Join(",", expected.Except(seenIds).Select(x => $"0x{x:X}"))}");
// Queue<T> (FIFO) and Stack<T> (LIFO)
var queue = new Queue<string>(["frame1", "frame2"]);
queue.Enqueue("frame3");
var stack = new Stack<string>(["page1", "page2"]);
stack.Push("page3");
Console.WriteLine($"Queue.Dequeue = {queue.Dequeue()}, Peek = {queue.Pee
…Expected Result
Dictionary enumeration order happens to match insertion order here, but it is not guaranteed — use SortedDictionary or an explicit sort when order matters.
List: GW, VCU, ABS (Count 3), contains ABS: True
Dictionary: P0A80=3, U0100=1, C1234=5
TryGetValue("B0001"): False (n = 0)
HashSet count 2; missing: 0x300
Queue.Dequeue = frame1, Peek = frame2 | Stack.Pop = page3, Peek = page2
LinkedList: 0 -> 1 -> 2 -> 3 -> 4
SortedSet: 1 3 5 9 (Min 1, Max 9)
SortedDictionary keys: current, temp, voltage; SortedList: a, b, c (index of key 20 = 1)
IReadOnlyList[0] = GW, IEnumerable sum = 18, ICollection.Count = 3, IReadOnlyDictionary has P0A80: True
collection expression + spread: 1,2,3,4,5,6
v1: VCU,BMS | v2: VCU,BMS,ABS | limits[rpm] = 6500 | ImmutableArray sorted: 1,2,3Common Mistakes
Using List<T>.Contains in a loop over large data (O(n²)) — use a HashSet<T>.
Reading dict[key] for a missing key (throws KeyNotFoundException).
Modifying a collection inside foreach over it (throws InvalidOperationException).
Relying on Dictionary or HashSet ordering.
Using mutable objects as dictionary keys and then changing them.
Sharing a normal List<T> or Dictionary between threads without locking (see concurrent collections, Module 34).
Best Practices
List<T> and Dictionary<TKey,TValue> cover most needs; pick others for specific semantics.
Set initial capacity when the size is known (new List<T>(10_000)).
Expose IReadOnlyList<T> / IReadOnlyDictionary<,> from classes.
Pass a comparer for case-insensitive keys: new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase).
Interview Relevance
Very high: "List vs array", "how does Dictionary work (hashing, buckets, GetHashCode/Equals)?", "IEnumerable vs ICollection vs IList", "which collection for FIFO/LIFO/unique/sorted?".
Practice Questions
Count word frequencies in a text with a Dictionary<string, int> (case-insensitive).
Find the first non-repeating character in a string.
Implement a browser history with two stacks (back/forward).
Use a Queue<T> to simulate a CAN transmit buffer of capacity 8.
Given two lists of ECU names, print those present in both and those in only one.
Quick Quiz
Q1. (Multiple choice) Best collection to check quickly whether a CAN ID has been seen? A) List<uint> B) HashSet<uint> C) Queue<uint> D) LinkedList<uint> Answer: B. O(1) average Contains.
Q2. (Predict the output) var q = new Queue<int>([1, 2]); q.Enqueue(3); Console.WriteLine(q.Dequeue()); Answer: 1 — FIFO.
Q3. (True/False) Dictionary<TKey,TValue> guarantees items are enumerated in insertion order.
Answer: False. Q4. (Find the bug) if (counts.ContainsKey(k)) counts[k]++; else counts.Add(k, 1); works, but what is a cleaner single-lookup alternative?
Answer: counts[k] = counts.GetValueOrDefault(k) + 1; (or CollectionsMarshal.GetValueRefOrAddDefault for hot paths).
Coding Challenge
Group a list of DTC codes by their first letter (P, C, B, U) and print each group with its count, ordered by letter.
string[] dtcs = ["P0A80", "U0100", "P0300", "C1234", "U0121", "P0171"];
var groups = new SortedDictionary<char, List<string>>();
foreach (string d in dtcs)
{
if (!groups.TryGetValue(d[0], out var list))
groups[d[0]] = list = [];
list.Add(d);
}
foreach (var (letter, codes) in groups)
Console.WriteLine($"{letter}: {codes.Count} ({string.Join(", ", codes)})");