← Back to C# series
🧩
OOP
OOP Β· Prerequisite: inheritance

09. Polymorphism

Polymorphism is when the same call site runs different behavior depending on the actual object's type. Learn virtual/override, abstract classes, and upcasting/downcasting.

C#.NET 8OOPpolymorphism
Duration
⏱ ~1-1.5 hours
Level
πŸ“Š Intermediate
Prerequisite
🎯 Inheritance
OUTCOME
Polymorphism is when the same call site runs different behavior depending on the actual object's type. Learn virtual/override, abstract classes, and upcasting/downcasting.

What you'll learn

  • 1Understand the difference between upcasting and downcasting
  • 2Do safe downcasting with the `is` pattern
  • 3Know when to use `abstract` classes and `abstract` methods
  • 4Put child objects into a collection like `List<Animal>` and process them uniformly

Overview

When a same-named call runs different behavior based on the actual object's type β€” that's **polymorphism**. The `virtual`/`override` pair from the previous lecture is the core tool; here we cover upcasting/downcasting, the `is` pattern, and abstract classes in full.

Core Concepts

1) Upcasting

Child β†’ parent direction. Always safe β€” no explicit cast needed.

csharp
Animal a = new Dog();    // Dog object stored in an Animal variable (upcast)
a.Speak();               // with virtual/override, Dog.Speak() runs

Even though the variable type is `Animal`, the actual object is `Dog`. Virtual methods dispatch by the actual type.

2) Downcasting

Parent β†’ child direction. Succeeds only when the actual object really is that child.

csharp
Animal a = new Dog();
Dog d = (Dog)a;          // actually a Dog β†’ OK
d.Bark();

Animal a2 = new Animal();
Dog d2 = (Dog)a2;        // throws InvalidCastException at runtime

3) Safer downcast with the `is` pattern

csharp
if (animal is Dog dog)
{
    dog.Bark();          // only entered when animal is a Dog; dog is already cast
}

Type check + variable declaration in one shot. No cast-failure risk.

`as` works similarly.

csharp
Dog? d = animal as Dog;  // null on failure

4) `abstract` class

Means "you cannot create an object from this class alone β€” children must make it concrete."

csharp
abstract class Shape
{
    public abstract double Area();   // no body β€” children must implement
    public void Print() => Console.WriteLine($"area={Area()}");
}

class Circle : Shape
{
    public double Radius;
    public Circle(double r) => Radius = r;
    public override double Area() => Math.PI * Radius * Radius;
}

// var s = new Shape();   // compile error: cannot instantiate abstract

`abstract` methods are automatically `virtual` and children must `override` them.

5) Virtual dispatch

Method lookup uses the **runtime (actual) type** of the object, not the variable's static type. That's how polymorphism works under the hood.

csharp
List<Animal> zoo = [new Dog(), new Cat(), new Cow()];
foreach (var a in zoo)
{
    a.Speak();   // different output each: Woof / Meow / Moo
}

One call site (`a.Speak()`) yields different behavior per object. That's the big win of OOP.

Examples

Example 1 β€” `UpcastDowncast`: cast directions

csharp
// Program.cs
using CodingNow.Lecture.Oop09;

// Upcast: child β†’ parent (safe, automatic)
Animal a = new Dog();
a.Speak();   // virtual dispatch β†’ Dog.Speak()

// Downcast: parent β†’ child (must match the actual type)
Dog d = (Dog)a;
d.Bark();
csharp
// Animal.cs
namespace CodingNow.Lecture.Oop09;

internal class Animal
{
    public virtual void Speak() => Console.WriteLine("animal sound");
}

// Dog.cs
internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("Woof!");
    public void Bark() => Console.WriteLine("Bow!Bow!");
}

**Output**

text
Woof!
Bow!Bow!

**Note:** The variable type is `Animal`, yet `Dog.Speak()` is called β€” that's the essence of polymorphism.

Example 2 β€” `IsPattern`: safer type checks

csharp
// Program.cs
using CodingNow.Lecture.Oop09;

Animal[] animals = [new Dog(), new Cat(), new Animal()];

foreach (var a in animals)
{
    a.Speak();

    if (a is Dog dog)
        dog.Bark();
    else if (a is Cat cat)
        cat.Purr();
}
csharp
// Animal.cs
namespace CodingNow.Lecture.Oop09;

internal class Animal
{
    public virtual void Speak() => Console.WriteLine("animal sound");
}

internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("Woof!");
    public void Bark() => Console.WriteLine("Bow!Bow!");
}

internal class Cat : Animal
{
    public override void Speak() => Console.WriteLine("Meow~");
    public void Purr() => Console.WriteLine("Purr~");
}

**Output**

text
Woof!
Bow!Bow!
Meow~
Purr~
animal sound

**Note:** Inside `if (a is Dog dog)` the variable `dog` is already typed as `Dog`. No cast-failure risk.

Example 3 β€” `AbstractShape`: abstract class

csharp
// Program.cs
using CodingNow.Lecture.Oop09;

Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);

s1.Print();
s2.Print();

// var s = new Shape();   // compile error: cannot instantiate abstract directly
csharp
// Shape.cs
namespace CodingNow.Lecture.Oop09;

internal abstract class Shape
{
    public abstract double Area();   // no body

    public void Print() => Console.WriteLine($"area = {Area():F2}");
}

// Circle.cs
internal class Circle : Shape
{
    public double Radius;
    public Circle(double r) => Radius = r;
    public override double Area() => Math.PI * Radius * Radius;
}

// Rectangle.cs
internal class Rectangle : Shape
{
    public double Width;
    public double Height;
    public Rectangle(double w, double h) { Width = w; Height = h; }
    public override double Area() => Width * Height;
}

**Output**

text
area = 78.54
area = 12.00

**Note:** `Shape` cannot be created directly, but it gathers common code (`Print`) in one place.

Example 4 β€” `PolymorphismList`: collection + uniform processing

csharp
// Program.cs
using CodingNow.Lecture.Oop09;

List<Animal> zoo = [new Dog(), new Cat(), new Cow()];

foreach (var a in zoo)
{
    a.Speak();   // same call, different output per object
}
csharp
// Animals.cs
namespace CodingNow.Lecture.Oop09;

internal class Animal
{
    public virtual void Speak() => Console.WriteLine("...");
}

internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("Woof!");
}

internal class Cat : Animal
{
    public override void Speak() => Console.WriteLine("Meow~");
}

internal class Cow : Animal
{
    public override void Speak() => Console.WriteLine("Moo~");
}

**Output**

text
Woof!
Meow~
Moo~

**Note:** Adding a new animal doesn't touch the loop β€” that's the real value of polymorphism: "closed to modification, open to extension."

Full example code (src/)

src/AbstractShape/AbstractShape.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/AbstractShape/Circle.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Circle : Shape
{
    public double Radius;

    public Circle(double radius) => Radius = radius;

    public override double Area() => Math.PI * Radius * Radius;
}

src/AbstractShape/Program.cs

csharp
using CodingNow.Lecture.Oop09;

Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);

s1.Print();
s2.Print();

// var s = new Shape();   // abstract β†’ cannot instantiate directly (uncommenting -> compile error)

src/AbstractShape/Rectangle.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Rectangle : Shape
{
    public double Width;
    public double Height;

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double Area() => Width * Height;
}

src/AbstractShape/Shape.cs

csharp
namespace CodingNow.Lecture.Oop09;

// abstract: cannot be instantiated by itself; children must make it concrete.
internal abstract class Shape
{
    // abstract method with no body β€” children must override.
    public abstract double Area();

    // Abstract classes can still have concrete methods.
    public void Print() => Console.WriteLine($"area = {Area():F2}");
}

src/IsPattern/Animals.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Animal
{
    public virtual void Speak() => Console.WriteLine("animal sound");
}

internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("Woof!");
    public void Bark() => Console.WriteLine("Bow!Bow!");
}

internal class Cat : Animal
{
    public override void Speak() => Console.WriteLine("Meow~");
    public void Purr() => Console.WriteLine("Purr~");
}

src/IsPattern/IsPattern.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/IsPattern/Program.cs

csharp
using CodingNow.Lecture.Oop09;

Animal[] animals = [new Dog(), new Cat(), new Animal()];

foreach (var a in animals)
{
    a.Speak();   // virtual dispatch

    // is-pattern: type check + variable declaration. Already-cast variable inside.
    if (a is Dog dog)
        dog.Bark();
    else if (a is Cat cat)
        cat.Purr();
}

src/PolymorphismList/Animals.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Animal
{
    public virtual void Speak() => Console.WriteLine("...");
}

internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("Woof!");
}

internal class Cat : Animal
{
    public override void Speak() => Console.WriteLine("Meow~");
}

internal class Cow : Animal
{
    public override void Speak() => Console.WriteLine("Moo~");
}

src/PolymorphismList/PolymorphismList.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/PolymorphismList/Program.cs

csharp
using CodingNow.Lecture.Oop09;

// You can put child-typed objects in a single parent-typed collection.
List<Animal> zoo = [new Dog(), new Cat(), new Cow()];

foreach (var a in zoo)
{
    a.Speak();   // same call, different output per object β€” that's polymorphism
}

src/UpcastDowncast/Animal.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Animal
{
    public virtual void Speak() => Console.WriteLine("animal sound");
}

src/UpcastDowncast/Dog.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("Woof!");
    public void Bark() => Console.WriteLine("Bow!Bow!");
}

src/UpcastDowncast/Program.cs

csharp
using CodingNow.Lecture.Oop09;

// Upcast: child(Dog) β†’ parent(Animal). Safe β€” no explicit cast needed.
Animal a = new Dog();
a.Speak();   // virtual dispatch: actual object is Dog β†’ Dog.Speak() runs

// Downcast: parent(Animal) β†’ child(Dog). OK only when the actual type is Dog.
Dog d = (Dog)a;
d.Bark();

src/UpcastDowncast/UpcastDowncast.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Common Mistakes

  1. Downcasting with just `(Dog)a` and no safety check β†’ `InvalidCastException`. Use `is` or `as`.
  2. Trying to `new` an `abstract` class β€” compile error.
  3. If the child doesn't `override` an `abstract` method, the child also has to be marked abstract; otherwise compile error.
  4. Assuming a virtual call uses the parent's version. Regardless of the variable type, the **actual object's** method runs.
  5. Trying to use a variable declared inside an `is` pattern outside its `if` β€” its scope is usually inside the block.

Summary

  • Polymorphism = "same call, different behavior." `virtual`/`override` and virtual dispatch are the engine.
  • Upcasting is automatic; downcasting is safer with the `is` pattern or `as`.
  • `abstract` classes define common parts and delegate the specifics to children.
  • Pairing polymorphism with collections is a force multiplier β€” adding child types doesn't change the processing code.

Practice

**Practice - 09. Polymorphism**

Problem 1 β€” `Vehicle`/`Car`/`Bike` (`Move()` polymorphism)

  • Project folder: `Homework01/`
  • Key concepts: `virtual` / `override`, child objects in a collection

Requirements

  • Parent `Vehicle` class has `virtual void Move()` (default message).
  • Children `Car` and `Bike` each override `Move()` with different messages.
  • In `Program.cs` put 3-4 objects in a `List<Vehicle>` and iterate, calling `Move()`.

Expected output

text
Vehicle moving
Car going vroom~
Bike going ring-ring~
Car going vroom~

Hints

  • Put `new Car()`, `new Bike()` directly into `List<Vehicle>` (upcast).

Problem 2 β€” `abstract Animal` + child `Speak()`s

  • Project folder: `Homework02/`
  • Key concepts: `abstract` class / `abstract` method, `is` pattern

Requirements

  • `abstract class Animal` has `abstract void Speak()`.
  • Children `Dog`, `Cat`, `Cow` each override `Speak()`.
  • Only `Dog` adds an extra method `Bark()`.
  • Iterate `Animal[] zoo = [...]` calling `Speak()`, and when `is Dog dog`, also call `dog.Bark()`.

Expected output

text
Woof!
Bow!Bow!
Meow~
Moo~

Hints

  • An `abstract` method has no body β€” ends with a semicolon.
  • `var animal = new Animal();` is a compile error (you can't directly instantiate an abstract class).

Check your answer

Try it yourself, then compare against the [`answer/`](./answer/) folder.

Answer (answer/)

homework/answer/Homework01/Homework01.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

homework/answer/Homework01/Program.cs

csharp
using CodingNow.Lecture.Oop09;

List<Vehicle> vehicles = [new Vehicle(), new Car(), new Bike(), new Car()];

foreach (var v in vehicles)
{
    v.Move();
}

homework/answer/Homework01/Vehicles.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal class Vehicle
{
    public virtual void Move() => Console.WriteLine("Vehicle moving");
}

internal class Car : Vehicle
{
    public override void Move() => Console.WriteLine("Car going vroom~");
}

internal class Bike : Vehicle
{
    public override void Move() => Console.WriteLine("Bike going ring-ring~");
}

homework/answer/Homework02/Animals.cs

csharp
namespace CodingNow.Lecture.Oop09;

internal abstract class Animal
{
    // Children must implement this.
    public abstract void Speak();
}

internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("Woof!");
    public void Bark() => Console.WriteLine("Bow!Bow!");
}

internal class Cat : Animal
{
    public override void Speak() => Console.WriteLine("Meow~");
}

internal class Cow : Animal
{
    public override void Speak() => Console.WriteLine("Moo~");
}

homework/answer/Homework02/Homework02.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

homework/answer/Homework02/Program.cs

csharp
using CodingNow.Lecture.Oop09;

Animal[] zoo = [new Dog(), new Cat(), new Cow()];

foreach (var a in zoo)
{
    a.Speak();

    if (a is Dog dog)
        dog.Bark();
}

Try It Yourself

bash
cd src/UpcastDowncast
dotnet run

cd ../IsPattern
dotnet run

cd ../AbstractShape
dotnet run

cd ../PolymorphismList
dotnet run

Next Lecture

[10_Interface](../10_%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4/) β€” Promise only "what can be done" β€” no inheritance required.

Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub β†—