Inheritance & Polymorphism
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
Same underlying idea as java1-5, different keywords — single inheritance via :, a base constructor call via base(...).
virtual and override — Required Explicitly
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
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
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.
| Aspect | Java (java1-5) | C# |
|---|---|---|
| Dispatch default | virtual by default | non-virtual by default |
| To enable dynamic dispatch | nothing needed — always on | virtual (base) + override (derived), both required |
| Redeclaring without opting in | n/a — always overrides | method hiding — a different, non-polymorphic mechanism |
| Compile-time feedback if forgotten | n/a | a warning (CS0114), not an error |
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
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 solutionRepeat 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 solutionWrite 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 solutionChapter 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