.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.
🎯 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
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
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 onlyFix: 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
Equalsrequires overridingGetHashCodeconsistently, orDictionary/HashSetbreak. - Mutating a field used in
GetHashCodeafter inserting into aHashSetmakes the item unfindable. - Compare strings with an explicit
StringComparison(Ordinal,OrdinalIgnoreCase); culture-sensitive comparisons produce surprises (the Turkish "I").
4Numbers
doublecan't represent 0.1 exactly:0.1 + 0.2 != 0.3. Usedecimalfor money.- Integer overflow wraps silently unless in a
checkedcontext:int.MaxValue + 1is negative. - Integer division truncates:
5 / 2 == 2.
5Dates and Times
DateTime.Nowis local time; servers should useDateTime.UtcNoworDateTimeOffset, and convert to local only for display.DateTime.Kind(Utc,Local,Unspecified) causes bugs when values cross systems;DateTimeOffsetcarries the offset.- Inject
TimeProviderto 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
foreachthrowsInvalidOperationException. IEnumerablevsIQueryable: filtering afterAsEnumerable()happens in memory.
7Async and Exceptions
async voidcrashes the process on exceptions;Task.Run(async …)without awaiting loses exceptions.throw ex;resets the stack trace; usethrow;.finallyruns even onreturn;usingdisposes 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
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
forloops. - 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
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.
- 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.
- 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.
- 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.
- 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.
- 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.
for (int i=0;i<3;i++) list.Add(() => i); then calling each returns…int.MaxValue + 1 in default (unchecked) context is…