Lesson 8 • Intermediate Track
Inheritance & Polymorphism
By the end of this lesson you'll be able to build a family of related classes that share code through inheritance, replace behaviour with virtual/override, and write one piece of code that works for every type via polymorphism — the OOP pillars that keep large programs flexible.
class with fields, properties, a constructor, and methods. Inheritance builds directly on those.What You'll Learn
- Create a derived class that inherits from a base class with the : syntax
- Pass data to the parent with : base(...) and chain constructors
- Replace behaviour using virtual on the base and override in the derived class
- Reuse the parent's logic with base.Method() instead of copying it
- Apply polymorphism — one base-typed loop, the right method every time
- Define abstract classes, contrast them with interfaces, use sealed, is and as
💡 Real-World Analogy
Inheritance is like biology. "Dog", "Cat" and "Bird" are all specialised kinds of "Animal": they inherit the general traits (a name, the ability to eat) and then add or change their own (a dog barks, a bird flies). You write the shared parts once in the base class instead of copying them into every animal. Polymorphism is like a universal remote's "play" button — you press the same button, but it does the right thing depending on whether a TV, a speaker, or a games console is listening. In code, you call Speak() on an Animal and each object responds in its own way.
📋 Inheritance Keywords Reference
| Keyword | Where it goes | What it does |
|---|---|---|
| : | Class header | class Dog : Animal — Dog inherits Animal |
| : base(...) | Constructor | Calls the parent's constructor |
| virtual | Base method | Allows derived classes to override it |
| override | Derived method | Replaces the base implementation |
| base.X() | Derived method | Calls the parent's version of a method |
| abstract | Class / method | Cannot be instantiated; must be implemented |
| sealed | Class / method | Prevents any further inheritance/override |
| is | Expression | if (a is Dog d) — check & cast |
| as | Expression | a as Cat — cast, or null on failure |
1. Base & Derived Classes
A base class (the parent) holds traits shared by a whole family of things. A derived class (the child) is written with class Child : Parent and automatically gets every public field, property and method of the parent — you don't rewrite them. The phrase to remember is "is a": a Car is a Vehicle, so Car : Vehicle is correct. When the parent has a constructor with parameters, the child must pass them up with : base(...). The base constructor runs first, then the child's. Read this worked example, run it, then you'll write your own.
Worked example: Car inherits from Vehicle
Read every comment, run it, and check the output matches.
using System;
// Base class (the "parent") — holds traits shared by ALL vehicles.
class Vehicle
{
public string Brand { get; set; }
public int Year { get; set; }
// Constructor: sets the shared fields when a Vehicle is built.
public Vehicle(string brand, int year)
{
Brand = brand;
Year = year;
}
public void Start()
{
Console.WriteLine($"{Brand} ({Year}) — engine started!");
}
}
// Derived class (the "child"). " : Vehicle " means "Ca
...Your turn. The Dog class is already written for you at the bottom. Fill in the two blanks marked ___ in Main so Dog is recognised as an Animal and you call the method it inherited.
🎯 Your turn: extend a base class
Make Dog an Animal and call the inherited Eat() method.
using System;
class Animal
{
public string Name { get; set; }
public Animal(string name) { Name = name; }
public void Eat()
{
Console.WriteLine($"{Name} is eating.");
}
}
class Program
{
static void Main()
{
// 🎯 YOUR TURN — make Dog inherit from Animal, then call the inherited method.
// 1) Make Dog a derived class of Animal (Dog IS AN Animal)
// 👉 fill the blank with the base class name
// class Dog : ___ // �
...2. Overriding with virtual & override
Inheriting a method gives you the parent's behaviour, but often a child needs to do something different. Mark the base method virtual ("you may replace me") and the child's version override ("I'm replacing it"). The magic part: even when the object is stored in a variable typed as the base class, C# runs the actual object's override at runtime. That is polymorphism — "many shapes" — one method name, the right behaviour for each type.
Worked example: virtual / override + polymorphism
Notice all three variables are typed Animal, yet each prints differently.
using System;
class Animal
{
public string Name { get; set; }
public Animal(string name) { Name = name; }
// virtual = "derived classes are ALLOWED to override this method".
public virtual void Speak()
{
Console.WriteLine($"{Name} makes a sound.");
}
}
class Dog : Animal
{
public Dog(string name) : base(name) { }
// override = "replace the base version when called on a Dog".
public override void Speak()
{
Console.WriteLine($"{Name}: Woo
...Now you try. FrenchGreeter needs to override Greet(). Fill in the keyword that replaces the base method and the French greeting it should return.
🎯 Your turn: override a virtual method
Override Greet() and watch the base-typed reference call your version.
using System;
class Greeter
{
// virtual so a derived class can replace it.
public virtual string Greet()
{
return "Hello.";
}
}
class FrenchGreeter : Greeter
{
// 🎯 YOUR TURN — override Greet() to return a French greeting.
// 1) Add the keyword that REPLACES the base method
public ___ string Greet() // 👉 replace ___ with override
{
// 2) Return a French hello
return ___; // 👉 replace ___ with "Bonjour."
}
}
...🔎 Deep Dive: extend, don't copy, with base.Method()
Sometimes you don't want to replace the parent's method — you want to add to it. From inside an override, call base.MethodName() to run the parent's version, then do your extra work. This keeps the shared logic in one place instead of copying it into every child.
Worked example: base.Log() then add a timestamp
The override reuses the parent's Log() rather than duplicating it.
using System;
class Logger
{
public virtual void Log(string message)
{
Console.WriteLine($"[LOG] {message}");
}
}
class TimestampLogger : Logger
{
public override void Log(string message)
{
// base.Log(...) calls the ORIGINAL version, then we add to it.
Console.Write("2026-06-14 ");
base.Log(message); // reuse the parent's behaviour instead of copying it
}
}
class Program
{
static void Main()
{
Logger logger = new Tim
...3. Polymorphism, is & as
Polymorphism really pays off when you treat a mixed bag of objects uniformly. Store different derived objects in one base-typed array (Animal[]), loop over them once, and each call lands on the correct override — no if/switch on the type. When you do need the specific type, is checks and casts in a single step (if (a is Dog dog)), and as attempts a cast that yields null instead of crashing if it fails.
Worked example: one loop, many behaviours
See virtual dispatch plus is/as pattern matching in action.
using System;
class Animal
{
public string Name { get; set; }
public Animal(string name) { Name = name; }
public virtual void Speak() => Console.WriteLine($"{Name} makes a sound.");
}
class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void Speak() => Console.WriteLine($"{Name}: Woof!");
public void Fetch() => Console.WriteLine($"{Name} fetches the ball!");
}
class Cat : Animal
{
public Cat(string name) : base(name) { }
public override
...4. Abstract Classes (vs Interfaces)
An abstract class is a base class you can never new directly — it only exists to be inherited. It can hold shared fields and finished methods, plus abstract methods that have no body and that every child must implement. That makes it a contract with some implementation baked in. An interface is a pure contract — method signatures only, no fields, and a class can implement many interfaces but inherit only one class. Rule of thumb: use an abstract class when relatives share state and code (Employee → Contractor); use an interface for a capability unrelated classes can share (IComparable, IDisposable).
Worked example: abstract Employee + payroll
CalculatePay() is abstract, so every employee type must define it.
using System;
// abstract = a class you CANNOT 'new' directly; it only exists to be inherited.
abstract class Employee
{
public string Name { get; set; }
public double BaseSalary { get; set; }
public Employee(string name, double baseSalary)
{
Name = name;
BaseSalary = baseSalary;
}
// Abstract method: NO body. Every derived class MUST implement it.
public abstract double CalculatePay();
// A normal method, inherited and shared by all employees.
...🔎 Deep Dive: sealed — closing a class for inheritance
The opposite of abstract is sealed: it marks a class as final so nothing can inherit from it. Use it when a class is complete and you don't want subclasses changing its behaviour — it also lets the runtime optimise calls. You can also seal a single override to stop deeper classes overriding it again.
Worked example: a sealed class
Uncomment the PremiumSavings line to see the sealed error.
using System;
class Account
{
public virtual void Describe() => Console.WriteLine("A bank account.");
}
// sealed = "this class is final — no one may inherit from it".
sealed class SavingsAccount : Account
{
public override void Describe() => Console.WriteLine("A savings account.");
}
// class PremiumSavings : SavingsAccount { } // ❌ CS0509: cannot derive from sealed
class Program
{
static void Main()
{
Account acc = new SavingsAccount();
acc.Describe(); //
...Pro Tips
- 💡 Override needs virtual: a method is only overridable if the base marks it
virtual(orabstract). Novirtual, nooverride. - 💡 Reuse with
base: callbase.Method()inside an override to extend the parent instead of copy-pasting its body. - 💡 Favour composition over deep trees: keep hierarchies to 2–3 levels; if "is a" feels forced, a "has a" field is usually better.
- 💡 Abstract class vs interface: abstract = shared state + code; interface = a capability many unrelated classes can opt into.
- 💡 Pattern matching beats casting:
if (a is Dog d)is safer and shorter than checking then casting on two lines.
Common Errors (and the fix)
- "CS0506 / CS0507: cannot override … not marked virtual, abstract, or override" — you wrote
overrideon a method whose base version isn'tvirtual. Addvirtualto the base method. - Warning "CS0114: … hides inherited member; use the
newkeyword oroverride" — you redeclared a method withoutoverride, so it hides the base one and polymorphism breaks. Addoverride(almost always what you want). - "CS1729: '…' does not contain a constructor that takes N arguments" — your
: base(...)call doesn't match any parent constructor. Pass the exact arguments the base constructor expects. - "CS7036 / no argument given that corresponds to required parameter" — the parent has only a parameterised constructor, so the child must call
: base(...). Add it. - "CS0144: cannot create an instance of the abstract type" — you tried to
newanabstractclass. Instantiate a concrete derived class instead. - "CS0509: cannot derive from sealed type" — you tried to inherit from a
sealedclass. Removesealed, or use composition instead of inheritance.
📋 Quick Reference
| Task | Code | Notes |
|---|---|---|
| Inherit a class | class Dog : Animal | Dog "is a" Animal |
| Call parent constructor | : base(name) | Runs before child body |
| Allow overriding | public virtual void X() | On the base method |
| Replace behaviour | public override void X() | On the derived method |
| Reuse parent method | base.X() | Inside the override |
| Must-implement method | public abstract void X(); | In an abstract class |
| Check & cast | if (a is Dog d) | d is typed Dog inside |
| Safe cast | a as Cat | null if it isn't a Cat |
Frequently Asked Questions
Q: What's the difference between override and new on a method?
override truly replaces the base method, so polymorphism works — a base-typed reference calls your version. new only hides the base method; which one runs depends on the variable's declared type, not the real object. You almost always want override.
Q: When should I use an abstract class instead of an interface?
Use an abstract class when the children are genuine relatives that share fields and some finished code (Employee → Manager). Use an interface for a capability unrelated classes can share (e.g. anything "comparable" or "disposable"). A class can implement many interfaces but inherit from only one class.
Q: Why must I write : base(...) sometimes but not always?
If the parent only has a constructor that takes arguments, the child must supply them with : base(...). If the parent has a parameterless constructor, C# calls it for you automatically and you can omit : base().
Q: Is is better than casting with (Dog)animal?
Yes, for safety. A direct cast throws InvalidCastException if the object isn't that type. if (animal is Dog dog) checks first and only enters the block when the cast is valid, and as returns null instead of throwing — both avoid crashes.
Mini-Challenge: Shape Areas
No blanks this time — just a brief and an outline. Build a small shape hierarchy, override Area() in each shape, then loop over a Shape[] and print every area. This is the exact pattern real graphics, billing and reporting code uses.
🎯 Mini-Challenge: areas with polymorphism
Write Shape, Circle and Rectangle yourself and loop over a Shape[].
using System;
class Program
{
static void Main()
{
// 🎯 MINI-CHALLENGE: Areas with polymorphism
//
// 1. Make a BASE class Shape with a virtual double Area()
// that returns 0.
// 2. Make Circle : Shape (field: double Radius) that OVERRIDES Area()
// to return Math.PI * Radius * Radius.
// 3. Make Rectangle : Shape (fields: double Width, double Height) that
// OVERRIDES Area() to return Width * Height.
...🎉 Lesson Complete
- ✅
class Child : Parentinherits every public member of the base class - ✅
: base(...)chains constructors; the base runs first - ✅
virtualon the base +overrideon the child = replaceable behaviour - ✅
base.Method()reuses the parent's logic instead of copying it - ✅ Polymorphism: one base-typed loop calls the right override for each object
- ✅
abstractclasses set contracts;interfacefor capabilities;sealedto lock down;is/asto check and cast safely - ✅ Next lesson: Collections — List, Dictionary, Stack, Queue, and more
Sign up for free to track which lessons you've completed and get learning reminders.