Engineering Notes
Home/Languages/C#/.NET Interview Gotchas
Languages · C#

.NET Interview Gotchas

The classic C#/.NET trick questions, and why each answer is what it is: closure capture, struct copies, equality and hashing, floating point and overflow, dates and time zones, deferred LINQ execution, async void and more.

💻 Languages● Intermediate⏱ 3 min read🛠️ 2 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Closures, struct copies and immutable strings
  • Equals/GetHashCode and string comparison
  • Numbers, overflow and money
  • Dates, deferred LINQ execution and async gotchas

📚 Before you start

Questions interviewers love because the obvious answer is wrong. Know the behaviour and the fix.

1Closures Capture Variables, Not Values

csharp
var actions = new List<Func<int>>();
for (int i = 0; i < 3; i++) actions.Add(() => i);
actions.Select(a => a());   // 3, 3, 3: all lambdas share one `i`

foreach gets a fresh variable per iteration (since C# 5), for doesn't. Fix: copy to a local inside the loop (int copy = i;).

2Mutating a Struct Copy

csharp
var points = new List<MutablePoint> { new MutablePoint(1, 1) };
points[0].X = 5;           // compile error for List<T>; for other APIs you silently change a copy
var p = points[0]; p.X = 5; // changes the copy only

Fix: make structs immutable, assign the whole element back (points[0] = points[0] with { X = 5 }), or use a class.

3Equality and Hashing

  • == on classes compares references unless overloaded; records and strings compare values.
  • Overriding Equals requires overriding GetHashCode consistently, or Dictionary/HashSet break.
  • Mutating a field used in GetHashCode after inserting into a HashSet makes the item unfindable.
  • Compare strings with an explicit StringComparison (Ordinal, OrdinalIgnoreCase); culture-sensitive comparisons produce surprises (the Turkish "I").

4Numbers

  • double can't represent 0.1 exactly: 0.1 + 0.2 != 0.3. Use decimal for money.
  • Integer overflow wraps silently unless in a checked context: int.MaxValue + 1 is negative.
  • Integer division truncates: 5 / 2 == 2.

5Dates and Times

  • DateTime.Now is local time; servers should use DateTime.UtcNow or DateTimeOffset, and convert to local only for display.
  • DateTime.Kind (Utc, Local, Unspecified) causes bugs when values cross systems; DateTimeOffset carries the offset.
  • Inject TimeProvider to make time testable.

6LINQ and Collections

  • Deferred execution: a query runs each time it's enumerated; enumerating twice hits the database twice. Materialize with ToList() when you need it more than once.
  • First() throws on empty; FirstOrDefault() returns default (null or 0).
  • Modifying a collection while iterating with foreach throws InvalidOperationException.
  • IEnumerable vs IQueryable: filtering after AsEnumerable() happens in memory.

7Async and Exceptions

  • async void crashes the process on exceptions; Task.Run(async …) without awaiting loses exceptions.
  • throw ex; resets the stack trace; use throw;.
  • finally runs even on return; using disposes even on exceptions.

8Strings

Strings are immutable: s.Replace("a", "b") returns a new string and doesn't change s. string.Empty vs null vs whitespace: use string.IsNullOrWhiteSpace.

📋 Quick Reference

text
closures capture variables (for-loop gotcha) · mutating struct copies · Equals ⇔ GetHashCode
decimal for money · overflow wraps (checked) · int division truncates
UtcNow / DateTimeOffset / TimeProvider · deferred LINQ execution · First vs FirstOrDefault
async void · throw; not throw ex; · strings immutable · explicit StringComparison

🧠 Key takeaways

  • Lambdas capture variables: copy loop variables in for loops.
  • Override Equals and GetHashCode together; compare strings with explicit StringComparison.
  • Use decimal for money, UtcNow/DateTimeOffset for time, checked where overflow matters.
  • LINQ is deferred: materialize once; avoid async void and throw ex;.

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1Why do lambdas in a for loop all see the last value?core▾

Closures capture variables, not values; a for loop has one variable shared by all iterations, so after the loop each lambda reads its final value. foreach creates a new variable per iteration (since C# 5). Copy to a local inside the loop.

Follow-ups interviewers ask
  • Same in JavaScript? Yes with var; let creates a per-iteration binding.
Q2What's the contract between Equals and GetHashCode?core▾

Equal objects must return equal hash codes; hash codes should be stable while the object is in a hash-based collection. Breaking it makes Dictionary/HashSet lookups fail or duplicates appear.

Follow-ups interviewers ask
  • Easiest correct approach? Records, or HashCode.Combine over the same fields used in Equals.
Q3double vs decimal?core▾

double is binary floating point: fast, huge range, but can't represent many decimal fractions exactly. decimal is base-10 with 28–29 significant digits, slower, and exact for money arithmetic.

Follow-ups interviewers ask
  • Comparing doubles? Use a tolerance.
Q4DateTime vs DateTimeOffset?senior▾

DateTime has a Kind (Utc/Local/Unspecified) but no offset, leading to ambiguous values across systems; DateTimeOffset stores the offset and represents an exact instant. Store UTC, convert for display, and inject TimeProvider for testability.

Follow-ups interviewers ask
  • DateTime.Now on servers? Avoid; use UtcNow.
Q5What's wrong with throw ex;?core▾

It rethrows the exception object but resets the stack trace to the current location, losing where it originated. Use throw; to preserve it, or wrap with a new exception passing the original as InnerException.

Follow-ups interviewers ask
  • ExceptionDispatchInfo? Rethrows captured exceptions later with original stack traces.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.

✅ Check yourself

Answer the questions — feedback is instant.

1. for (int i=0;i<3;i++) list.Add(() => i); then calling each returns…
Shared variable.
2. Overriding Equals without GetHashCode breaks…
Contract.
3. int.MaxValue + 1 in default (unchecked) context is…
Wraps around.
4. A LINQ query over a DB is enumerated twice. Result?
Deferred execution.