L12 — OOP pt.3: Inheritance & Interfaces
The last OOP pillar: how classes can reuse and extend each other. We learn inheritance, virtual/override, abstract classes, and interfaces — and when to use each.
🎯Learning Objectives
- Create a derived class that inherits fields, properties, and methods from a base class
- Use
protectedto expose internals to subclasses without exposing them to the world - Call a base constructor with
base(...) - Override a base method using
virtual/override - Define an interface and implement it in a class
- Explain when to reach for inheritance vs an interface
📖Theory
1. Why Inheritance?
Imagine you're adding three new classes to your school app: Student, Teacher, Librarian. They all have Name, Email, and a Greet() method. Writing the same three properties and method three times is copy-paste programming — the worst kind of code to maintain.
Inheritance lets one class (the derived class) take everything another class (the base class) has, and then add or change pieces on top. You write the common stuff once, in the base.
C#
class Person
{
public string Name { get; set; }
public string Email { get; set; }
public void Greet() =>
Console.WriteLine($"Hi, I'm {Name}.");
}
class Student : Person // Student "is-a" Person
{
public double GPA { get; set; }
}
class Teacher : Person // Teacher "is-a" Person
{
public string Subject { get; set; }
}The colon : Person means inherits from Person. Student and Teacher automatically get Name, Email, and Greet() — for free.
C#
Student s = new Student { Name = "Alice", Email = "a@x.com", GPA = 3.8 };
s.Greet(); // Hi, I'm Alice. ← method came from Person
Console.WriteLine(s.GPA); // 3.8 ← added by StudentThe "is-a" test. Inheritance only makes sense when the sentence "a Student is a Person" sounds right. If you find yourself saying "a Car has an Engine" — that's composition, not inheritance. Use a field of type Engine, not Car : Engine.
2. protected — Access for Subclasses Only
In L11 we made fields private. Private means only this class. But a subclass often needs to reach into its parent's state. protected is the middle ground: this class and any class that inherits from it.
C#
class Person
{
protected string _internalId; // subclasses see this; outside code doesn't
public Person(string id) => _internalId = id;
}
class Student : Person
{
public Student(string id) : base(id) { }
public void DebugPrint() =>
Console.WriteLine(_internalId); // ✅ ok — protected is visible here
}Outside code still can't touch _internalId. Only Person and its descendants can.
3. Calling the Base Constructor — base(...)
When a derived class has a constructor, it must call one of the base constructors so the base part of the object gets initialised. You do that with base(...):
C#
class Person
{
public string Name { get; }
public string Email { get; }
public Person(string name, string email)
{
Name = name;
Email = email;
}
}
class Student : Person
{
public double GPA { get; set; }
// Call Person's constructor first, then run the Student-specific part
public Student(string name, string email, double gpa) : base(name, email)
{
GPA = gpa;
}
}The order is always: base constructor → derived constructor body. C# enforces it automatically.
4. Overriding Methods — virtual / override
Sometimes a derived class wants to change what a base method does. The base marks the method virtual (meaning "you may override me"), and the derived class writes its own version marked override.
C#
class Person
{
public string Name { get; set; }
public virtual void Greet() =>
Console.WriteLine($"Hi, I'm {Name}.");
}
class Teacher : Person
{
public string Subject { get; set; }
public override void Greet() =>
Console.WriteLine($"Hi, I'm {Name}, I teach {Subject}.");
}Calling Greet() on a Teacher runs the Teacher version, not the Person one:
C#
Teacher t = new Teacher { Name = "Mr. Smith", Subject = "Math" };
t.Greet(); // Hi, I'm Mr. Smith, I teach Math.You can still reach the base version from inside the override with base.Greet():
C#
public override void Greet()
{
base.Greet(); // Hi, I'm Mr. Smith.
Console.WriteLine($"I teach {Subject}.");
}5. Polymorphism — One Variable, Many Shapes
Because a Teacher is also a Person, you can store a Teacher in a Person-typed variable. And when you call Greet() on that variable, C# picks the right version at runtime based on the actual object — not the variable's declared type. This is called polymorphism.
C#
Person[] people = new Person[]
{
new Student { Name = "Alice" },
new Teacher { Name = "Mr. Smith", Subject = "Math" },
new Person { Name = "Bob" }
};
foreach (Person p in people)
p.Greet();
// Hi, I'm Alice.
// Hi, I'm Mr. Smith, I teach Math.
// Hi, I'm Bob.One loop, one line, three different behaviours — chosen by the object itself. This is the feature that makes OOP worth using.
6. Abstract Classes — Blueprints That Can't Be Built Alone
Sometimes a base class is too generic to exist on its own. "Shape" is a shape — but you never hold a plain "shape" in your hand; you hold a circle or a square. An abstract class is a base you can't directly instantiate. It can also declare abstract methods — methods with no body that every subclass must fill in.
C#
abstract class Shape
{
public abstract double Area(); // no body — subclasses must override
public void Print() =>
Console.WriteLine($"Area = {Area()}");
}
class Circle : Shape
{
public double Radius { get; set; }
public override double Area() => Math.PI * Radius * Radius;
}
class Square : Shape
{
public double Side { get; set; }
public override double Area() => Side * Side;
}
// Shape s = new Shape(); // ❌ compile error — abstract, can't be created
Shape c = new Circle { Radius = 5 };
c.Print(); // Area = 78.53...7. Interfaces — A Contract Without Any Code
An interface is a pure contract: a list of methods/properties a class promises to provide, with no implementation. A class can inherit from only one base class, but it can implement many interfaces.
C#
interface IPrintable
{
void Print();
}
interface ISaveable
{
void Save();
}
class Report : IPrintable, ISaveable
{
public string Title { get; set; }
public void Print() => Console.WriteLine($"Printing: {Title}");
public void Save() => Console.WriteLine($"Saving: {Title}");
}Naming convention. Interfaces in C# start with a capital I: IPrintable, IDisposable, IEnumerable. This is just a convention, but the whole ecosystem follows it — stick to it.
You can also use an interface as a variable type. That's powerful: the code doesn't care which class it got, only that it honours the contract.
C#
IPrintable[] printables = new IPrintable[] { new Report { Title = "Q1" } };
foreach (IPrintable p in printables)
p.Print();8. Inheritance vs Interface — Which One?
| Situation | Use |
|---|---|
| A genuine "is-a" relationship and you want to share implementation | Inheritance (base class) |
| A capability that several unrelated classes can have (printable, comparable, drawable) | Interface |
| A base with some shared code and some methods subclasses must fill in | Abstract class |
| You need to combine behaviour from multiple sources | Interfaces (you can implement many, inherit one) |
Default in modern C#: prefer interfaces when you're designing boundaries between parts of your app. Reach for inheritance when you truly share state and behaviour and the "is-a" sentence fits.
💻Code Examples
Example A — Employee hierarchy with overriding
C#
class Employee
{
public string Name { get; set; }
public decimal BaseSalary { get; set; }
public virtual decimal MonthlyPay() => BaseSalary;
public void Print() =>
Console.WriteLine($"{Name}: {MonthlyPay():C}");
}
class Manager : Employee
{
public decimal Bonus { get; set; }
public override decimal MonthlyPay() => BaseSalary + Bonus;
}
class Intern : Employee
{
public override decimal MonthlyPay() => BaseSalary / 2m; // interns earn half
}
Employee[] team = new Employee[]
{
new Manager { Name = "Alice", BaseSalary = 5000m, Bonus = 1500m },
new Intern { Name = "Bob", BaseSalary = 2000m },
new Employee{ Name = "Carol", BaseSalary = 3000m }
};
foreach (Employee e in team) e.Print();
// Alice: $6,500.00
// Bob: $1,000.00
// Carol: $3,000.00Example B — Interface-based comparison
C#
interface IComparableSize
{
double SizeMetric();
}
class Book : IComparableSize
{
public string Title { get; set; }
public int Pages { get; set; }
public double SizeMetric() => Pages;
}
class Movie : IComparableSize
{
public string Title { get; set; }
public int MinutesLong { get; set; }
public double SizeMetric() => MinutesLong;
}
IComparableSize a = new Book { Title = "1984", Pages = 328 };
IComparableSize b = new Movie { Title = "Matrix", MinutesLong = 136 };
Console.WriteLine(a.SizeMetric() > b.SizeMetric() ? "a is bigger" : "b is bigger");Notice how the loop doesn't know or care about Book vs Movie — it only speaks the interface.
✏️Practice Tasks
Create an Animal base class with property Name (string) and a virtual method Speak() that prints "some sound".
Create three derived classes:
Dog : Animal— overridesSpeak()to print"{Name} says: Woof!"Cat : Animal— overridesSpeak()to print"{Name} says: Meow!"Cow : Animal— overridesSpeak()to print"{Name} says: Moo!"
In the main program, create an Animal[] containing a Dog, Cat, and Cow. Loop through and call Speak() on each.
💡 Hint
virtual on the base method and override on each derived one — otherwise the loop will call the base version every time. If that happens, check both keywords.Build the Shape hierarchy from section 6, but add a third subclass Rectangle(Width, Height). Then:
- Create an array
Shape[]with a Circle (radius 3), a Square (side 4), and a Rectangle (5 × 2) - Loop through the array, call
Print()on each, and also print the total area of all shapes combined - Try writing
Shape s = new Shape();and explain in a comment why the compiler refuses
💡 Hint
double total = 0; variable before the loop, then do total += shape.Area(); inside. After the loop, print the total. The abstract class can't be instantiated because at least one of its methods has no body — there's nothing to run.Define an interface:
C#
interface ILogger
{
void Log(string message);
}Create two classes that implement it:
ConsoleLogger— prints"[CONSOLE] {message}"with a timestampFileLogger— pretends to write to a file by printing"[FILE:log.txt] {message}"(don't actually write to disk for this task)
Create a method RunApp(ILogger logger) that calls logger.Log("App started"), logger.Log("Doing work"), logger.Log("App finished"). Call RunApp twice — once with each logger. Observe that RunApp doesn't change, only the logger you pass in does.
💡 Hint
RunApp) depends on the contract, not the concrete class. Swapping implementations requires zero changes to the code that uses them. This pattern — "program to an interface" — is one of the most useful ideas in OOP, and you'll see it again in MAUI with services.⚠️Common Mistakes
Forgetting virtual on the base method
If the base method isn't virtual, the derived override won't compile. And if you use the new keyword instead of override to "hide" the method, polymorphism breaks — calling through the base-typed variable will run the base version. Use virtual/override, not new, unless you know exactly why.
Trying to inherit from two classes
C# allows only single inheritance of classes: class A : B, C is illegal. If you need to combine behaviours from multiple sources, use interfaces — you can implement as many as you want.
Confusing abstract and interface
An abstract class can contain real code and state; an interface (until recently) cannot. If you find yourself wanting some shared fields, it has to be an abstract class. If you only want to list required methods, it's an interface.
Making every class inherit from something
Inheritance is overused. Before adding : SomeBase, say the "is-a" sentence out loud. "An Employee is a Person" — yes, ok. "A Logger is a Writer" — that's shakier; maybe just compose.
Upcasting without downcasting carefully
C#
Person p = new Student(); // upcast — always safe
Student s = (Student)p; // downcast — crashes at runtime if p is not really a StudentUse the is keyword to check first: if (p is Student s) { ... }.
🎓Instructor Notes
⚡ How to run this lesson (~80 min)
- [10 min] Motivation via copy-paste pain. Put three classes with duplicated Name/Email/Greet on the projector. Ask: "What's wrong with this code?" Let them say it. Introduce
: Person. - [15 min] Live code: base → derived → override. Build Person → Student → Teacher. Use the
virtual/overridepair. Then store a Teacher in aPersonvariable and callGreet(). This is the "aha" moment — don't rush past it. - [15 min] Abstract class or interface? Show the Shape example. Show the ILogger interface example. Spend time on the table in section 8. This is conceptual — many students conflate abstract and interface the first week.
- [5 min] Protected + base(...) walkthrough.
- [30 min] Task 1 in class. Watch for: missing
virtual, missingoverride, trying to instantiate the abstract class in Task 2 — have the expected compile errors on the projector as teaching moments. - [5 min] Wrap up + assign homework. Preview L13: leaving the console and entering .NET MAUI. Assign Tasks 2 and 3.
💬 Discussion questions
- "Give an example from real life where inheritance feels natural. Now one where it would feel wrong."
- "Why can't an abstract class be created with
new?" - "In Task 3, what would change inside
RunAppif we added a third logger (e.g., DatabaseLogger)?"