Collections and LINQ
List and Dictionary, and the Where/Select/OrderBy chain that replaces a hand-written loop.
After this lesson you can
- Choose between List<T> and Dictionary<K, V> for a given problem
- Chain Where, Select and OrderBy instead of writing the loop by hand
- Explain why a LINQ query does not run until it is enumerated
List<T> keeps order and allows duplicates, backed by a resizable
array — list[i] is O(1). Dictionary<TKey, TValue> maps a key to a
value with O(1) average lookup, at the cost of no defined ordering.
var scores = new List<int> { 90, 75, 90 };
var byName = new Dictionary<string, int> { ["Nino"] = 90, ["Ana"] = 75 };
if (byName.TryGetValue("Nino", out var score))
Console.WriteLine(score);
TryGetValue is the idiom for "look this up, and tell me whether it was
there" in one call, without a separate ContainsKey check followed by
an indexer lookup that walks the same bucket twice.
LINQ
var names = people
.Where(p => p.Age >= 18)
.OrderBy(p => p.Name)
.Select(p => p.Name)
.ToList();
Where filters, Select transforms, OrderBy sorts — the same three
ideas as filter/map/sort in most other languages, chained in the
order you think about the problem. None of them mutate the original
collection; each returns a new sequence.
Try it
public static class Solution{ public static string Describe() { var scores = new Dictionary<string, int> { ["Nino"] = 90, ["Ana"] = 55, ["Luka"] = 70, ["Mari"] = 40, }; var passingNames = scores .Where(kv => kv.Value >= 60) .OrderByDescending(kv => kv.Value) .Select(kv => kv.Key) .ToList(); return string.Join(", ", passingNames); }}Deferred execution
A LINQ query built with Where/Select does not run when you write
it — it runs when something actually enumerates it: a foreach, or a
terminal call like .ToList(), .Count(), or .First().
var query = numbers.Where(n => n > 0); // nothing has executed yet
numbers.Add(-5);
var result = query.ToList(); // executes now, over the current numbers
If numbers changes between building the query and enumerating it, the
enumeration sees the current state, not a snapshot from when the
query was written — because there never was a snapshot, only a
description of what to do once asked. .ToList() at the point you
actually want a fixed result is the way to pin one down, the same
discipline a LINQ-to-Entities query over a database needs even more
urgently, where "run it twice" can mean two round trips.
Common mistakes
public static class Solution{ public static string Describe() { var stock = new Dictionary<string, int> { ["apple"] = 3 }; var log = new List<string>(); try { var n = stock["pear"]; } catch (KeyNotFoundException) { log.Add("indexer: KeyNotFoundException"); } try { stock.Add("apple", 5); } catch (ArgumentException) { log.Add("Add duplicate: ArgumentException"); } stock["apple"] = 5; log.Add("indexer set: apple=" + stock["apple"]); var nums = new List<int> { 1, 2, 3 }; try { foreach (var x in nums) if (x == 2) nums.Remove(x); } catch (InvalidOperationException) { log.Add("remove during foreach: InvalidOperationException"); } var empty = new List<int>(); log.Add("FirstOrDefault on empty=" + empty.FirstOrDefault()); try { empty.First(); } catch (InvalidOperationException) { log.Add("First on empty: InvalidOperationException"); } return string.Join("; ", log); }}- Reading a missing key with the indexer.
dict["pear"]throwsKeyNotFoundException;TryGetValuereports absence without an exception. Writing through the indexer is the opposite:dict[k] = vquietly overwrites, whileAddthrowsArgumentExceptionon a duplicate key. Pick the one whose failure you actually want. - Removing from a list while a
foreachwalks it. The enumerator notices the list changed and throwsInvalidOperationException. UseRemoveAll(predicate), or loop backwards with an index. First()on something that might be empty. It throws;FirstOrDefault()returnsdefault, which forintis0and so is indistinguishable from a real zero.- Relying on
Dictionaryenumeration order. It is not part of the contract. Sort withOrderBywhen the order matters.
What interviewers ask
- Index access is O(1) but on a list is O(n); dictionary and set lookups are O(1) on average because they hash the key.
Try it yourself
2 visible tests · 3 hidden testsImplement AveragePassing(Dictionary<string, int> scores, int threshold).
Using LINQ, return the average of every score that is greater than or
equal to threshold, rounded to 2 decimal places. If nobody passes,
return 0.
AveragePassing({"Ana":55,"Luka":70,"Nino":90}, 60)AveragePassing({"A":10,"B":20}, 60)
Sign up to run the tests, check the hidden ones and save your progress. Sign up