DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Mastering C# Fundamentals: Polymorphism

Meta Description: Learn about polymorphism in C#, a key object-oriented programming concept. Understand how to use virtual and override keywords to achieve flexible and maintainable code with real-world examples.

In C#, polymorphism is a key concept of object-oriented programming that allows methods to have multiple forms. If you have understood inheritance, polymorphism is the next essential step in leveraging the power of object-oriented programming to make your code more dynamic and maintainable. Let's break down polymorphism in a clear, practical way.

The Concept of Polymorphism

The term polymorphism comes from Greek, meaning "many forms." In programming, polymorphism allows objects to take on different forms depending on their context. Specifically, polymorphism allows a base class to define methods that derived classes can override to provide their own unique implementations.

Polymorphism helps us:

  • Reuse code effectively.
  • Write more extensible and maintainable code.
  • Treat instances of derived classes uniformly through a base class reference.

Consider a scenario involving employees in a company. You might have a base class Employee, and derived classes like Manager and Intern. Although every employee has some common functionalities, such as working, each type of employee may have different ways of implementing that functionality.

Virtual and Override Keywords

To achieve polymorphism in C#, we use the virtual and override keywords. Let’s see an example:

public class Employee
{
    public virtual void PerformWork()
    {
        Console.WriteLine("Employee is working.");
    }
}
Enter fullscreen mode Exit fullscreen mode

The PerformWork method here is marked as virtual, meaning it can be overridden in any derived class.

Now let’s define a Manager class:

public class Manager : Employee
{
    public override void PerformWork()
    {
        Console.WriteLine("Manager is managing team meetings and setting goals.");
    }
}
Enter fullscreen mode Exit fullscreen mode

And an Intern class:

public class Intern : Employee
{
    public override void PerformWork()
    {
        Console.WriteLine("Intern is assisting with tasks and learning the workflow.");
    }
}
Enter fullscreen mode Exit fullscreen mode

By marking PerformWork in Employee as virtual, we have given derived classes like Manager and Intern the ability to provide their own specific implementation of that method using the override keyword.

Polymorphism in Action

Here’s a more realistic scenario where polymorphism is useful: we have a list of various employees, and we want to call the PerformWork method on all of them without having to worry about their specific type.

List<Employee> employees = new List<Employee>
{
    new Employee(),
    new Manager(),
    new Intern()
};

foreach (var employee in employees)
{
    employee.PerformWork();
}
Enter fullscreen mode Exit fullscreen mode

Output:

Employee is working.
Manager is managing team meetings and setting goals.
Intern is assisting with tasks and learning the workflow.
Enter fullscreen mode Exit fullscreen mode

Notice how each employee type calls the appropriate PerformWork method based on the actual instance. Even though the list is typed as Employee, each derived class’s overridden method is used. This is the essence of polymorphism.

Why Use Polymorphism?

One common question is, "Why would I use polymorphism?" One reason is simplicity. When you have a collection of various objects that share a common base class, polymorphism allows you to call methods uniformly, without writing specific logic for each type. This makes your code more flexible and scalable.

Imagine we want to process monthly salaries for different employees:

public class Employee
{
    public virtual void ProcessSalary()
    {
        Console.WriteLine("Processing standard salary.");
    }
}

public class Manager : Employee
{
    public override void ProcessSalary()
    {
        Console.WriteLine("Processing manager salary with bonuses.");
    }
}

public class Intern : Employee
{
    public override void ProcessSalary()
    {
        Console.WriteLine("Processing intern stipend.");
    }
}

List<Employee> employees = new List<Employee>
{
    new Employee(),
    new Manager(),
    new Intern()
};

foreach (var employee in employees)
{
    employee.ProcessSalary();
}
Enter fullscreen mode Exit fullscreen mode

Output:

Processing standard salary.
Processing manager salary with bonuses.
Processing intern stipend.
Enter fullscreen mode Exit fullscreen mode

Using polymorphism, the correct ProcessSalary method is called based on the type of the employee without additional if-else or switch statements, simplifying code management and reducing potential errors.

Base Type References to Derived Instances

Polymorphism also allows you to reference derived class instances using a base class reference. For example:

Employee manager = new Manager();
manager.PerformWork(); // Output: Manager is managing team meetings and setting goals.
Enter fullscreen mode Exit fullscreen mode

Even though manager is of type Employee, it points to a Manager object. When calling PerformWork, the Manager version of the method is invoked, thanks to polymorphism.

However, note that if the Manager class has methods that do not exist in Employee, those methods won’t be accessible through an Employee reference:

public class Manager : Employee
{
    public override void PerformWork()
    {
        Console.WriteLine("Manager is managing team meetings and setting goals.");
    }

    public void ConductMeeting()
    {
        Console.WriteLine("Manager is conducting a meeting.");
    }
}

Employee employee = new Manager();
employee.ConductMeeting(); // Error: 'Employee' does not contain a definition for 'ConductMeeting'
Enter fullscreen mode Exit fullscreen mode

Assignments to Practice Polymorphism

To help you understand polymorphism more deeply, here are three levels of assignments:

Easy Level

  1. Create a Simple Inheritance Chain:
    • Create a base class named Vehicle with a virtual method StartEngine().
    • Create derived classes Car and Motorbike that override the StartEngine() method.
    • Instantiate both Car and Motorbike and call StartEngine().

Medium Level

  1. Implement a Polymorphic Animal Hierarchy:

    • Create an abstract base class Animal with a method MakeSound().
    • Create derived classes like Dog, Cat, and Bird that override MakeSound() with different outputs.
    • Put the animals in a list of Animal and call MakeSound() for each.
  2. Bonus Calculation System:

    • Create an Employee class with a GiveBonus() method.
    • Create subclasses like Manager, SalesPerson, and Intern that override GiveBonus() with their unique implementations.
    • Create a list of different types of employees and call GiveBonus() on each.

Difficult Level

  1. Shape Hierarchy with Abstract Classes:
    • Create an abstract base class Shape with an abstract method CalculateArea().
    • Implement derived classes like Circle, Rectangle, and Triangle with specific implementations of CalculateArea().
    • Create a list of Shape objects and iterate over the list to calculate and print each shape's area.

Real-World Example: Company Hierarchy

To simulate a more complex real-world scenario, you can create a system to manage a company's hierarchy:

  1. Base Class: Employee: Include common methods like PerformWork(), TakeLeave().
  2. Derived Classes: Manager, Engineer, Intern: Each class should override PerformWork() and include unique methods like ApproveLeave() (for Manager).
  3. Scenario:
    • Create a list of Employee objects, containing different types of employees.
    • Iterate through this list, calling PerformWork().
    • Implement methods like AssignTask() where managers can assign tasks to engineers, highlighting interaction between classes.

Conclusion

Polymorphism allows you to write flexible, reusable, and maintainable code. It lets you treat objects of different types in a uniform way while still utilizing their specific behavior. With the right use of virtual and override keywords, polymorphism brings flexibility and scalability to your applications. It allows your code to be more readable and concise, eliminating redundancy and the need for cumbersome conditional statements.

Top comments (0)