📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

How do methods work in C#? Explain instance versus static methods, method signatures and overloading (and how overload resolution picks a method), expression-bodied methods, and extension methods including the C# 14 extension members syntax.

C# methods end to end: instance and static, signatures, overloading rules, expression-bodied form, and classic and C# 14 extension members.

Mid-Level Theory Intermediate csharp extension-methods language-basics methods overloading

🎯 Key points

  • A signature is the name plus parameter types, order and ref/out/in kinds; the return type is not part of it
  • Instance methods use `this`; static methods belong to the type and cannot be overridden
  • Overload resolution picks the best match at compile time; ties give CS0121
  • Exact types beat conversions, specific beats general, normal form beats `params` expansion
  • Optional parameter defaults are compiled into callers, so prefer overloads in public libraries
  • Extension methods are static, resolved at compile time, and lose to applicable instance methods
  • C# 14 `extension` blocks add extension properties and static extension members
  • Expression-bodied methods suit single expressions; keep methods small with few parameters

⏱ 30-second answer

A method is a named block of code with a signature: its name plus the number, types, order and ref/out/in kinds of its parameters (the return type is not part of it, so you cannot overload by return type alone). Instance methods run on an object and can use this; static methods belong to the type and cannot. Overloading means several methods with the same name and different signatures; the compiler picks the best match at compile time, preferring exact types over conversions and normal forms over params. Expression-bodied methods (=> expr) are short forms for one expression. Extension methods are static methods with a this parameter that look like instance methods; an instance method always wins if one fits. C# 14 adds extension blocks, which also allow extension properties and static extension members.

🗣 Full answer (senior)

How I would answer this in an interview

I cover four things: instance vs static (what this is and where state lives), signatures and overloading (and how the compiler picks an overload), expression-bodied methods (syntax only), and extension methods, including the new C# 14 extension blocks.

1. Instance vs static methods

Instance method Static method
Called on An object: order.AddLine(...) The type: Order.Create(...)
Has this Yes (hidden first argument) No
Can access Instance and static members Only static members (or what is passed in)
Can be virtual / implement interfaces Yes Only via static abstract/static virtual interface members (C# 11)
Typical use Behaviour that uses or changes the object's state Factories, pure helpers, utilities

Static methods with no state are easy to test and reason about. Static methods that touch static mutable state (caches, singletons in statics) are the opposite: hidden global state, hard to test, and not thread-safe by default. In ASP.NET Core, shared state belongs in DI singletons, not static fields.

2. Method signatures and overloading

A method's signature for overloading is its name, the number of type parameters, and the types and ref-kinds of its parameters. It does not include:

  • the return type,
  • parameter names,
  • params, optional defaults,
  • whether a reference type is nullable (string vs string?).

So int Parse(string s) and long Parse(string s) cannot coexist. M(int) and M(ref int) can, but M(ref int) and M(out int) cannot.

3. How overload resolution picks a method

The compiler does this at compile time using the static types of the arguments:

  1. Candidate set — all accessible methods with that name in the type (and base types; derived-class methods are preferred first, which can surprise you).
  2. Applicable set — those whose parameters can accept the arguments, considering implicit conversions, params expanded form, optional parameters and named arguments.
  3. Better function member — for each argument, the parameter type with the better conversion wins: identity beats implicit numeric conversion; a more specific type beats a less specific one (string over object); non-generic beats generic when otherwise equal; normal form beats params expanded form; candidates that do not need optional defaults filled beat those that do.
  4. If no single method is better than all others → CS0121 ambiguous call.

Pitfalls:

void Log(object o) => Console.WriteLine("object");
void Log(string s) => Console.WriteLine("string");

object value = "hi";
Log(value);   // "object" - chosen by STATIC type, not runtime type
Log(null);    // "string" - string is more specific than object

Overloading is not polymorphism: runtime dispatch only happens for virtual/interface calls on the receiver. If you need dispatch on argument runtime types, use pattern matching (or dynamic, rarely).

4. Expression-bodied methods

public decimal Gst() => Subtotal * 0.1m; — identical IL to a block body. Use for single-expression methods; use a block when there are guards, logging or multiple steps.

5. Extension methods

Classic syntax (C# 3):

public static class StringExtensions
{
    public static bool IsBlank(this string? value) => string.IsNullOrWhiteSpace(value);
}

if (name.IsBlank()) { ... }   // compiled as StringExtensions.IsBlank(name)
  • Static methods in a non-generic static class, this on the first parameter.
  • Pure syntax sugar: no access to private members, no virtual dispatch, resolved at compile time by namespace (using).
  • Instance methods always win over extension methods with the same applicable signature.
  • They can be called on null receivers (they are just static methods), which is why IsBlank above works on null.

C# 14 extension members add extension blocks that support extension properties, static extension members and operators, not just methods:

public static class EnumerableExtensions
{
    extension<T>(IEnumerable<T> source)
    {
        public bool IsEmpty => !source.Any();                     // extension property
        public IEnumerable<T> WhereNotNull() => source.Where(x => x is not null);
    }

    extension<T>(IEnumerable<T>)
    {
        public static IEnumerable<T> Empty => [];                 // static extension member
    }
}

if (orders.IsEmpty) { ... }

Existing this-parameter extension methods remain valid and binary compatible; the new syntax compiles to the same kind of static methods plus metadata.

6. A production example

A team added public static class StringExtensions { public static string ToSlug(this string s) ... } in a shared package, and a different package added another ToSlug extension with slightly different rules. Depending on which using was in scope, URLs were generated differently, and two services produced different slugs for the same product, breaking links. We resolved it by owning one implementation in a single namespace, adding analyser rules for banned APIs (BannedSymbols.txt with Microsoft.CodeAnalysis.BannedApiAnalyzers), and preferring explicit static helpers for business rules. Extensions are great for fluent, discoverable APIs (services.AddOrderingModule()), but business logic hidden behind them is hard to find and easy to duplicate.

🔍 What the interviewer is checking

Checks solid knowledge of C# method rules and API design sense.

  • Green flags: defines the signature correctly, explains overload resolution preferences and ambiguity, knows optional parameter versioning, explains extension method resolution and C# 14 extension members.
  • Red flags: believes you can overload by return type, thinks extension methods override instance methods, or writes very long methods with many boolean flags.

📚 Learn in depth

1. Instance methods: the hidden this

In IL, an instance method has an implicit first argument ldarg.0 = this. Calls use call or callvirt:

  • C# emits callvirt for almost all instance calls on reference types — even non-virtual ones — because callvirt performs a null check on the receiver, giving you NullReferenceException at the call site rather than inside the method.
  • For virtual methods, callvirt dispatches through the method table (vtable). For interface methods, the runtime uses virtual stub dispatch with per-call-site caches.
  • Static methods use plain call: no receiver, no null check, no dispatch — which makes them easy to inline.

For structs, this is a managed reference (ref T) to the struct, which is why a mutating struct method changes the variable it is called on (and why calling it on a copy changes the copy).

2. Overload resolution details worth knowing

Better conversion target rules (simplified):

  1. Identity conversion beats any other.
  2. If there is an implicit conversion from T1 to T2 but not back, T1 is better (string is better than object; int is better than long).
  3. Signed integral types beat unsigned ones for the same size in certain cases (int over uint).
  4. For lambdas, the candidate whose delegate return type matches the lambda body better wins.

Tie-breakers when conversions are equal:

  • Non-generic beats generic (M(string) over M<T>(T)).
  • More specific generic beats less specific (M<T>(List<T>) over M<T>(T)).
  • Normal form beats params expanded form.
  • A candidate with all arguments matching explicitly beats one needing default values.
  • C# 13 added OverloadResolutionPriorityAttribute, letting library authors steer callers toward a new overload (e.g. a span overload) without breaking existing ones.
  • C# 13/14 also prefer ReadOnlySpan<T> overloads with first-class span conversions in more cases.

The base/derived trap: if a derived class declares an applicable method, the compiler does not even consider base-class overloads that might be a better match:

class Base    { public void Write(int x) => Console.WriteLine("Base.int"); }
class Derived : Base { public void Write(double x) => Console.WriteLine("Derived.double"); }

new Derived().Write(5);   // "Derived.double" - int converts to double

3. Overloading vs overriding vs hiding

Concept Same name, different... Resolved Keyword
Overloading Parameter list Compile time none
Overriding Same signature, derived class Run time (vtable) virtual / override
Hiding Same signature, derived class Compile time (static type) new

Hiding with new is almost always a smell — which method runs depends on the variable's declared type.

4. How extension methods are compiled and resolved

name.IsBlank();
// compiles to
StringExtensions.IsBlank(name);

The method is marked with [Extension] in metadata. Lookup order:

  1. The compiler first tries instance members of the receiver's type. If any applicable instance method exists, extension methods are not considered.
  2. Otherwise it searches static classes in enclosing namespaces, then using directives, from the innermost scope outwards, stopping at the first scope that has an applicable candidate.

Consequences:

  • Adding an instance method with the same name to a type silently changes which method runs in all callers on the next compile.
  • Two extensions with the same signature in different imported namespaces → ambiguity error, or a silent switch if one scope is "closer".
  • Extensions on interfaces (IEnumerable<T>) are how LINQ adds dozens of operators without changing the interface.

5. C# 14 extension members under the hood

The extension(Receiver r) { ... } block declares members for a receiver type. The compiler emits:

  • Ordinary static methods (so existing callers and reflection see familiar methods), plus
  • Metadata describing the extension block, so the compiler can expose extension properties (orders.IsEmpty), static extension members (IEnumerable<T>.Empty), and user-defined operators on types you do not own.

The disambiguation syntax still exists: you can call EnumerableExtensions.get_IsEmpty(orders)-style members via the containing static class when two extensions clash. Extension properties are still computed on every access — they cannot add state to the target type.

6. Static abstract interface members (C# 11)

Static methods can now take part in polymorphism through generics:

public interface IParsable<TSelf> where TSelf : IParsable<TSelf>
{
    static abstract TSelf Parse(string s, IFormatProvider? provider);
}

static T ParseAll<T>(string s) where T : IParsable<T> => T.Parse(s, CultureInfo.InvariantCulture);

This is the basis of .NET 7+ generic math (INumber<T>). The call is resolved per closed generic type, with no virtual dispatch at run time for value types.

Try it yourself

  1. Reproduce the base/derived overload trap and fix it with a using-free design or by adding the overload to the derived class.
  2. Add an instance method with the same name as an existing extension method and watch call sites switch silently.
  3. Write a C# 14 extension property on IEnumerable<T> and look at the generated metadata in ILSpy.
  4. Call Log(null) with Log(object) and Log(string), then add Log(Uri) and read CS0121.

💻 Code

public sealed class Order
{
    private readonly List<OrderLine> _lines = [];

    // Static factory: no 'this', validates input
    public static Order Create(int customerId)
    {
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(customerId);
        return new Order { CustomerId = customerId };
    }

    public int CustomerId { get; private init; }

    // Overloads: same intent, different inputs
    public void AddLine(OrderLine line) => _lines.Add(line);
    public void AddLine(string sku, int quantity, decimal price) =>
        AddLine(new OrderLine(sku, quantity, price));

    // Expression-bodied instance method
    public decimal Total() => _lines.Sum(l => l.Quantity * l.Price);
}

public sealed record OrderLine(string Sku, int Quantity, decimal Price);

// Classic extension method
public static class OrderExtensions
{
    public static bool IsLarge(this Order order) => order.Total() > 1_000m;
}

// C# 14 extension members: methods, properties and static members
public static class StringExtensions
{
    extension(string? s)
    {
        public bool IsBlank => string.IsNullOrWhiteSpace(s);              // extension property
        public string Truncate(int max) =>
            s is null ? "" : s.Length <= max ? s : s[..max];
    }

    extension(string)
    {
        public static string JoinLines(IEnumerable<string> lines) =>     // static extension
            string.Join(Environment.NewLine, lines);
    }
}

// Usage
var order = Order.Create(customerId: 7);
order.AddLine("SKU-1", 2, 19.95m);
bool large = order.IsLarge();
bool blank = "  ".IsBlank;
string text = string.JoinLines(["a", "b"]);

⚠ Common mistakes

1. Expecting overloads to dispatch on runtime type

What happens: Handle(object o) and Handle(Order o) overloads, called with a variable typed object that contains an Order. Why it is wrong: overloads are chosen at compile time from the static type, so Handle(object) runs. Do instead: use virtual methods, a switch on type patterns, or a dictionary of handlers.

2. Overloads that differ only subtly

What happens: Send(string to, int retries) and Send(string to, long timeoutMs). Why it is wrong: callers passing an int literal get the first; changing a literal to a variable of type long silently switches overloads. Do instead: give methods different names (SendWithRetries) or use an options record.

3. Mixing optional parameters with overloads

What happens: Get(int id) plus Get(int id, bool includeLines = false). Why it is wrong: confusing resolution rules (the one without defaults wins), and adding more optional parameters later becomes ambiguous. Do instead: choose one approach per method group; prefer overloads for public APIs because they are binary-compatible to extend.

4. Static methods with hidden mutable state

What happens: static Dictionary<string, Rate> _cache populated by static helper methods in a web app. Why it is wrong: not thread-safe, impossible to reset in tests, and shared across all tenants/requests. Do instead: move state into a DI-registered singleton using concurrent or immutable collections, or IMemoryCache.

5. Making everything static "for performance"

What happens: services become static classes to avoid DI. Why it is wrong: the call overhead difference is negligible; you lose testability (no substitution), configuration and lifetime management. Do instead: use static for pure, stateless helpers; use DI for anything with dependencies or state.

6. Extension methods with business logic

What happens: order.CalculateDiscount() implemented as an extension in a utilities namespace. Why it is wrong: core rules become hard to find, cannot access private state, and multiple versions can appear in different namespaces. Do instead: put domain behaviour on the domain type; keep extensions for fluent configuration (services.AddX()), adapters and convenience over types you do not own.

7. Extension methods that hide null handling

What happens: customer.GetDisplayName() works when customer is null because the extension checks for null. Why it is wrong: it looks like an instance call, so readers assume customer is non-null; it masks bugs. Do instead: keep null-accepting extensions rare and obvious (IsNullOrBlank), and annotate parameters with nullability.

8. Ambiguous or shadowed extensions across packages

What happens: two libraries define ToSlug or ToJson extensions for the same type. Why it is wrong: compile errors at best; at worst, the closer namespace silently wins and behaviour differs between projects. Do instead: own extensions in one well-named namespace, avoid generic names on common types, and use banned-API analysers.

9. Using new to hide base methods

What happens: a derived class declares public new void Save(). Why it is wrong: which method runs depends on the variable's declared type; calls through the base type skip the new logic. Do instead: make the base method virtual and override it, or rename the derived method.

10. Expression-bodied methods for multi-step logic

What happens: a long chain of calls and ternaries squeezed into one => line. Why it is wrong: hard to read and debug; breakpoints cover the whole expression. Do instead: use a block body once there is more than one obvious step.

🏛 Senior insight: trade-offs

Seniors think about methods as API surface: how they evolve, how callers bind to them, and how clear they are.

Decision Senior default Reason
Static or instance static for pure helpers and factories no hidden state
Overloads vs optional params overloads in public libraries versioning safety
Many parameters options record readability, evolvability
Extension vs instance instance on owned types encapsulation
Extension properties (C# 14) for read-only computed helpers discoverability
Expression-bodied single, clear expression only readability

They also know binding is mostly compile time: overloads and extension methods are chosen by static types, only virtual dispatch happens at run time. That explains many "wrong method called" surprises.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions parameters and optional arguments Why: Checks ref, out, in, params and argument passing
    How to answer

    C# passes everything by value by default. For a value type that copies the data; for a reference type it copies the reference, so the method can change the object but reassigning the parameter does not affect the caller. ref passes the caller's variable itself; out is a ref the callee must assign, used by TryParse; in and ref readonly pass a read-only reference, useful for large structs. params takes a variable number of arguments, and since C# 13 it can be a ReadOnlySpan<T> to avoid allocating. Optional parameters' defaults are compiled into the caller, which matters when versioning public APIs.

  2. When: Candidate talks about what methods should return Why: Checks return type design
    How to answer

    The return type is the contract, so I would make it honest. Not-found becomes Task<Order?> with nullable reference types on, so callers are forced to handle null, or a Result<Order> if there are several expected failure reasons. The two out parameters must go because async methods cannot have out parameters; their information moves into the result object. The internal List<Order> is replaced with IReadOnlyList<Order> (or a copy) so callers cannot mutate our state. C# gives us void, values, tuples, records, out/Try pattern, ref returns, IEnumerable<T>/IAsyncEnumerable<T>, and Task/ValueTask.

  3. When: Candidate mentions helper methods used in one place Why: Checks local functions
    How to answer

    A local function is a named method declared inside another method. Unlike a lambda it can be recursive, generic, an iterator or async, and called before it is declared. The big practical difference is allocation: if a local function captures variables and is only called directly, the compiler passes them in a struct by reference, so there is no heap allocation, while a capturing lambda always allocates a closure object and a delegate. A static local function cannot capture anything, which guarantees no hidden allocations. I use them for helpers used in one method and for the eager-validation pattern in iterators and async methods.

🛠 Real-world problems: how you handle them

Changed optional parameter default did not reach services until rebuilt
Situation
A shared NuGet library exposed `Send(string to, string subject, bool trackOpens = false)`. A new version changed the default to `true` for compliance reporting.
Signals
After deploying the new package to the shared folder, some services tracked opens and others did not; only services rebuilt against the new version picked up the change.
Investigation
Decompiled a caller and saw `Send(to, subject, false)` with the old default embedded at the call site. Optional parameter defaults are compiled into the caller, not read from the library at run time.
Resolution
Replaced the optional parameter with an overload set and an `EmailOptions` record whose defaults live inside the library, then rebuilt all consumers once.
Prevention
Added an API review rule: no optional parameters on public library methods; use overloads or options objects. Enabled package validation to flag breaking API changes.

How to tell it (STAR)

**Situation:** At [company] our email library changed a default so that open tracking was on, but after the release only some services behaved differently. **Task:** I was asked why the change was inconsistent. **Action:** I decompiled one of the services and found the old default baked into its call. That is how C# optional parameters work: the value is compiled into the caller. I redesigned the public API with overloads and an options record so defaults live inside the library, rebuilt the consumers, and added an API review rule plus package validation. **Result:** All services behaved the same after one coordinated rebuild, and later default changes rolled out with a package update alone.

↩ Usually asked after

More in Language Basics

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me