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

📚 Interview question

How does inheritance work in C#? Explain single inheritance of classes, what a derived class inherits, base calls, member hiding with new versus overriding, protected members, and the problems that deep or misused inheritance causes.

Class inheritance in C#: mechanics, base calls, hiding versus overriding, protected access, the Liskov principle and common pitfalls.

Mid-Level Theory Intermediate inheritance liskov method-hiding oop protected

🎯 Key points

  • C# classes have one base class and can implement many interfaces
  • Derived classes inherit all members except constructors and finalisers; private ones are inaccessible
  • `override` gives runtime dispatch; `new` hides and depends on the variable's static type
  • `base.Member()` calls the base implementation from a derived class
  • `protected`, `private protected` and `protected internal` control access from derived classes
  • Liskov substitution: a derived object must work anywhere the base is expected
  • Fragile base classes, deep hierarchies and inheriting for reuse are the main pitfalls
  • Seal classes by default and keep hierarchies shallow

⏱ 30-second answer

Inheritance lets a derived class reuse and specialise a base class: class SavingsAccount : Account. C# allows one base class but many interfaces. The derived class inherits all members except constructors and finalisers (private ones exist but are not accessible), can call base members with base., and can override virtual/abstract members. Declaring a member with the same name without override hides it (new), and which one runs then depends on the variable's static type, a common source of bugs. Use inheritance only for a real is-a relationship where a derived object can be used anywhere the base is expected (Liskov substitution). Deep hierarchies, inheriting just to reuse code, and fragile base classes are the main problems; composition is often better.

🗣 Full answer (senior)

How I would answer this in an interview

Inheritance lets a class derive from one base class, reusing its implementation and being substitutable for it. C# supports single inheritance of classes (plus multiple interfaces). I'd cover what's inherited, base calls, overriding vs hiding, protected, and — the senior part — why deep or misused inheritance hurts and when to prefer composition.

1. Single inheritance

public abstract class Notification(string recipient)
{
    public string Recipient { get; } = recipient;
    public DateTimeOffset CreatedAt { get; } = DateTimeOffset.UtcNow;

    public async Task SendAsync(CancellationToken ct)
    {
        Validate();
        await DeliverAsync(ct);           // varies per subclass
    }

    protected virtual void Validate() => ArgumentException.ThrowIfNullOrWhiteSpace(Recipient);
    protected abstract Task DeliverAsync(CancellationToken ct);
}

public sealed class SmsNotification(string mobile, string text, ISmsSender sender) : Notification(mobile)
{
    protected override void Validate()
    {
        base.Validate();                                   // reuse base rules
        if (text.Length > 160) throw new ArgumentException("SMS too long");
    }
    protected override Task DeliverAsync(CancellationToken ct) => sender.SendAsync(Recipient, text, ct);
}
  • A class has exactly one base class (implicitly object); multiple class inheritance isn't allowed, avoiding the "diamond problem". Multiple interfaces are allowed.
  • Structs can't inherit (they implicitly derive from ValueType); sealed classes can't be inherited.

2. What a derived class inherits

  • All members of the base except constructors, finalisers and static constructors. Private members exist in the object but aren't accessible from the derived class.
  • Constructors aren't inherited: the derived class must declare its own and chain with base(...).
  • Static members are accessible via the derived type name but aren't polymorphic.
  • Interface implementations of the base are inherited.

3. Base calls

base.Method() calls the base implementation (even if overridden). base(...) selects a base constructor. Forgetting base.Validate() in an override silently drops the base class's rules — a common fragile base class bug.

4. Overriding vs hiding with new

override new (hiding)
Requires base to be virtual/abstract/override Anything
Dispatch Run-time type Compile-time (declared) type
Called via base reference Derived version runs Base version runs
Base b = new Derived();
b.Describe();   // override -> Derived.Describe; new -> Base.Describe

Hiding means the behaviour depends on the variable's declared type — almost always a bug or a sign of poor design. The compiler warns (CS0108) if you hide without new.

5. protected members

protected exposes members to derived classes — including derived classes in other assemblies you don't control. Every protected member becomes part of the class's inheritance contract; changing it can break subclasses. Variants: private protected (derived types in the same assembly only) and protected internal.

6. Problems with deep or misused inheritance

  • Fragile base class: changes in the base break derived classes in unexpected ways (overrides relying on base call order).
  • Liskov violations: ReadOnlyCollection : Collection that throws on Add; a Square : Rectangle whose setters break expectations. Callers using the base type get surprises.
  • Inheritance for code reuse: CustomerService : BaseService just to reuse logging/helpers — couples unrelated classes and leaks base members.
  • Deep hierarchies (5+ levels): understanding behaviour requires reading the whole chain; overrides of overrides.
  • Combinatorial explosion: PdfEmailReport, PdfSftpReport, CsvEmailReport... — two dimensions of variation modelled with inheritance instead of composition.
  • Rigid at run time: you can't change a base class dynamically; with composition you can swap strategies.

Use inheritance for a genuine is-a relationship with substitutability and shared invariants (frameworks like ASP.NET Core's ControllerBase, BackgroundService, DelegatingHandler), keep hierarchies shallow, seal by default, and prefer composition for variation.

7. A production example

An importer hierarchy had BaseImporter → CsvImporter → BankCsvImporter → WestpacCsvImporter and similar branches. A change to CsvImporter.ParseLine to handle quoted fields broke two bank importers that overrode a helper it called. We replaced the hierarchy with composition: an Importer that takes an IRecordReader (CSV, fixed-width) and an IRecordMapper per bank. New banks became a mapper class plus a registration; behaviour no longer depended on override chains.

🔍 What the interviewer is checking

Checks mechanics plus design judgement about one of the most misused OOP features.

  • Green flags: single inheritance plus interfaces, override versus new hiding with static versus runtime type, protected access, LSP with an example, fragile base class, sealing by default.
  • Red flags: inheritance for code reuse everywhere, cannot explain hiding, or deep hierarchies presented as good design.

📚 Learn in depth

1. How inheritance is represented at run time

Each class's MethodTable contains:

  • a pointer to the parent MethodTable (used for casting: is/as walk this chain or use cached checks);
  • a vtable whose first slots are inherited from the parent (in the same order), followed by new virtual methods.

When a derived class overrides a method, the derived vtable's slot for that method points to the derived implementation. A virtual call loads the object's MethodTable, indexes the slot, and calls it — so Base b = new Derived(); b.M() reaches Derived.M. A new (hiding) method gets a new slot (newslot in IL), so calls through the base reference still use the base slot.

Instance fields from the base are laid out first in the object, followed by derived fields, so a reference to a derived object is also a valid base reference.

2. Constructors and inheritance order

For new SmsNotification(...): derived field initialisers → base field initialisers → base constructor body → derived constructor body. Calling virtual methods from the base constructor runs derived code before the derived constructor has initialised its fields — avoid it.

3. Liskov Substitution Principle (LSP)

Barbara Liskov's rule: objects of a subtype must be usable wherever the base type is expected without altering correctness. Concretely, a subclass must not:

  • strengthen preconditions (accept fewer inputs),
  • weaken postconditions (guarantee less),
  • break invariants of the base,
  • throw new kinds of exceptions callers aren't prepared for (NotSupportedException on Add),
  • change side-effect expectations.

BCL examples of tension: Stream has CanSeek/CanWrite flags because not every stream supports everything — the contract includes the flags, which keeps it honest. ReadOnlyCollection<T> implements IList<T> and throws on Add; IList<T>.IsReadOnly exists to signal that.

4. The fragile base class problem

public class CountingSet<T> : HashSet<T>
{
    public int Added { get; private set; }
    public new bool Add(T item) { Added++; return base.Add(item); }
}

Or the classic Java example: overriding both add and addAll when addAll internally calls add double-counts — depending on base implementation details you can't see. Inheritance couples you to the implementation of the base, not just its contract. Composition (wrap a set, delegate) couples only to the public interface.

5. Designing for inheritance

If a class is meant to be inherited:

  • Document which methods are called by which ("self-use") and what overrides must do.
  • Keep the number of virtual/protected members small; prefer template methods with protected abstract hooks.
  • Don't call overridable methods from constructors.
  • Test the extension points with real subclasses.
  • Otherwise, seal the class (CA1852 suggests sealing internal types with no subclasses; sealed classes also enable devirtualisation).

6. Inheritance with EF Core

EF Core maps hierarchies with TPH (one table, discriminator column — default, fastest), TPT (table per type, joins — slower), or TPC (table per concrete type, EF Core 7+). Deep hierarchies in the domain model create complex mappings, wide sparse TPH tables or expensive TPT joins — another practical cost of inheritance.

Try it yourself

  1. Implement a method with override and with new, call both through base and derived references, and explain the output.
  2. Decompile and look for newslot in the IL for the new version.
  3. Write a subclass that forgets base.Validate() and add a test that catches it.
  4. Refactor a two-dimensional inheritance hierarchy (format × delivery) into composition.

💻 Code

public abstract class Account(string owner)
{
    public string Owner { get; } = owner;
    public decimal Balance { get; protected set; }

    public void Withdraw(decimal amount)
    {
        decimal total = amount + Fee(amount);       // template step: fee varies by type
        if (total > Balance) throw new InvalidOperationException("Insufficient funds");
        Balance -= total;
    }

    protected virtual decimal Fee(decimal amount) => 2.50m;
}

public sealed class SavingsAccount(string owner) : Account(owner)
{
    protected override decimal Fee(decimal amount) => 0m;     // specialise
}

public sealed class BusinessAccount(string owner) : Account(owner)
{
    protected override decimal Fee(decimal amount) =>
        base.Fee(amount) + amount * 0.001m;                    // extend base behaviour
}

// Hiding: compiles with warning CS0108 unless 'new' is written, and is not polymorphic
public class Animal { public string Sound() => "..."; }
public class Dog : Animal { public new string Sound() => "Woof"; }

Animal pet = new Dog();
Console.WriteLine(pet.Sound());          // "..." (static type Animal decides)
Console.WriteLine(((Dog)pet).Sound());   // "Woof"

⚠ Common mistakes

1. Inheriting for code reuse

What happens: OrderService : BaseService to reuse logging and helper methods. Why it is wrong: creates is-a relationships that aren't true, couples unrelated classes, and exposes base members. Do instead: compose helpers or inject services.

2. Hiding with new instead of overriding

What happens: a derived class redeclares a method with new (or ignores CS0108). Why it is wrong: behaviour depends on the declared type; calls through the base reference run the old code. Do instead: make the base member virtual and override it, or rename the method.

3. Forgetting to call base in overrides

What happens: an override replaces base logic that should still run (validation, disposal). Why it is wrong: silently drops invariants or resource cleanup. Do instead: call base.Method() where the contract requires it, or use template methods so the base controls flow.

4. Violating Liskov substitution

What happens: subclasses throw NotSupportedException or ignore base behaviour. Why it is wrong: code written against the base type breaks with the subtype. Do instead: design hierarchies so every subtype honours the base contract, or use composition/separate interfaces.

5. Deep hierarchies

What happens: five or more levels of inheritance with overrides at each level. Why it is wrong: behaviour is hard to trace and changes ripple unpredictably. Do instead: keep hierarchies shallow (one or two levels) and compose behaviour.

6. Modelling multiple dimensions with inheritance

What happens: classes like PdfEmailReport, CsvEmailReport, PdfSftpReport. Why it is wrong: combinatorial explosion of classes. Do instead: compose independent strategies (formatter + delivery channel).

7. Exposing too many protected members

What happens: fields and helpers marked protected "in case subclasses need them". Why it is wrong: every protected member is a public contract for subclasses; changing it breaks them. Do instead: keep state private; expose a few protected hooks deliberately.

8. Not sealing classes

What happens: classes are left open by default. Why it is wrong: unintended subclassing, fragile base class problems, and missed JIT devirtualisation. Do instead: seal classes unless designed for inheritance.

🏛 Senior insight: trade-offs

Seniors use inheritance deliberately and sparingly, and seal by default.

Use inheritance when Prefer something else when
true is-a and LSP holds reuse is the only reason (composition)
framework extension point (BackgroundService) behaviour varies at run time (strategy)
small closed set of variants features combine freely (decorator)
template method with few hooks many protected members are needed

They also know the versioning angle: adding a virtual member to a public base class can break subclasses in other assemblies, so public library bases are designed for extension explicitly or sealed.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate explains override versus new Why: Checks they understand virtual dispatch
    How to answer

    Polymorphism means one interface, many forms: code written against a base type or interface works with any implementation, and each object behaves according to its actual type. Run-time (subtype) polymorphism uses virtual/override and interfaces; the call is dispatched through the object's method table (a vtable slot for virtual methods, an interface dispatch stub for interfaces), so shape.Area() runs Circle.Area or Square.Area. Compile-time polymorphism is chosen by the compiler: method overloading, generics (and static abstract interface members for generic maths). Pattern matching on types (switch on shape) is the alternative: good for closed sets of data types and operations added often; virtual methods are better when new types are added often.

  2. When: Candidate mentions fragile base classes Why: Checks they know the composition alternative
    How to answer

    Composition means a class has other objects as parts and delegates work to them, instead of inheriting behaviour. A Car has an Engine and Wheels; an OrderProcessor has a validator, a pricer and a notifier. In the strict UML sense, composition also means ownership: the parts are created and destroyed with the whole and are not shared. Benefits: behaviour can be swapped (pass a different IPricingStrategy), combined freely (decorators wrap each other), tested in isolation, and changed without a fragile base class. The cost is a little more wiring, usually handled by constructors or dependency injection.

🛠 Real-world problems: how you handle them

Fragile BaseReport hierarchy broke three reports after a small change
Situation
A reporting module had a `BaseReport` class with [about 40] protected members used by [twelve] report subclasses. A developer changed the date formatting helper in the base class for one new report.
Signals
Three existing reports showed dates in the wrong format in production; no tests failed because each subclass was tested in isolation with mocks.
Investigation
Traced the change to a protected `FormatDate` method used by many subclasses with different expectations. The base class had become a dumping ground for shared code.
Resolution
Replaced the hierarchy with a `ReportBuilder` that composes an `IReportFormatter`, an `IExporter` and a data source. Each report configures the pieces it needs; shared helpers became small injected services.
Prevention
Added snapshot tests of report outputs, a rule to seal report classes, and a design guideline to prefer composition for shared behaviour.

How to tell it (STAR)

**Situation:** At [company] a one-line change in our base report class broke the date format in three production reports. **Task:** I was asked to fix the reports and stop this kind of regression. **Action:** The base class had grown to around 40 protected members that twelve subclasses relied on in different ways, which is the fragile base class problem. I fixed the immediate bug, then replaced the hierarchy with a builder that composes a formatter, an exporter and a data source. I added snapshot tests of every report's output. **Result:** Reports now change independently; the next formatting change affected [only the intended report], and adding a new report took [hours instead of days].

↩ Usually asked after

More in Core OOP

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me