The last post chose between an interface and an abstract class. This one is about what happens after you have a hierarchy: which method actually runs when a base-typed variable holds a derived object. Get virtual, override and new straight and you can answer almost any OOP interview question. Get them wrong and you write a bug that behaves differently depending on how the variable was declared.
Polymorphism in one example
public class Book
{
public string Title { get; set; }
public int Price { get; set; }
public virtual int GetShippingCost() => 3;
}
public class EBook : Book
{
public override int GetShippingCost() => 0;
}
public class Hardback : Book
{
public override int GetShippingCost() => 6;
}
Now the payoff:
var basket = new List<Book> { new Book(), new EBook(), new Hardback() };
foreach (var item in basket)
Console.WriteLine(item.GetShippingCost()); // 3, 0, 6
Every variable in that loop is declared Book. The compiler has no idea which subclass each item is. Yet each one runs its own version, because the decision is made at runtime, based on the actual object, not at compile time based on the declared type. That is polymorphism: one call site, many behaviours.
The value is in what is absent. There is no if (item is EBook) chain, and adding an AudioBook class tomorrow requires no change to this loop at all. A switch on type would need editing every time; a virtual method never does.
virtual and override
The two keywords are a pair, and each does exactly one thing:
virtualon a base member says: subclasses are allowed to replace this, and calls should be dispatched on the runtime type.overrideon a derived member says: this replaces the base version.
Without virtual, the base method is fixed and cannot be overridden. C# defaults to non-virtual, unlike Java where methods are virtual unless marked final. That default is deliberate: making a method virtual is a public commitment that subclasses may change its behaviour, and every caller must then tolerate that.
abstract is virtual with no body, a member that must be overridden, only legal in an abstract class:
public abstract class Product
{
public abstract int GetShippingCost(); // no body, subclass must supply one
}
sealed on an override stops the chain, so no further subclass can change it again:
public sealed override int GetShippingCost() => 0;
And sealed on a class prevents inheritance entirely. It is worth using more than most people do: sealing a class you did not design for extension prevents subclasses depending on internals you intend to change, and it lets the JIT devirtualise calls.
Calling the base version
An override does not have to discard the base behaviour:
public class SignedHardback : Hardback
{
public override int GetShippingCost() => base.GetShippingCost() + 2; // 8
}
base.X() calls the parent’s implementation non-virtually. It is essential when the base method does required work (validation, logging, raising an event), and you are adding to it rather than replacing it.
Where this matters most is constructors, and there is a genuine trap:
public class Book
{
public Book() => Initialise(); // DANGER
protected virtual void Initialise() { }
}
public class EBook : Book
{
private readonly List<string> _formats = new();
protected override void Initialise() => _formats.Add("epub"); // NullReferenceException
}
Base constructors run before derived field initialisers. So Initialise() runs the EBook override at a moment when _formats is still null. Never call a virtual method from a constructor. Every analyser warns about it, and this is why.
The new keyword, and why it lies
Here is the one that trips people up. If you hide a base member instead of overriding it:
public class Book
{
public int GetShippingCost() => 3; // NOT virtual
}
public class EBook : Book
{
public new int GetShippingCost() => 0; // hides, does not override
}
Now watch:
EBook ebook = new EBook();
Book asBook = ebook; // the same object, both times
Console.WriteLine(ebook.GetShippingCost()); // 0
Console.WriteLine(asBook.GetShippingCost()); // 3 ← same object, different answer
One object, two answers, decided purely by how the variable was declared. That is method hiding, and it is not polymorphism: it is two unrelated methods that happen to share a name, resolved at compile time.
Why does the language allow it? Versioning. If a library adds a GetShippingCost method in v2 and your subclass already had one, your code would break. new lets you say “I know, they are different methods, leave mine alone”. That is its legitimate use: silencing the compiler warning when a base class collides with your existing member.
As a design tool it is almost always a mistake, because behaviour that depends on the declared type of a variable is behaviour nobody can reason about. If you write new and you did not mean “the base class changed under me”, you probably wanted virtual/override.
The distinction in one line: override replaces the method, new hides it.
Casting and type tests
You occasionally do need the concrete type back. Modern C# has clean syntax for it:
if (item is EBook ebook) // pattern matching, test and cast in one
Console.WriteLine(ebook.DownloadUrl);
var ebook = item as EBook; // null if it is not an EBook
var forced = (EBook)item; // InvalidCastException if it is not
Use is with a pattern by default. Use as when null is a fine answer you will handle. Use a hard cast only when a wrong type is a bug you want to hear about immediately.
But treat a chain of type tests as a code smell:
if (item is EBook) return 0;
else if (item is Hardback) return 6;
else return 3;
That is a virtual method someone forgot to write. Every new subclass requires finding and editing every one of these chains, the exact problem polymorphism removes. (The pattern matching post later in this series covers the cases where a type switch genuinely is the better answer.)
Where inheritance goes wrong
Inheritance is the most over-used tool in OOP, and knowing when not to use it is the senior-level part of this topic.
It is the tightest coupling available. A subclass depends on its base class’s fields, call order, and internal behaviour, including things that were never part of a documented contract. Change the base and subclasses break in ways the compiler will not catch. That is the fragile base class problem from the last post.
“Is a” is not enough; substitutability is the test. The Liskov substitution principle says anywhere the base type is expected, a derived instance must work without surprising anyone. The textbook failure:
public class Rectangle { public virtual int Width { get; set; } public virtual int Height { get; set; } }
public class Square : Rectangle { /* setting Width must also set Height */ }
A square is a rectangle in English, and a Square that silently changes Height when you set Width breaks every method written against Rectangle. English “is a” does not imply substitutable behaviour.
Composition is usually the better answer. Instead of inheriting to get behaviour, hold the thing that has it:
public class DiscountedBook // has a pricing strategy, is not a subclass of one
{
private readonly IPricingStrategy _pricing;
public DiscountedBook(IPricingStrategy pricing) => _pricing = pricing;
public int Price => _pricing.Calculate(this);
}
The behaviour can now be swapped at runtime, combined with others, and faked in a test, none of which inheritance allows, because a base class is fixed at compile time and there is only one slot.
The practical rule: inherit when you are genuinely extending a type you own, within a shallow hierarchy (one or two levels), where every subclass is fully substitutable. Otherwise, implement an interface and compose.
The same idea inside Java, Swift and Go
Java (Netflix, Android). Same mechanism, opposite default: methods are virtual unless marked final, and @Override is an optional annotation rather than a required keyword. That means Java has no new-style hiding for instance methods, and it also means every method is an extension point whether the author intended it or not. Effective Java’s advice (“design and document for inheritance, or else prohibit it”) is the argument C# built into its defaults.
Kotlin (Google, Android). Kotlin fixed Java’s default and landed exactly where C# is, only stricter: classes and methods are final unless marked open, and override is mandatory.
open class Book { open fun shippingCost() = 3 }
class EBook : Book() { override fun shippingCost() = 0 }
Swift (Apple). override is required, and final is encouraged for performance because it lets the compiler skip dynamic dispatch. Apple pushes protocols and extensions over class inheritance for the same coupling reasons.
Go (Google). No inheritance at all. Polymorphism comes from interfaces, and reuse comes from embedding one struct in another, which is composition made syntactically convenient. Go’s designers left inheritance out deliberately, and Go services do not appear to be suffering.
Rust. Also no inheritance. Trait objects give dynamic dispatch, generics give static dispatch, and shared behaviour comes from default trait methods.
JavaScript/TypeScript (Meta). class and extends exist over prototypes, all methods are virtual, and React explicitly abandoned class inheritance for composition and hooks, one of the largest real-world tests of “composition over inheritance” ever run, and it went well.
The pattern across all of them: polymorphism is universal and treasured; implementation inheritance is increasingly optional.
Summary
- Polymorphism means the runtime type decides which implementation runs, so one call site serves many types and new subclasses need no changes at the call site.
virtualpermits overriding and enables runtime dispatch;overridereplaces the base implementation;abstractis a virtual member with no body that subclasses must supply.- C# methods are non-virtual by default, which makes extensibility a deliberate decision.
newhides rather than overrides. The same object then behaves differently depending on the declared type of the variable. Use it only to resolve a collision with a changed base class.base.Method()calls the parent implementation; never call a virtual method from a constructor, because derived fields are not initialised yet.sealedstops further overriding and helps the JIT; use it on classes not designed for inheritance.- A chain of
is-type checks is usually a missing virtual method. - Inheritance is the tightest coupling in the language. Prefer interfaces plus composition, keep hierarchies shallow, and require true substitutability.
Interview questions and answers
1. What is polymorphism?
The ability for a single call site to invoke different implementations depending on the runtime type of the object. In C# this is achieved with virtual members and interfaces, so code written against a base type or interface automatically works with types written later.
2. What is the difference between virtual, abstract and override?
virtual marks a base member that has an implementation and may be replaced. abstract marks a member with no implementation that subclasses must supply, and requires the class to be abstract too. override marks the derived member that replaces either of them and participates in runtime dispatch.
3. What is the difference between override and new?
override replaces the base implementation, so calls through a base-typed reference reach the derived version. new hides the base member with an unrelated one, and which method runs is decided at compile time from the declared type of the variable, so the same object can give two different answers. Only override is polymorphism.
4. Why are C# methods non-virtual by default?
Because making a member virtual is a commitment that subclasses may change its behaviour, and every caller must remain correct when they do. Requiring the author to opt in keeps the extension surface deliberate and documented, and it lets the JIT dispatch non-virtual calls directly.
5. Can you override a non-virtual method?
No. Without virtual or abstract on the base member, override is a compile error. The only way to introduce a same-named member is new, which hides rather than overrides and does not give polymorphic behaviour.
6. What does sealed do?
On a class it prevents inheritance entirely. On an override it stops that member being overridden further down the chain. Sealing communicates that a type was not designed for extension and allows the runtime to devirtualise calls, which can be a measurable optimisation.
7. Why should you not call a virtual method from a constructor?
Because the base constructor runs before the derived class’s field initialisers and constructor body. If the base constructor calls a virtual method that the derived class overrides, that override executes against a half-initialised object, typically producing a NullReferenceException on fields that look obviously assigned in the source.
8. What is the Liskov substitution principle?
That an instance of a derived type must be usable anywhere the base type is expected without breaking the caller’s expectations. Overrides should not strengthen preconditions, weaken postconditions, or throw where the base contract said they would not. English “is a” is not sufficient: the classic square/rectangle case satisfies the phrase and violates the principle.
9. Why is composition often preferred over inheritance?
Because inheritance is compile-time, single-slot and deeply coupled: a subclass depends on internals of its base, and changes to the base propagate silently. Composition holds a collaborator behind an interface, so behaviour can be swapped at runtime, combined freely, injected for tests, and changed without touching the consumer.
10. How do you decide between overriding a method and using an interface?
Override when you are specialising one step of a type you genuinely extend and want to reuse its state and behaviour. Use an interface when the behaviour is a capability that unrelated types should provide, when implementers may already have a base class, or when you need a swappable, mockable seam.