C# Coding Interview Gotchas: What Compiles and Still Fails

May 29, 202610 min read
dsaalgorithmsinterview-prepdata-structures
C# Coding Interview Gotchas: What Compiles and Still Fails
TL;DR
  • Integer overflow wraps silently in C# by default; cast to long or use checked near int.MinValue
  • Comparator subtraction overflows when one operand is int.MinValue; use a.CompareTo(b) instead
  • List<T>.Sort() is unstable (introsort); use OrderBy() when equal-key order must be preserved
  • Dictionary<K,V> iteration order is undefined; use SortedDictionary for sorted traversal
  • PriorityQueue is min-heap by default; negate priority values to get max-heap behavior
  • for loop lambdas capture the variable by reference, not value; copy to a local before capturing
  • LINQ queries are lazy and re-execute on each enumeration; call .ToList() to materialize results

Pick C# for a coding interview and you get readable syntax, solid collection types, and a standard library that covers almost everything. You also get a handful of traps that produce wrong answers without a compile error, a runtime crash, or any indication that something went wrong. The compiler is fine. The tests pass locally. The interviewer adds one boundary value and your solution quietly outputs the wrong number.

These aren't obscure edge cases. They show up on sorting problems, graph traversal, and sliding window questions. Here are the eight that will actually bite you.

Integer Overflow Wraps Without a Sound

Java throws ArithmeticException. Python integers grow unbounded. C# wraps and says nothing.

By default, C# integer arithmetic runs in an unchecked context. Overflow discards the high-order bits and wraps to the opposite end of the range. No exception. No visible sign anything went wrong. Your code produces a number, just not the right one.

int x = int.MaxValue; int y = x + 1; Console.WriteLine(y); // -2147483648

The checked keyword turns overflow into an OverflowException:

checked { int y = int.MaxValue + 1; // throws OverflowException }

The nastiest version is Math.Abs(int.MinValue). int.MinValue is -2,147,483,648. Its positive counterpart would be 2,147,483,648, which exceeds int.MaxValue by one. So negating int.MinValue overflows right back to int.MinValue. You ask for a positive number and get back the same negative number you started with.

Console.WriteLine(Math.Abs(int.MinValue)); // -2147483648, not a positive number

Overflow number line diagram showing int.MaxValue wrapping to int.MinValue

Adding one to the maximum wraps all the way around to the minimum. The language does not complain.

Any time your code involves absolute values, differences, or accumulations near the integer boundaries, cast to long before operating. The integer overflow guide covers the cross-language pattern.

Comparator Subtraction Overflows Too, and Looks Fine Doing It

This one shows up constantly as a "clever shortcut" in Stack Overflow answers and interview solutions. It is subtly broken.

var nums = new[] { int.MinValue, 1, 2 }; Array.Sort(nums, (a, b) => a - b); // broken

If a is int.MinValue (-2,147,483,648) and b is 1, then a - b overflows to a large positive number, reversing the sign of the comparison. The sort produces wrong output without crashing, and the bug only surfaces when the test case happens to include extreme values. An interviewer who adds int.MinValue to their examples will catch it. They often do.

CompareTo returns -1, 0, or 1 without doing arithmetic. Use it.

Array.Sort(nums, (a, b) => a.CompareTo(b)); // correct Array.Sort(nums, Comparer<int>.Default.Compare); // also correct

This is the right default for any custom comparator on integers.

List.Sort() Is Unstable. OrderBy() Is Stable.

Array.Sort, List<T>.Sort, and Span<T>.Sort all use introsort: a hybrid of quicksort, heapsort, and insertion sort. Fast, in-place, but unstable. Equal elements can move relative to each other.

LINQ's OrderBy and ThenBy are documented as stable sorts. Equal-key elements stay in their original relative order.

var items = new[] { ("Alice", 30), ("Bob", 25), ("Charlie", 30) }; // Stable: Alice comes before Charlie (original order preserved among ties) var stable = items.OrderBy(p => p.Item2).ToArray(); // Unstable: Alice vs Charlie order after sorting by age is not guaranteed Array.Sort(items, (x, y) => x.Item2.CompareTo(y.Item2));

Stability matters whenever equal keys exist and relative order carries meaning. Sorting intervals by start time and breaking ties by original index requires a stable sort. List.Sort() silently gives wrong results in that case. The problem will work for most inputs and fail quietly on the ones where two elements share a key.

When in doubt about ties, use OrderBy. The stable sort explainer covers why this distinction matters for multi-pass sorting.

Dictionary Iteration Order Is Undefined (and Happens to Work Until It Doesn't)

Dictionary<TKey, TValue> is a hash table. Iteration order depends on hash bucket layout, insertion history, and resizes. It is not insertion order, not alphabetical, not anything you can rely on.

var d = new Dictionary<string, int> { ["banana"] = 2, ["apple"] = 1, ["cherry"] = 3, }; foreach (var kv in d) Console.Write(kv.Key + " "); // might print: banana apple cherry // might not. order is undefined

The .NET runtime often preserves insertion order as an implementation side effect. That is not a contract. Code that depends on it works until a runtime update or a slightly different input reshuffles the buckets, then it fails in a way that looks random.

When you need sorted iteration or ordered lookups:

CollectionBacking structureInsertLookupNotes
SortedDictionary<K,V>Red-black treeO(log n)O(log n)Always iterates in sorted key order
SortedList<K,V>Two parallel arraysO(n)O(log n)Index access; better for read-heavy
SortedSet<T>Red-black treeO(log n)O(log n)Has Min, Max, GetViewBetween

SortedSet<T> is underused in interview solutions. Its Min, Max, and range views make it a natural replacement for a heap in sliding window problems that need the minimum or maximum of a dynamic set. More on time complexities in the hash map guide.

PriorityQueue Dequeues the Smallest Thing First

.NET 6 added PriorityQueue<TElement, TPriority>. The element with the lowest priority value dequeues first. This trips up anyone who reads "priority queue" and expects the highest-priority item at the front.

var pq = new PriorityQueue<string, int>(); pq.Enqueue("slow", 10); pq.Enqueue("fast", 1); Console.WriteLine(pq.Dequeue()); // "fast" -- priority 1 dequeues first

To get max-heap behavior, negate the priority when you enqueue:

pq.Enqueue(element, -score); // largest score has most-negative (smallest) priority

Two subtleties that matter in interview problems:

PriorityQueue has no Contains, no way to check or update an element's priority. For Dijkstra-style problems, push duplicate entries and skip stale ones on dequeue by comparing the popped distance against the current known distance before processing.

Elements with equal priority dequeue in unspecified order. If your problem needs tie-breaking by insertion sequence, use a composite priority: wrap it in a tuple (priority, seqNum) where seqNum is a monotonically increasing counter.

for Loop Lambdas Capture the Variable, Not Its Value

C# 5.0 fixed foreach so each iteration gets its own copy of the loop variable when captured by a lambda. The for loop was not changed. This is the kind of asymmetry that produces bugs you will stare at for longer than you'd like to admit.

var actions = new List<Action>(); for (int i = 0; i < 3; i++) { actions.Add(() => Console.WriteLine(i)); } actions.ForEach(a => a()); // prints: 3 3 3 // not: 0 1 2

All three lambdas close over the same i. By the time any of them runs, i is 3.

Copy the loop variable to a local inside the iteration before capturing it:

for (int i = 0; i < 3; i++) { int captured = i; actions.Add(() => Console.WriteLine(captured)); // 0 1 2 }

foreach (var item in collection) does not have this problem in modern C#. Each iteration's item is a fresh binding. The trap is specific to for and while when lambdas reference the loop counter directly.

LINQ Is Lazy Until You Force It to Do Something

Where, Select, OrderBy, and most LINQ operators return a lazy IEnumerable<T>. The query body does not execute when you write it. It executes each time you iterate the result. Write the query, nothing happens. Call .Count(), the filter runs. Call .Count() again, the filter runs again.

var nums = new[] { 1, 2, 3, 4, 5 }; var evens = nums.Where(n => n % 2 == 0); Console.WriteLine(evens.Count()); // runs the filter Console.WriteLine(evens.Count()); // runs it again

For pure filtering this is harmless. The bite comes with predicates that have side effects:

var seen = new HashSet<int>(); var distinct = nums.Where(n => seen.Add(n)); var first = distinct.ToList(); // seen = {1,2,3,4,5} var second = distinct.ToList(); // seen already full, returns empty list

Call .ToList() or .ToArray() immediately when you need stable results. This materializes the query into a concrete collection you can iterate multiple times without re-running the filter.

In an interview, the symptom is a method that returns an empty result the second time you call it, or a query that runs unexpectedly slowly because it re-enumerates a large source. Both are confusing under pressure.

String Comparison: == Is Fine, Other Methods Aren't

In C#, == on strings uses ordinal comparison: byte-by-byte, no culture, no folding. For algorithm problems, this is what you want.

The trouble starts with the overloads that accept a boolean ignoreCase:

string.Compare("abc", "ABC", ignoreCase: true) // uses CurrentCulture

That form uses the current thread's culture for case folding. On a Turkish-locale machine, the uppercase of lowercase i is İ (U+0130), not I. Code that does s.ToUpper() == "FILE" or uses culture-sensitive comparison will produce wrong results on Turkish systems. This is a real production bug called the Turkish-I problem, not a contrived gotcha. Entire authentication systems have failed because of it.

For identifiers, dictionary keys, and anything that needs consistent behavior across locales, use:

string.Equals(a, b, StringComparison.OrdinalIgnoreCase)

When constructing a Dictionary or HashSet for case-insensitive keys:

var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

This matters in problems that count words case-insensitively. With the default comparer, "Apple" and "apple" are different keys. Your word count will be wrong and the test case will have exactly that input.

Eight Gotchas, One Table

GotchaDefault behaviorSafe alternative
int arithmeticWraps silently on overflowCast to long; avoid Math.Abs(int.MinValue)
(a, b) => a - bOverflows near int.MinValuea.CompareTo(b)
List<T>.Sort()Unstable (introsort)OrderBy() when equal-key order matters
Dictionary<K,V> iterationOrder undefinedSortedDictionary for sorted traversal
PriorityQueue<E,P>Min-heap, lowest P firstNegate priority for max-heap
for loop lambdaCaptures variable by refCopy int captured = i inside the loop
LINQ query variableLazy, re-executes each iteration.ToList() to materialize
string.Compare(a, b, true)Culture-sensitiveStringComparison.OrdinalIgnoreCase

What to Drill Before Your Interview

The overflow trap and the comparator bug are the two highest priorities. They produce wrong answers on edge-case inputs with no error, and an interviewer who adds int.MinValue to the test cases catches both. That test case is not rare.

Practice explaining your comparator choice and sort stability decision out loud. SpaceComplexity runs voice-based mock interviews where you narrate these decisions in real time, which is exactly where the silent bugs slip through when you're only testing mentally.

The C# for Coding Interviews overview covers collection selection. The C# vs Java comparison maps the differences if you're switching from a Java background. For the complete language choice question, best language for coding interviews lays out the tradeoffs.

Further Reading