C# (.NET 8 LTS) track

Lesson 09 · Part 2: Object-Oriented Programming

Polymorphism

Call the right implementation through base references and use casts and abstract classes safely.

Part 2: Object-Oriented Programming6 min readFree lesson

Method calls with the same name have different behavior depending on the actual type of the object — this is polymorphism. virtual/override, seen in the previous section, was the core tool, and this section covers the Up/downcasting, is patterns, and abstract classes in earnest.

Learning Objectives

  • Understand the difference between upcasting and downcasting.
  • Safe downcasting with is pattern matching.
  • Know the use of abstract class and abstract method
  • Put child objects in a collection like List<Animal> and process them in batches

Key concepts

1) Upcasting

Child → parent direction. Always safe, no explicit cast required.

Animal a = new Dog();    //Putting a Dog object into an Animal variable (upcasting)
a.Speak();               //If virtual/override, Dog.Speak() is executed.

Even though the variable type is Animal, the actual object is Dog. Virtual methods are called according to their actual type.

2) Downcasting

Parent → child direction. Successful only if the actual object is one of its children.

Animal a = new Dog();
Dog d = (Dog)a;          //It's actually a dog so it's OK
d.Bark();

Animal a2 = new Animal();
Dog d2 = (Dog)a2;        //Runtime InvalidCastException occurs

3) Safe downcasting with is patterns

if (animal is Dog dog)
{
    dog.Bark();          //Comes only when animal is Dog, dog has already been cast
}

Condition check and variable declaration at once. There is no risk of cast failure.

The as keyword can be used similarly.

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

4) Abstract classes

It means "You cannot create an object with this class alone. The child must materialize it."

abstract class Shape
{
    public abstract double Area();   //No body, child 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();   // Compilation error: abstract cannot be instantiated directly

The abstract method is automatically virtual, and its children must be override.

5) Virtual dispatch

Finding and calling a method with the dynamic type of the actual object rather than the static type of the variable. This is how polymorphism works.

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

There is only one line of code to call (a.Speak()), but each object produces different behavior. This is a big advantage of OOP.

See as example

Example 1 — UpcastDowncast: Casting direction

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

//Upcasting: child → parent (safe, automatic)
Animal a = new Dog();
a.Speak();   //Virtual Dispatch → Dog.Speak()

//Downcasting: Parent → Child (actual type must match)
Dog d = (Dog)a;
d.Bark();
// Animal.cs
namespace CodingNow.Lecture.Oop09;

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

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

Execution result

Woof woof!
King! King!

Memo: The essence of polymorphism is that even though the variable type is Animal, Dog.Speak() is called.

Example 2 — IsPattern: Safe type checking

// 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();
}
// Animal.cs
namespace CodingNow.Lecture.Oop09;

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

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

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

Execution result

Woof woof!
King! King!
Meow~
Purr~
animal sounds

Memo: Within if (a is Dog dog), the dog variable can already be used as the Dog type. There is no risk of cast failure.

Example 3 — AbstractShape: Abstract class

// 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();   // Compilation error: abstract cannot be instantiated directly
// Shape.cs
namespace CodingNow.Lecture.Oop09;

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

    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;
}

Execution result

Area = 78.54
Area = 12.00

Memo: Shape is abstract and cannot be created directly, but the common code (Print) can be collected in one place.

Example 4 — PolymorphismList: Collection + Batch Processing

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

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

foreach (var a in zoo)
{
    a.Speak();   //Same call, but different results for each object
}
// 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 woof!");
}

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

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

Execution result

Woof woof!
Meow~
Mood~

Note: Even if you add a new animal, the code iterating over zoo remains the same — the real value of polymorphism is that it is "closed for addition, open for extension."

Common Mistakes

  1. Use only (Dog)a for downcasting and omit the safety check to generate InvalidCastException → Use is or as.
  2. Trying to create the abstract class directly as new — compilation error.
  3. If the child of method abstract does not use override, a compilation error occurs because the child is not processed as abstract.
  4. I called a virtual method, but I think the parent version appears. Regardless of the variable type, the method of actual object is called.
  5. Trying to use a variable declared within the is pattern outside of if — the scope is usually inside the if block.

Cleanup

  • Polymorphism = "same call, different behavior". virtual/override and virtual dispatch are the engines.
  • Upcasting is automatic, downcasting is safe with is pattern or as.
  • The abstract class defines only the common parts and is used when delegating the specific parts to children.
  • Power explodes when it meets a collection — Even if the number of children increases, the processing code remains the same.

Try it yourself

cd src/UpcastDowncast
dotnet run

cd ../IsPattern
dotnet run

cd ../AbstractShape
dotnet run

cd ../PolymorphismList
dotnet run

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/AbstractShape/AbstractShape.csprojText
<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.csC#
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.csC#
using CodingNow.Lecture.Oop09;

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

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

//var s = new Shape();   // Because it is abstract, it cannot be instantiated directly (compilation error occurs if you uncomment it)
src/AbstractShape/Rectangle.csC#
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.csC#
namespace CodingNow.Lecture.Oop09;

//abstract: cannot be instantiated by itself and must be instantiated by its children.
internal abstract class Shape
{
    //abstract method without body — child must override it.
    public abstract double Area();

    //Abstract classes can also have general (concrete) methods.
    public void Print() => Console.WriteLine($"Area = {Area():F2}");
}
src/IsPattern/Animals.csC#
namespace CodingNow.Lecture.Oop09;

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

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

internal class Cat : Animal
{
    public override void Speak() => Console.WriteLine("Meow~");
    public void Purr() => Console.WriteLine("Purr~");
}
src/IsPattern/IsPattern.csprojText
<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.csC#
using CodingNow.Lecture.Oop09;

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

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

    //is pattern: type checking + variable declaration all at once. Inside, variables that have already been cast are used.
    if (a is Dog dog)
        dog.Bark();
    else if (a is Cat cat)
        cat.Purr();
}
src/PolymorphismList/Animals.csC#
namespace CodingNow.Lecture.Oop09;

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

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

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

internal class Cow : Animal
{
    public override void Speak() => Console.WriteLine("Mood~");
}
src/PolymorphismList/PolymorphismList.csprojText
<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.csC#
using CodingNow.Lecture.Oop09;

//All child type objects can be contained in one parent type collection.
List<Animal> zoo = [new Dog(), new Cat(), new Cow()];

foreach (var a in zoo)
{
    a.Speak();   //Same call but different results for different objects — this is polymorphism
}
src/UpcastDowncast/Animal.csC#
namespace CodingNow.Lecture.Oop09;

internal class Animal
{
    public virtual void Speak() => Console.WriteLine("animal sounds");
}
src/UpcastDowncast/Dog.csC#
namespace CodingNow.Lecture.Oop09;

internal class Dog : Animal
{
    public override void Speak() => Console.WriteLine("Woof woof!");
    public void Bark() => Console.WriteLine("King! King!");
}
src/UpcastDowncast/Program.csC#
using CodingNow.Lecture.Oop09;

//Upcasting: Child (Dog) → Parent (Animal). Safe, so no explicit cast required.
Animal a = new Dog();
a.Speak();   //Virtual dispatch: Dog.Speak() is executed because the actual object is Dog

//Downcasting: Parent (Animal) → Child (Dog). OK only when the actual type is Dog.
Dog d = (Dog)a;
d.Bark();
src/UpcastDowncast/UpcastDowncast.csprojText
<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>

Assignment

Problem 1 — Vehicle/Car/Bike (Move() polymorphism)

  • Reference solution: shown below
  • Core concept: virtual / override, adding child objects to a collection

Requirements

  • There is virtual void Move() in the parent Vehicle class (default message output).
  • The children Car and Bike each override Move() and output different messages.
  • From Program.cs, place 3 to 4 objects in List<Vehicle>, iterate over them, and call Move().

Expected output

vehicle movement
The car is booming~
A bicycle follows me~
The car is booming~

Hint

  • Just put new Car() and new Bike() in List<Vehicle> (upcasting).

Problem 2 — abstract Animal + children Speak()

  • Reference solution: shown below
  • Core concepts: abstract class / abstract method, is pattern

Requirements

  • Place abstract void Speak() in abstract class Animal.
  • The children Dog, Cat, Cow override Speak() respectively.
  • Place additional method Bark() only in Dog.
  • Calling Speak() while traversing Animal[] zoo = [...], and also calling dog.Bark() when is Dog dog.

Expected output

Woof woof!
King! King!
Meow~
Mood~

Hint

  • The abstract method has no body and ends with a semicolon.
  • var animal = new Animal(); is a compilation error (abstract classes cannot be created directly).

Check the correct answer

After solving it yourself, compare it with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

Homework01/Homework01.csprojText
<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>
Homework01/Program.csC#
using CodingNow.Lecture.Oop09;

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

foreach (var v in vehicles)
{
    v.Move();
}
Homework01/Vehicles.csC#
namespace CodingNow.Lecture.Oop09;

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

internal class Car : Vehicle
{
    public override void Move() => Console.WriteLine("The car is booming~");
}

internal class Bike : Vehicle
{
    public override void Move() => Console.WriteLine("A bicycle follows me~");
}
Homework02/Animals.csC#
namespace CodingNow.Lecture.Oop09;

internal abstract class Animal
{
    //The child must implement it.
    public abstract void Speak();
}

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

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

internal class Cow : Animal
{
    public override void Speak() => Console.WriteLine("Mood~");
}
Homework02/Homework02.csprojText
<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>
Homework02/Program.csC#
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();
}

Learn this lesson with a mentor

Work through the C# (.NET 8 LTS) track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now