Introduction
In Part 1, we ran our little Coffee Shop Order System through the wringer — method overloading, params, and enough object-array plot twists to make your head spin (in a good way, hopefully).
This time, we're parking the overloads and going all-in on Inheritance. Let's define it in a few quick bullet points before we start breaking things again:
It's a way of creating a new class from an already-existing class.
The existing class is called the base class (or super class).
The new class is called the derived class (or sub class).
Inheritance exists mainly for code reuse — the "why write it twice when the base class already did the work" principle.
A derived class inherits the variables and methods of its base class — free of charge, no extra typing required.
Roadmap
Still the same five-part journey:
Diving in OOP (Polymorphism and Inheritance – Part 1)
Diving in OOP (Polymorphism and Inheritance – Part 2) — you're here
Diving in OOP (Polymorphism and Inheritance – Part 3)
Diving in OOP (Access Modifiers)
Diving in OOP (Properties)
Note: Every code snippet here has actually been run — no theoretical guessing, just real compiler output (including the errors, especially the errors).
Inheritance in Action
Let's fire up our CoffeeShop console app again. This time, add two classes: MenuItem and SpecialMenuItem.
MenuItem.cs:
public class MenuItem
{
public int basePrice = 100;
public void ShowPrice()
{
Console.WriteLine("MenuItem ShowPrice");
}
public void ShowDescription()
{
Console.WriteLine("MenuItem ShowDescription");
}
}
SpecialMenuItem.cs (empty, for now):
public class SpecialMenuItem
{
}
SpecialMenuItem is currently as empty as a coffee cup at 9 AM before the first customer walks in. MenuItem, meanwhile, actually has stuff going on — a price and two methods.
Program.cs:
var special = new SpecialMenuItem();
special.ShowPrice();
Run this, and C# immediately slaps us with a compile-time error:
Error: 'SpecialMenuItem' does not contain a definition for 'ShowPrice' and no extension method 'ShowPrice'
accepting a first argument of type 'SpecialMenuItem' could be found
Fair enough — SpecialMenuItem really doesn't have a ShowPrice method. It's not related to MenuItem in any way, so it has no business borrowing its methods. An empty class is perfectly valid on its own (you can still create an instance of it — congratulations, it's a very boring object), but it genuinely has nothing to offer beyond existing.
Now here's where the fun starts. Let's make SpecialMenuItem inherit from MenuItem using the : operator:
public class SpecialMenuItem : MenuItem
{
}
var special = new SpecialMenuItem();
special.ShowPrice();
Output:
MenuItem ShowPrice
One colon, and suddenly our previously-empty SpecialMenuItem can do everything MenuItem can do. We didn't write a single line of logic inside SpecialMenuItem, yet it now behaves as if it has a basePrice, a ShowPrice(), and a ShowDescription(). It's basically adopted all of MenuItem's furniture without lifting a finger. This is inheritance: MenuItem is the base class, SpecialMenuItem is the derived class.
When the Derived Class Has a Method With the Same Name
What if SpecialMenuItem decides it wants its own version of ShowPrice?
public class SpecialMenuItem : MenuItem
{
public void ShowPrice()
{
Console.WriteLine("SpecialMenuItem ShowPrice");
}
}
var special = new SpecialMenuItem();
special.ShowPrice();
Output:
SpecialMenuItem ShowPrice
But — and this is the fun part — you'll also get a compiler warning:
Warning: 'SpecialMenuItem.ShowPrice()' hides inherited member 'MenuItem.ShowPrice()'.
Use the new keyword if hiding was intended.
Point to Remember: Nothing stops a derived class from declaring a method with the exact same name as one already in its base class.
When we call special.ShowPrice(), C# checks SpecialMenuItem first. Found a match? Great, done, base class never even gets consulted. This is the derived class getting "first dibs" — like a customer at the counter who cuts straight to the front because they already know the barista.
Point to Remember: The derived class always gets the first chance at execution — the base class only gets called if the derived class has nothing to offer.
Calling the Base Class Anyway, With base
What if we still want MenuItem's version of ShowPrice to run too — maybe right after our own?
public class SpecialMenuItem : MenuItem
{
public void ShowPrice()
{
Console.WriteLine("SpecialMenuItem ShowPrice");
base.ShowPrice();
}
}
var special = new SpecialMenuItem();
special.ShowPrice();
Output:
SpecialMenuItem ShowPrice
MenuItem ShowPrice
The base keyword is C#'s way of saying "yes, I know I've got my own version, but let me also reach back and grab the base class's copy." Think of it as calling your manager after you've already made a decision — just to keep them in the loop.
Point to Remember: The reserved keyword base lets a derived class explicitly call a method from its base class.
What If We Call a Different Base Method Instead?
public class SpecialMenuItem : MenuItem
{
public void ShowPrice()
{
Console.WriteLine("SpecialMenuItem ShowPrice");
base.ShowDescription();
}
}
var special = new SpecialMenuItem();
special.ShowPrice();
Output:
SpecialMenuItem ShowPrice
MenuItem ShowDescription
base isn't locked to calling "the same-named method from the parent" — it can reach any accessible member of the base class. It's basically your all-access backstage pass to the base class. One thing worth noting: you can't use base inside MenuItem itself, since MenuItem isn't derived from anything (well — technically it is, as we're about to see).
Inheritance Is a One-Way Street
public class MenuItem
{
public int basePrice = 100;
public void ShowPrice()
{
Console.WriteLine("MenuItem ShowPrice");
}
}
public class SpecialMenuItem : MenuItem
{
public void ShowDescription()
{
Console.WriteLine("SpecialMenuItem ShowDescription");
}
}
var item = new MenuItem();
item.ShowDescription();
Output:
Error: 'MenuItem' does not contain a definition for 'ShowDescription' and no extension method
'ShowDescription' accepting a first argument of type 'MenuItem' could be found
SpecialMenuItem can use everything MenuItem has. But MenuItem cannot reach into SpecialMenuItem and borrow its stuff. It's like a kid inheriting the family recipe book — the kid gets full access to grandma's recipes, but grandma doesn't suddenly know the fusion tacos her grandkid invented.
Point to Remember: Inheritance flows downward only — from base to derived, never the other way around.
Point to Remember: Except for constructors and destructors, a derived class inherits everything from its base class.
If PremiumSpecialMenuItem derives from SpecialMenuItem, which derives from MenuItem, then PremiumSpecialMenuItem inherits from both — this chaining is called transitive inheritance. A derived class can hide a base class member by redeclaring it with the same name, but it can never actually delete or remove a base class member — the original just sits there, quietly unreachable from the derived class's namesake method, but completely intact in the base class itself.
A Quick Word on Static vs. Instance Members
While we're on the topic of what gets inherited, it's worth pausing on something easy to gloss over: every class member is either a static member (belongs directly to the class itself) or an instance member (belongs to one specific object created from that class).
public class MenuItem
{
public int basePrice = 100; // instance member — every cup has its own price
public static string ShopName = "The Byte-Sized Café"; // static member — belongs to the class itself
}
An instance member is only reachable through an actual object — special.basePrice, not MenuItem.basePrice. A static member works the other way around — you reach it through the class name directly, and every object shares the exact same copy, like a shop-wide announcement board rather than something taped to one specific cup. By default, anything you declare in a class is an instance member — you only get static behavior by explicitly adding the static keyword.
Everyone's Ultimate Base Class: object
Also worth knowing: every class in C# ultimately derives from object, whether you asked for it or not. Take this innocent-looking code:
public class MenuItem
{
}
public class SpecialMenuItem : MenuItem
{
}
Behind the scenes, at compile time, C# quietly rewrites this as:
public class MenuItem : object
{
}
public class SpecialMenuItem : MenuItem
{
}
You never typed : object anywhere, but it's there anyway. object is the one class that isn't derived from anything else — the ultimate ancestor, the OG base class, present at every family reunion whether invited or not. So technically, SpecialMenuItem's real family tree is MenuItem and object — even though only MenuItem was ever mentioned by name.
You Can't Inherit From Just Any Built-In Class
public class CupSize : System.ValueType { }
public class DrinkCategory : System.Enum { }
public class OrderCallback : System.Delegate { }
public class MenuList : System.Array { }
Run this, and C# throws a small pile of errors at you:
Error: 'CupSize' cannot derive from special class 'System.ValueType'
Error: 'DrinkCategory' cannot derive from special class 'System.Enum'
Error: 'OrderCallback' cannot derive from special class 'System.Delegate'
Error: 'MenuList' cannot derive from special class 'System.Array'
Don't panic — this is entirely expected. Some built-in C# classes are marked "special," meaning the language reserves them for its own internal plumbing. You're not allowed to derive your own classes from them, no matter how good your reason sounds.
Point to Remember: Custom classes cannot derive from special built-in classes like System.ValueType, System.Enum, System.Delegate, and System.Array.
One Base Class Only, Please
public class MenuItem { }
public class Beverage { }
public class SpecialCombo : MenuItem, Beverage { }
Error: Class 'SpecialCombo' cannot have multiple base classes: 'MenuItem' and 'Beverage'
C# only lets a class have one base class — no double-dipping. If you genuinely need behavior from multiple sources, that's what interfaces are for (a topic for another day, deliberately not covered here).
Point to Remember: A class can only derive from one class in C#. C# does not support multiple inheritance through classes.
No Going in Circles
public class Espresso : Mocha { }
public class Mocha : Latte { }
public class Latte : Espresso { }
Error: Circular base class dependency involving 'Mocha' and 'Espresso'
This one reads perfectly reasonably at a glance — Espresso derives from Mocha, Mocha derives from Latte, Latte derives from Espresso — and that's exactly the problem. It's a closed loop with no actual starting point, like three people each insisting the other one owes them coffee money. C# refuses to untangle it, because logically, it can't be untangled.
Point to Remember: Circular dependency in inheritance is not allowed — a chain of classes cannot loop back on itself.
Equalizing the Instances/Objects
Let's try something slightly different — comparing two completely unrelated classes that just happen to look alike.
public class Invoice
{
public int amount = 100;
}
public class Receipt
{
public int amount = 100;
}
var receipt = new Receipt();
var invoice = new Invoice();
invoice = receipt;
receipt = invoice;
Error: Cannot implicitly convert type 'Receipt' to 'Invoice'
Error: Cannot implicitly convert type 'Invoice' to 'Receipt'
Both classes have an identical-looking amount field, both set to 100. Doesn't matter one bit — C# doesn't care that they look the same on the inside. Unless one is actually derived from the other, they're strangers to each other as far as the compiler is concerned. C# doesn't do "close enough."
But What If One Derives From the Other?
public class Invoice
{
public int amount = 100;
}
public class Receipt : Invoice
{
public int tax = 10;
}
var invoice = new Invoice();
var receipt = new Receipt();
invoice = receipt; // this line works!
receipt = invoice; // but this one won't
The first assignment, invoice = receipt, compiles just fine. Since Receipt derives from Invoice, every Receipt genuinely is an Invoice, plus a little extra (tax). Assigning it to an Invoice-typed variable is safe — you're just choosing to see the receipt as "an invoice," ignoring its extra tax field for now.
The second line, receipt = invoice, throws:
Error: Cannot implicitly convert type 'Invoice' to 'Receipt'. An explicit conversion exists (are you missing a cast?)
Going the other way isn't automatically safe — an Invoice doesn't necessarily have a tax field, so C# won't silently assume it does.
Point to Remember: You can only equate objects of unrelated classes if one is actually derived from the other — and even then, only from derived-to-base directly. Base-to-derived needs your explicit permission.
Forcing It With a Cast
var invoice = new Invoice();
var receipt = new Receipt();
invoice = receipt;
receipt = (Receipt)invoice;
Adding (Receipt) in front tells C# "trust me, I know what I'm doing" — and since invoice actually holds a real Receipt object underneath (we just assigned one to it a line earlier), the cast succeeds. This only works because there's a genuine inheritance relationship between the two. Try this same trick between two totally unrelated classes, and C# won't budge:
public class Invoice { public int amount = 100; }
public class Receipt { public int tax = 10; } // no longer derived from Invoice
var invoice = new Invoice();
var receipt = new Receipt();
receipt = (Receipt)invoice;
invoice = (Invoice)receipt;
Error: Cannot convert type 'Invoice' to 'Receipt'
Error: Cannot convert type 'Receipt' to 'Invoice'
No inheritance relationship means no cast is going to save you. Casting isn't magic — it only works when there's already a real family connection between the two types.
A Quick Bonus Rule (Not Related to Classes, But Equally Sneaky)
int cupCount = 10;
char sizeCode = 'A';
cupCount = sizeCode; // works fine
sizeCode = cupCount; // does not
Error: Cannot implicitly convert type 'int' to 'char'. An explicit conversion exists (are you missing a cast?)
Point to Remember: A char can be implicitly converted to an int, but not the other way around without an explicit cast.
Conclusion
That's inheritance covered, top to bottom, one broken build at a time. In the next part of the series, we'll dive into run-time polymorphism — and inheritance is going to matter a lot there too, since you can't really have one without the other.
Here's the full round-up of everything to remember:
A derived class can declare a method with the same name as one in its base class — nothing stops it.
The derived class always gets first chance at execution; the base class only runs if the derived class doesn't have its own version.
The base keyword lets a derived class explicitly call a member of its base class.
Inheritance only flows downward — base to derived, never derived to base.
Except constructors and destructors, a class inherits everything from its base class.
Custom classes cannot derive from special built-in classes like System.ValueType, System.Enum, System.Delegate, or System.Array.
A class can only derive from one class in C# — no multiple inheritance through classes.
Circular dependency in inheritance is not allowed.
You can only equate objects of two different classes if one is derived from the other — and only in the derived-to-base direction automatically.
int cannot implicitly convert to char, but char can implicitly convert to int.
If you missed Part 1, go check out our adventures with method overloading and the params keyword in the Coffee Shop Order System — it's where this whole series started brewing. Keep coding, keep learning, and maybe grab a coffee while you're at it. ☕