Lesson 7 · ~14 min · ← Lesson 6
Inheritance & interfaces
Two ways for types to relate: inheritance ("a Dog IS an Animal — gets everything it has") and interfaces ("a Robot CAN Speak() — promises a behavior"). Both exist so code can work with many types through one shape.
virtual / override: the base lets you replace behavior
class Animal
{
public string Name;
public Animal(string name)
{
Name = name;
}
public virtual void Speak() // virtual: children may replace this
{
Console.WriteLine(Name + " makes a sound");
}
}
class Dog : Animal // Dog inherits from Animal
{
public Dog(string name) : base(name) { } // base(...): run Animal's constructor
public override void Speak() // replace it
{
Console.WriteLine(Name + " says woof");
}
}
Animal a = new Dog("Rex"); // a Dog IS an Animal — this assignment is legal
a.Speak(); // dispatch follows the OBJECT: Dog's version runs
Animal b = new Animal("Generic");
b.Speak();
The last two lines are the whole point: the variable's type is Animal, but the METHOD chosen is the one on the actual object — virtual dispatch. One loop over a mixed collection can call Speak() on each and get the right bark every time.
base: build on what the parent did
base(...) calls the parent constructor; base.Method() calls the parent's version — the standard "extend, don't replace" shape:
class Animal
{
public virtual void Speak() { Console.WriteLine("..."); }
}
class Cat : Animal
{
public override void Speak()
{
base.Speak(); // do what animals do…
Console.WriteLine("meow"); // …then add cat behavior
}
}
is: "is this really that?" — with a bonus binding
class Animal { }
class Dog : Animal
{
public void Fetch()
{
Console.WriteLine("fetching!");
}
}
Animal x = new Dog(); // upcast: fine, Dog is an Animal
if (x is Dog d) // test + declare in one step
{
d.Fetch(); // only Dogs can fetch
}
Its quieter sibling as converts and yields null on mismatch: Dog dd = x as Dog; — then check dd != null. Use is when you'd only branch; as when you'd keep using the value either way.
Interfaces: a promise, not a family
interface IGreeter
{
void Greet(); // a contract: no body, just the promise
}
class EnglishGreeter : IGreeter
{
public void Greet() { Console.WriteLine("Hello"); }
}
class SpanishGreeter : IGreeter
{
public void Greet() { Console.WriteLine("Hola"); }
}
IGreeter[] greeters = { new EnglishGreeter(), new SpanishGreeter() }; // unrelated classes, same promise
foreach (IGreeter g in greeters)
{
g.Greet(); // each keeps its own behavior
}
The two greeters share no base class — the interface isn't ancestry, it's a common promise. That's why C# lets a class implement MANY interfaces but inherit from only ONE base class: family is exclusive, promises are not.
Which tool, when?
| Situation | Use |
|---|---|
| "X is a kind of Y" with shared code | Inheritance (class Dog : Animal) |
| "X can do Z", across unrelated families | Interface (: IComparable) |
| Child tweaks parent behavior | virtual + override |
| Child extends parent behavior | override + base.Method() |
| Safely downcast before using extra members | is (branch) / as (keep) |
Practice this now
- Build
Shapewithvirtual double Area() { return 0; }, thenSquareandCircleoverriding it — loop over aShape[]and print each area. (The playground has a starter in its examples.) - Add
IScalablewithvoid Scale(double factor)to one shape only — prove interfaces cut across the family tree. - Next lesson: Lesson 8 — Collections.