Inheritance & Polymorphism

Course 1 · Ch 5
Inheritance & Polymorphism
The exact reverse of java1-5's virtual-by-default dispatch — and a gotcha the compiler only warns about

This is the chapter where C# and Java's shared architecture stops predicting shared behavior. java1-5 made every instance method virtual unless told otherwise. C# does the opposite.

base and Inheritance Basics

class Animal { protected string Name; public Animal(string name) { Name = name; } } class Dog : Animal { // : instead of extends public Dog(string name) : base(name) {} // base(...) instead of super(...) }

Same underlying idea as java1-5, different keywords — single inheritance via :, a base constructor call via base(...).

virtual and override — Required Explicitly

class Animal { public virtual string Speak() => "..."; // must opt IN to being overridable } class Dog : Animal { public override string Speak() => "Woof"; // must opt IN to overriding it } Animal a = new Dog(); a.Speak(); // "Woof" — dynamic dispatch, because BOTH keywords were present

Here's the real reversal: in C#, a method is not dynamically dispatched by default. java1-5 established that every Java instance method is virtual unless explicitly marked final/private/static. C# requires the base class to mark a method virtual and the derived class to mark its own version override — without both keywords present, calling that method through a base-typed reference always runs the base class's own version, regardless of the object's real runtime type.

What Happens Without virtual/override — Method Hiding

class Animal { public string Speak() => "..."; // NOT virtual } class Dog : Animal { public new string Speak() => "Woof"; // hides, does not override } Animal a = new Dog(); a.Speak(); // "..." — Animal's own version, based on the DECLARED type ((Dog)a).Speak(); // "Woof" — only when accessed through the Dog-typed reference

Redeclaring a method with the same signature but no override creates an entirely different, non-polymorphic mechanism called method hiding — the new keyword makes this explicit. Which version runs depends on the reference's declared type, not the object's real runtime type — the opposite of what java1-5 trained every reader to expect from an object hierarchy.

Why C# Requires Opt-In Virtual

Two real, deliberate reasons, not accidents of history: a non-virtual method call can be resolved at compile time, avoiding the runtime dispatch-table lookup a virtual call requires — a genuine performance difference at scale. And a base class author is forced to explicitly decide, and effectively document, which methods are meant to be extension points — rather than every method automatically becoming one whether the original author intended it or not, as java1-5's virtual-by-default makes true for every Java class.

sealed — Preventing Further Overriding

public override sealed string Speak() => "Woof"; // no further class may override THIS override public sealed class FinalBreed : Dog {} // prevents inheritance from this class entirely

sealed override stops a further subclass from overriding an already-overridden method; sealed class prevents any further inheritance at all — genuinely comparable to Kotlin's own classes being closed to inheritance by default, an ironic point of alignment given C# otherwise opts into virtual dispatch the same non-default way Kotlin does.

AspectJava (java1-5)C#
Dispatch defaultvirtual by defaultnon-virtual by default
To enable dynamic dispatchnothing needed — always onvirtual (base) + override (derived), both required
Redeclaring without opting inn/a — always overridesmethod hiding — a different, non-polymorphic mechanism
Compile-time feedback if forgottenn/aa warning (CS0114), not an error
Mark virtual only when a method is genuinely meant to be extended
C#'s opt-in model exists specifically so a base class's author states real intent — reach for virtual deliberately, as a design decision, not as a default habit carried over from Java.
Forgetting override is a warning, not an error — the code still runs
Redeclaring a base method's signature without override or new compiles successfully with only a CS0114 warning ("hides inherited member") — the program builds and runs, silently producing method-hiding behavior instead of the polymorphism the programmer likely intended. This is genuinely easy to miss if warnings aren't being read.

Coding Challenges

Challenge 1

Write a Shape base class with a virtual Area() method returning 0, and a Circle subclass that overrides it correctly. Store the Circle in a Shape-typed variable and call Area(), showing Circle's version runs.

📄 View solution
Challenge 2

Repeat Challenge 1, but make Shape's Area() NOT virtual, and have Circle redeclare it with new instead of override. Call Area() through a Shape-typed variable and through a Circle-typed variable, showing the two different results.

📄 View solution
Challenge 3

Write Challenge 2's Circle.Area() with neither override nor new (just a plain redeclaration), compile it, and report the exact CS0114 warning text produced, explaining why the code still compiles and runs despite it.

📄 View solution

Chapter 5 Quick Reference

  • C# requires virtual (base) AND override (derived) for dynamic dispatch — the exact reverse of java1-5's virtual-by-default
  • Redeclaring without override creates method hiding (new keyword) — dispatch depends on the reference's declared type, not the object's real type
  • Opt-in virtual buys compile-time-resolvable calls by default and forces explicit intent about what's an extension point
  • sealed override stops further overriding; sealed class stops further inheritance entirely, echoing Kotlin's own closed-by-default classes
  • Forgetting override produces only a CS0114 warning, not a compile error — the silent method-hiding gotcha still compiles and runs
  • Next chapter: interfaces and abstract classes — default interface methods since C# 8, and explicit interface implementation