CodeGym /Courses /C# SELF /The Concept of Polymorphism and Its Role in OOP

The Concept of Polymorphism and Its Role in OOP

C# SELF
Level 21, Lesson 0
Available

1. Introduction

Imagine this: at home, you have a TV, a music center, an air conditioner, and a smart lamp. Each one has its own remote. To turn on the TV, you grab the TV remote and hit the "Turn On" button. To turn on the music center, you grab its remote and hit the same "Turn On" button. Got the picture? Total chaos, right?

But what if you had a universal remote? You pick it up, press the "Turn On" button, and it somehow magically figures out which device needs to be turned on and sends the right command. And the remote itself doesn't know how exactly the TV turns on or how exactly the music center turns on. It just knows that all these devices have a common "turn on" function.

That's basically an analogy for polymorphism in programming!

Polymorphism

The word "polymorphism" comes from the Greek words poly (many) and morph (form). Literally, it means "many forms." In the OOP context, it's the ability of an object to take on different forms or, more precisely, the ability of the same method to behave differently depending on the type of object it's called on.

In C#, polymorphism is mainly achieved through inheritance and the use of virtual and override methods.

The key idea of polymorphism is upcasting. What's that?

Let's go back to our hierarchy Animal → Dog, Cat. We know that a dog is an animal. A cat is an animal. This "is-a" relationship is the fundamental basis of inheritance.


public class Animal
{
    public virtual void MakeSound()
    {
        Console.WriteLine("Some sound...");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Woof-woof!");
    }
}

public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Meow!");
    }
}

Thanks to the "is-a" relationship, we can assign a derived class object to a base class variable. This is called upcasting because we're kind of "lifting" the object up to a more general, base type. The C# compiler lets you do this automatically:


// We have a specific dog
Dog myDog = new Dog();

// We can assign a Dog object to a variable of type Animal!
// This is upcasting.
// On the right - a specific Dog, on the left - a more general Animal.
Animal generalAnimal = myDog;

// And we can call MakeSound()!
generalAnimal.MakeSound(); // Outputs "Woof-woof!" (Not "Some sound...", but exactly "Woof-woof!")               

What happened here?

  1. When we wrote Animal generalAnimal = myDog;, we didn't create a new animal. We just took the myDog object (which is actually a Dog) and put it in a "box" labeled Animal.
  2. At compile time (when the code is turned into bytecode), the variable generalAnimal has the type Animal. So the compiler "knows" it can only call methods and properties that exist in the Animal class.
  3. But here's the cool part: when it comes to runtime (when the program is actually running), and we call generalAnimal.MakeSound(), the .NET runtime doesn't look at the variable's type (Animal), but at the actual type of the object inside (Dog)! And since the MakeSound() method was marked as virtual in Animal and override in Dog, it calls the implementation from Dog.

This magic, where the method's behavior is determined by the actual type of the object at runtime and not the variable's type, is called dynamic dispatch or runtime polymorphism.

Imagine a box: on the outside, it says "Fruit" (that's our Animal variable type). Inside, you put an apple (that's our Dog object). When you ask "Fruit, make a sound!" (call MakeSound()), the box, knowing there's an apple inside, makes a "crunch" sound, not some generic "fruit" sound.

2. Polymorphism in Action

The best way to get polymorphism is to see it in action, especially when you have not just one, but a whole bunch of objects.

Let's expand our "My Little Zoo" app. Imagine we're working at a vet clinic, and different animals are brought in for checkups. We want each checkup to follow its own rules, but the code that calls the checkup should be universal.

First, let's add a new virtual method Examine() to our base Animal class and its derived classes Dog and Cat:


public class Animal
{
    public string Name { get; set; }
    public Animal(string name) { Name = name; }
    public virtual void MakeSound() { Console.WriteLine($"{Name} makes some sound..."); }
    public virtual void Examine()
    {
        Console.WriteLine($"Examining {Name}:");
        Console.WriteLine("  - Checking breathing.");
        Console.WriteLine("  - Measuring temperature.");
    }
}

public class Dog : Animal
{
    public Dog(string name) : base(name) { }
    public override void MakeSound() { Console.WriteLine($"{Name} says: Woof!"); }
    public override void Examine()
    {
        base.Examine();
        Console.WriteLine("  - Checking teeth.");
        Console.WriteLine("  - Rabies vaccination.");
    }
}

public class Cat : Animal
{
    public Cat(string name) : base(name) { }
    public override void MakeSound() { Console.WriteLine($"{Name} says: Meow!"); }
    public override void Examine()
    {
        base.Examine();
        Console.WriteLine("  - Checking claws.");
        Console.WriteLine("  - Deworming pill.");
    }
}

Now we've got specialized Examine() methods for dogs and cats. Notice the base.Examine(); in the overridden methods. That lets us run the common checkup steps (from the base Animal class) first, then add animal-specific stuff. Super handy!

Now let's picture our vet clinic. We've got a list of animals that came in for a checkup:


class Program
{
    static void Main()
    {
        Animal[] animals = {
            new Dog("Rex"),
            new Cat("Murka"),
            new Animal("Rabbit")
        };

        Console.WriteLine("--- Animal Checkup ---");
        foreach (Animal animal in animals)
        {
            animal.Examine();
            Console.WriteLine();
        }

        Console.WriteLine("--- Voice Hour ---");
        foreach (Animal animal in animals)
            animal.MakeSound();
    }
}

Result:


--- Animal Checkup ---
Examining Rex:
  - Checking breathing.
  - Measuring temperature.
  - Checking teeth.
  - Rabies vaccination.

Examining Murka:
  - Checking breathing.
  - Measuring temperature.
  - Checking claws.
  - Deworming pill.

Examining Rabbit:
  - Checking breathing.
  - Measuring temperature.

--- Voice Hour ---
Rex says: Woof!
Murka says: Meow!
Rabbit makes some sound...

That's the power of polymorphism! We wrote the same foreach (Animal animal in animals) loop, and in each iteration called the same animal.Examine() (or animal.MakeSound()) method. But each time, the right, specific implementation ran for the dog, cat, or just the generic animal. The code stays simple, clean, and universal, and the implementation details are hidden inside each class.

If tomorrow someone brings in a hamster Hamster, all we need to do is create a Hamster : Animal class, override Examine() in it, and it'll work in our vet checkup system without any changes to the foreach loop! That's the real strength of polymorphism.

3. The Role of Polymorphism in OOP

Polymorphism isn't just a fancy word or a code trick. It's a fundamental principle that makes your programs flexible, scalable, and easy to maintain. Let's break down the role it plays:

Flexibility and Scalability (Open/Closed Principle)

This is probably the biggest advantage. Polymorphism lets you write code that's open for extension but closed for modification. What does that mean?

  • Open for extension: You can add new animal types (like Bird, Fish, Hamster) just by creating new classes that inherit from Animal and override the needed methods.
  • Closed for modification: You don't need to change any existing code that works with Animal[]. The foreach loop that calls animal.Examine() stays totally unchanged, no matter how many new animal types you add.

Imagine how many if-else if or switch statements you'd have to write and constantly update if there was no polymorphism! That would be a real headache.

Code Simplification

Polymorphism seriously simplifies code that works with collections of different but related objects. Instead of figuring out each object's type and then calling the right method, you just call the common base class method, and the system figures out which implementation to run.

It's like having one "Open" button for different doors: regular, sliding, revolving. You don't have to pull, push, or spin each time—just "open," and the specific door does its thing.

Abstraction

Polymorphism is closely tied to abstraction, another pillar of OOP that we'll talk about in detail in future lectures. It lets you focus on "what" an object does (like "makes a sound," "gets examined") instead of "how" it does it. You work with the abstract idea of "animal," not a specific "dog" or "cat." This lets you write high-level, clean code that doesn't depend on low-level implementation details.

Code Reuse

While inheritance itself gives you code reuse by letting you reuse properties and methods from the base class, polymorphism takes it further. It lets you create universal algorithms and data structures (like Animal[]) that can work with any objects inherited from the base class, without duplicating logic for each derived type.

Basically, polymorphism makes your code more "professional" and ready for change, which is super important in real-world development where requirements are always changing.

4. Handy Nuances

Polymorphism Diagram

classDiagram
    class Animal {
        +MakeSound()
    }
    class Dog {
        +MakeSound()
    }
    class Cat {
        +MakeSound()
    }
    class Parrot {
        +MakeSound()
    }

    Animal <|-- Dog
    Animal <|-- Cat
    Animal <|-- Parrot
Class hierarchy diagram for polymorphism

When you call MakeSound() through a reference of type Animal — the method of the class whose object is actually in the variable will be called.

Using Polymorphism with Arrays and Collections

A super common task is to loop through a collection of different entities and "run" the same logic on all of them.


// In our main training app:
Animal[] zoo = { new Dog("Sharik"), new Cat("Barsik"), new Parrot("Kesha") };

foreach (Animal animal in zoo)
{
    animal.MakeSound();
}

In real projects, this is how they do things like event handling, drawing on the screen (each shape in its own way), payment processing (different card types and services).

How Polymorphism Works

Object Type Variable Type Which method is called?
Dog
Animal
Dog.MakeSound()
Cat
Animal
Cat.MakeSound()
Parrot
Animal
Parrot.MakeSound()
Animal
Animal
Animal.MakeSound()

The main thing — the method must be virtual or abstract in the base class and override in the derived class!

5. Common Mistakes with Polymorphism

In real life, students often run into these mistakes:

  • You don't make the method virtual in the base class — and instead of polymorphism, you always get the same behavior.
  • You get confused: which class should the variable be? Always remember: the variable has a type (like Animal), and the object you put in it is an instance (like new Dog()).
  • You try to use new members that aren't in the base class through a base type variable. For example:

Animal pet = new Dog();
pet.Bark(); // Error! Animal doesn't have Bark()

How to deal with this? If you really need to, use type casting, but try to structure your code so you only work with methods that exist in the base class.

2
Task
C# SELF, level 21, lesson 0
Locked
Simple demonstration of polymorphism
Simple demonstration of polymorphism
2
Task
C# SELF, level 21, lesson 0
Locked
Animal Checkup
Animal Checkup
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION