The C# Interview

Lesson 1 of 5

What C# Interviews Probe

"Try to love the questions themselves, like locked rooms and like books that are written in a very foreign tongue." — Rainer Maria Rilke, Letters to a Young Poet (1903)

A C# interview question is a locked room. The question on the surface is almost always about syntax — two keywords, a comparison, a cleanup construct — and what is inside is almost always the runtime. Candidates who prepare the surface answer can recite the difference between two keywords and then have nothing to say when asked why it matters, because the why lives one level down, in the CLR.

That is the organising idea of this volume, stated plainly at last: C# interviews probe the runtime through the language. This chapter takes five questions that look like trivia, measures what is actually happening underneath each, and shows the gap between the answer people prepare and the answer that gets an offer.

The big question

Here are four questions that look like syntax: what is the difference between == and Equals; between const and readonly; what does using actually compile to; why are mutable structs discouraged. For each one, name the runtime behaviour being probed — and say what a wrong answer would cost you in production.

The question that looks like trivia

Start with the most-asked question in C# interviews, because the usual answer is right and useless.

C# — Try it Yourself
Loading editor…
Output
Click Run to compile and execute the code.

The folklore answer — "== compares references, Equals compares values" — is wrong in both directions for the most common type in the language. a == b is true by reference because the compiler folded "hel" + "lo" into a literal and literals are interned, so the two variables name one object. And a == c is true by value, because string declares an == overload.

Then the measurement that does the work. Change nothing about the objects, only the static type of the two variables, and oa == oc is false. The same two objects, compared with the same operator, give a different answer depending on what the compiler thought their type was.

That is the thing being probed. == is an operator, resolved at compile time from the static types; object declares no overload, so that line compiled to a reference comparison. Equals is a virtual method, dispatched at run time on the real object, so it still finds string's override and says true.

It matters in production the moment values travel as object, dynamic or an unconstrained generic T — a cache keyed on object, a comparison inside a generic helper — where == quietly becomes reference equality and a lookup that worked in testing with interned literals starts missing with runtime-built strings. That is a bug that only appears with real data, which is exactly the kind interviewers are checking you have met.

An analogy

Think of posting a letter to "the manager" versus to a person by name. Addressing it to the role is resolved by whoever happens to hold that role when the letter arrives — late binding, decided at delivery. Addressing it by name is fixed the moment you write it, and if that person has since changed jobs the letter goes to the wrong desk even though the role is now filled by somebody else entirely. Neither is wrong; they resolve at different moments against different information. Most confusion about == comes from assuming it behaves like the role when it behaves like the name — and the reason nobody notices is that for strings the two usually agree.

What "compile time" actually means

The second question looks like a style preference between two keywords. It is a question about where a value physically ends up.

C# — Try it Yourself
Loading editor…
Output
Click Run to compile and execute the code.

The metadata settles it. MaxRetries is IsLiteral and carries its value in the metadata — GetRawConstantValue() returns 3. Timeout is IsInitOnly: a real field, assigned when the type initialises, and asking it for a constant value throws because it does not have one.

The consequence is the reason the question is asked. A const is copied into every caller at compile time. if (attempts < Config.MaxRetries) compiles to if (attempts < 3), with no read of Config at run time at all — the field need not exist in the shipped assembly.

So when a library changes a const from 3 to 5 and you update the DLL without rebuilding, your code still uses 3. Nothing is corrupt, nothing throws, and two assemblies quietly disagree about a number. That is the whole answer, and it generalises to a rule worth stating: make something const only when the value is part of your contract and could never sensibly change — Math.PI, a protocol's magic number — and static readonly for everything that is merely a current value.

The last group adds the other half. static readonly can hold anything, including a value only knowable at run time; a const must be computable by the compiler, which limits it to primitives, strings and enums, and means it can never be a new anything.

A note on history

The versioning hazard in that block is not an accident of C#; it is an inherited problem that .NET was explicitly designed to improve on. Under COM and classic Windows DLLs, replacing a shared library with a newer version routinely broke applications that depended on the old one's exact layout — the problem that got the name "DLL hell". .NET's answer was strong-named assemblies, versioned references and side-by-side loading, so two applications could use different versions of one library without a central registry deciding for them. const inlining is the one place where the old model leaks through: the value is burned into the consumer at compile time, so no amount of assembly versioning can keep the two in step. The framework team has lived with this publicly — it is the documented reason the BCL favours static readonly for values that are not mathematically fixed, and why constants like int.MaxValue are safe to inline while a tuning parameter is not. Knowing which category a value belongs to is the actual skill; the keyword difference is just how you write the decision down.

The question that separates a senior

The third question is rarely asked directly and comes out of a discussion about error handling. It is one of the sharpest level filters in the language.

C# — Try it Yourself
Loading editor…
Output
Click Run to compile and execute the code.

Read the first line of output carefully: FILTER runs before inner-finally. The filter in Outer is evaluated while Inner's finally has not yet executed — which means the stack has not been unwound and Inner's frame is still live.

The contrast confirms it is the filter and not something else: replace when (Filter()) with a plain catch that decides inside its body, and the order flips — inner-finally now runs first, because a catch body only executes once the stack has already been unwound to it.

This is the two-pass exception model, and it is a CLR behaviour rather than a C# one. The runtime walks the stack twice: first to find a handler, evaluating filters as it goes with nothing yet unwound; then to unwind to that handler, running finally blocks on the way. C# exposed the first pass to the language only in C# 6; the mechanism was in the CLR from the beginning and visible from Visual Basic for years before that.

The practical consequence is why this is a level filter. A filter sees the stack intact, so logging from a filter captures state a catch block has already destroyed — which is what makes catch (Exception e) when (Log(e)), with Log always returning false, a legitimate technique rather than a hack. The last group shows the other half: a filter returning false does not handle the exception and does not disturb it, so it passes outward untouched.

From the notebook

Writing this chapter meant auditing my own volume first, and the result changed its contents. My initial shortlist of "classic C# interview questions worth measuring" led with async void and closure capture — and both turned out to be covered already, async void in Part IV's chapter on threads and tasks, closures in Part III's LINQ chapter, where the measured result about capturing the variable rather than its value already appears. Had I not checked, this chapter would have re-measured two things the reader met fifteen chapters ago and called it a capstone. There is a small lesson in that about interview preparation too, which is roughly the inverse: candidates tend to over-prepare the topics they have already internalised, because revisiting them feels productive, and under-prepare the ones they have never had to say out loud. The fix in both cases is the same — inventory what you actually have before deciding what to study.

What using compiles to

The fourth question sounds like a beginner's question and has a sharp edge at the end.

C# — Try it Yourself
Loading editor…
Output
Click Run to compile and execute the code.

Disposal is reverse of declaration, and the second group shows why by writing out what the compiler emits: each using is a nested try/finally, so the innermost finishes first. That is the whole mechanism, and being able to write the expansion is a better answer than reciting the order.

The edge is the last part and it is genuinely dangerous. When the body throws and Dispose throws, the caller sees NotSupportedException: from Dispose. The body's InvalidOperationException is not wrapped, not attached as an inner exception — it is gone. A finally block that throws replaces whatever exception was propagating, and using is a finally block.

That is why "Dispose must not throw" is a rule rather than a style preference. The failure mode is silent and actively misleading: you lose the diagnosis of the original fault and get told about the cleanup instead, which sends the investigation in exactly the wrong direction.

In the real world

This exact shape produces one of the more confusing incident patterns: an error log full of cleanup failures and no sign of whatever actually broke. The usual version involves a connection or a stream whose Dispose tries to flush, the flush fails because the underlying thing is already broken, and the real fault — the timeout, the reset, the disk error — is replaced by a message about disposal. Teams then spend the first hour investigating the disposal code, which is working correctly and reporting a true fact about a secondary failure. The defences are small. Make Dispose tolerant of a half-constructed or already-failed object rather than assuming the happy path, since that is precisely when it runs; if cleanup can genuinely fail in a way worth knowing about, log it inside Dispose rather than throwing, so the information survives without displacing anything. And when reading an unfamiliar stack trace, it is worth asking whether the exception you are looking at arrived during unwinding, because if it did, the interesting one may no longer exist.

Why mutable structs are discouraged

The last question is usually phrased as a style rule, and the honest answer is that the language will let you write a mutation the runtime discards.

C# — Try it Yourself
Loading editor…
Output
Click Run to compile and execute the code.

Thirty calls to Increment, and twenty of them did nothing. The readonly field reports 0, the property reports 0, and only the plain mutable field reaches 10. Nothing errored and nothing warned.

The mechanism is the chapter's thesis one more time. A readonly field must not change, so the compiler emits a defensive copy and calls the method on that. A property getter returns a copy by definition, so the mutation lands on a temporary that is discarded on the next line. The array case shows the contrast — indexing an array yields a reference to the element, so the mutation sticks — and List<T> behaves like the property, because its indexer is one.

So the honest answer to "why are mutable structs discouraged" is not that mutation is impure. It is that whether a mutation takes effect depends on how you reached the value, and nothing in the language tells you which case you got. That is an unusually bad property for a construct to have.

The fix is to make the intent impossible to get wrong: readonly struct makes the compiler reject mutating members outright, and record struct gives value semantics with with for producing changed copies explicitly. Reaching for either is the senior answer, because it removes the category of bug rather than documenting it.

Common Mistakes

  1. Answering "== is reference, Equals is value". It is wrong for strings in both directions; the real distinction is static binding versus virtual dispatch.

  2. Forgetting that == on object is reference equality. Values travelling as object, dynamic or unconstrained T silently change the comparison.

  3. Using const for a tuning value. It is copied into every caller at compile time, so updating the library without rebuilding leaves consumers on the old number.

  4. Thinking const and static readonly differ only in style. One is metadata, the other is a field; only the second can hold a runtime value.

  5. Believing finally runs before a filter. The filter runs in the first pass, with the stack still intact — that is what makes logging from a filter useful.

  6. Throwing from Dispose. It replaces the exception that was propagating, so the original fault disappears entirely.

  7. Assuming using disposes in declaration order. It is a nested try/finally, so disposal is reverse.

  8. Mutating a struct through a property. The getter returned a copy; the mutation is discarded with no warning.

  9. Mutating a struct held in a readonly field. The compiler inserts a defensive copy for exactly the same reason.

  10. Preparing the keyword difference instead of the consequence. Every question in this chapter is really asking what happens at run time.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Warm-up
What is the difference between const and static readonly?
Model answer — A const is a compile-time literal: its value lives in the metadata and is copied into every place that reads it, so there is no field access at run time at all. A static readonly is a real field, assigned once when the type initialises and never again. Two practical consequences follow. First, a const is limited to things the compiler can compute — primitives, strings and enums — so it can never be a new object or anything time-dependent, whereas static readonly can hold whatever you like. Second, and this is the one that bites, because the value is copied into callers, changing a const in a library and shipping the new DLL does not change consumers that were not rebuilt — they keep using the old number, silently. So I use const only for values that are genuinely fixed for ever, and static readonly for anything that is merely the current value.
Intermediate
Two strings have the same characters. When can == return false?
Model answer — When the static type is not string. == is an operator and operator resolution happens at compile time against the declared types, so if the variables are typed as object the compiler picks the reference comparison, because object declares no overload — and two equal strings built separately are different objects, so it returns false. Typed as string it picks string's overload and compares characters. Equals behaves differently because it is a virtual method dispatched on the actual object at run time, so it finds string's override either way. The reason this hides is interning: literals are interned by the compiler, so two identical literals really are one object and reference comparison accidentally gives the right answer in most test code. It surfaces with strings built at run time, which is why the bug appears with real data and not in tests.
Advanced
In what order do a nested finally and an outer exception filter run, and why does it matter?
Model answer — The filter runs first, before the inner finally. That surprises people because it implies the stack has not been unwound when the filter is evaluated — which is exactly right, and it is the two-pass exception model. The CLR walks the stack twice: the first pass searches for a handler and evaluates filters as it goes, with everything still live; the second pass unwinds to the handler that was found, running finally blocks on the way down. A plain catch with an if inside it behaves differently, because the body only runs after unwinding has already happened. Practically this means a filter can observe state that a catch block cannot, since the frames below are still intact — which is what makes catch (Exception e) when (Log(e)) with Log returning false a genuine technique for capturing a full picture without handling anything. It is also worth knowing that this was always in the CLR and available in Visual Basic long before C# 6 exposed it; it is a runtime capability the language caught up with.
Company lens — how the same topic gets asked
micro1Direct keyword comparisons: const vs readonly, == vs Equals.
GoogleThe mechanism underneath — static binding, metadata, the two-pass model.
MetaCode review: a struct mutated through a property, or a throwing Dispose.
AmazonConsequences: a stale const across a deployment, or a lost root-cause exception.

Hands-on

Hands-on

Write the five rules this chapter measured: which == the compiler picks, what gets baked into callers, what runs before an inner finally, whether a struct mutation sticks, and which resource is disposed first. Ten checks, each pairing a case that must be true with one that must be false. A correct solution prints score 10/10.

Run it unchanged first: ten failures and score 0/10. A stub that scores above zero means a check is not testing what it claims.

C# — Try it Yourself
Loading editor…
Output
Click Run to compile and execute the code.

Summary

  • C# interviews probe the runtime through the language. Every question in this chapter looks like syntax and is answered by the CLR.
  • == binds statically; Equals dispatches virtually. The same two strings compared as object returned false.
  • Interning hides the bug. Identical literals are one object, so reference comparison is accidentally right in most test code.
  • A const is metadata, not a field — IsLiteral is true and the value is carried in GetRawConstantValue.
  • A const is copied into every caller, so updating a library without rebuilding leaves consumers on the old value, silently.
  • Exception filters run in pass one, before any finally below them, because the CLR searches the stack before unwinding it.
  • A filter sees the stack intact, which is what makes logging from a filter capture what a catch block cannot.
  • using is a nested try/finally, so disposal is reverse of declaration — and writing the expansion beats reciting the rule.
  • A throwing Dispose destroys the original exception. The body's fault was not wrapped or attached; it was gone.
  • Whether a struct mutation sticks depends on how you reached the value. Twenty of thirty calls were discarded without a warning.

Next, in The CLR at the Whiteboard, we take the runtime this chapter kept pointing at and draw it: what you should be able to sketch from memory about loading, memory and execution, and how to do it without hand-waving.

Editorial reflection

There is a pattern in which answers count as senior, and it is not depth for its own sake. In every case here, the better answer replaced a rule with a mechanism: not "disposal is reverse" but "it is a nested try/finally, so of course it is"; not "don't use mutable structs" but "whether the mutation sticks depends on how you reached the value". Rules are compressed mechanisms, and compression is lossy in a specific way — it preserves the common case and discards the boundary, which is exactly where production failures live. That is why an interviewer keeps asking "why" after you have given a correct answer. They are not testing whether you know more facts; they are testing whether your knowledge decompresses. Knowing a mechanism lets you derive the rule and predict what happens at the edges the rule never mentioned, and that difference is worth more than any number of memorised comparisons — in an interview, and considerably more at three in the morning.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.