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
ispattern matching. - Know the use of
abstractclass andabstractmethod - 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
- Use only
(Dog)afor downcasting and omit the safety check to generateInvalidCastException→ Useisoras. - Trying to create the
abstractclass directly asnew— compilation error. - If the child of method
abstractdoes not useoverride, a compilation error occurs because the child is not processed as abstract. - I called a virtual method, but I think the parent version appears. Regardless of the variable type, the method of actual object is called.
- Trying to use a variable declared within the
ispattern outside ofif— the scope is usually inside theifblock.
Cleanup
- Polymorphism = "same call, different behavior".
virtual/overrideand virtual dispatch are the engines. - Upcasting is automatic, downcasting is safe with
ispattern oras. - The
abstractclass 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