Polymorphism in C#

Spread the love

Polymorphism is one of the core principles of object-oriented programming (OOP), and it plays an important role in making code flexible, reusable, and easy to maintain. In this blog, you will understand what polymorphism is, why it’s essential, and the different types of polymorphism in C#. We will also explore the Real-World Use Case of polymorphism.

What is Polymorphism?

The word “polymorphism” comes from Greek, meaning “many shapes” or “many forms.”  In programming, polymorphism allows objects of different types to be treated as objects of a common type. Polymorphism is important in any application because it reduces code repetition and allows developers to create generalized and reusable code. If you are not using polymorphism, you would have to write individual methods for each new class or object, which is difficult to manage.

Types of Polymorphism in C#

  1. Compile-time polymorphism (also known as static polymorphism)
  2. Run-time polymorphism (also known as dynamic polymorphism)

1. Compile-Time Polymorphism

Compile-time polymorphism occurs when the method that will be executed is determined during the compile time of the program. In C#, this is achieved using method overloading and operator overloading.

a) Method Overloading

Method overloading allows you to have multiple methods with the same name but different parameters. The compiler determines which version of the method to call based on the method’s signature (the number and type of parameters).
Example:

public class Calculator
{
    // Overloaded methods with different parameters
    public int Add(int a, int b)
    {
        return a + b;
    }

    public double Add(double a, double b)
    {
        return a + b;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Calculator calc = new Calculator();

        // Calling overloaded methods
        int sumInt = calc.Add(7,9);           // Calls Add(int, int)
        double sumDouble = calc.Add(5.3, 7.9); // Calls Add(double, double)

        Console.WriteLine("Integer sum: " + sumInt);         // Output: 16
        Console.WriteLine("Double sum: " + sumDouble);       // Output: 13.2
    }
}

In the above example, we have two Add functions. One function adds integer values, and the other function adds double values. Depending on the arguments passed, the compiler decides which method to call. If you pass integers like Add(7, 9), the first method is invoked. If you pass doubles like Add(5.3, 7.9), the second method is executed.

b) Operator Overloading

Operator overloading is another form of compile-time polymorphism. It allows you to redefine the functionality of an operator ( +, -, *, etc.) for your custom classes.
Example:

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }

    // Overloading the + operator for Point objects
    public static Point operator +(Point p1, Point p2)
    {
        return new Point { X = p1.X + p2.X, Y = p1.Y + p2.Y };
    }
}

class Program
{
    static void Main(string[] args)
    {
        Point p1 = new Point { X = 3, Y = 5 };
        Point p2 = new Point { X = 2, Y = 7 };

        // Using the overloaded + operator
        Point p3 = p1 + p2;

        Console.WriteLine($"Resulting Point: X = {p3.X}, Y = {p3.Y}"); // Output: X = 5, Y = 12
    }
}

Here, we have overloaded the + operator for the Point class. Now, you can add two Point objects together using the + operator.

Real-World Use Case of Compile-Time Polymorphism

Suppose you are building a calculator application. In this application, users can add, subtract, or multiply different types of numbers (integers, floating-point numbers, etc. ). To achieve this functionality, instead of writing separate methods for each number, you can use method overloading to handle these operations cleanly and efficiently. This makes your code more organized and easy to extend as new operations or data types are introduced. 

2. Run-Time Polymorphism

Run-time polymorphism occurs when the method that will be executed is determined during the program’s execution, not at compile time. This type of polymorphism is achieved using method overriding in inheritance and interfaces.

a) Method Overriding

Method overriding allows a subclass to provide an implementation of a method that is already defined in its parent class. To override a method in C#, you use the override keyword in the derived class and ensure the method in the base class is marked with the virtual keyword.

public class Animal
{
    // Virtual method in the base class
    public virtual void Speak()
    {
        Console.WriteLine("The animal makes a sound.");
    }
}

public class Dog : Animal
{
    // Overriding the Speak method
    public override void Speak()
    {
        Console.WriteLine("The dog barks.");
    }
}

public class Cat : Animal
{
    // Overriding the Speak method
    public override void Speak()
    {
        Console.WriteLine("The cat meows.");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Animal myDog = new Dog();  // Upcasting Dog to Animal
        Animal myCat = new Cat();  // Upcasting Cat to Animal

        // Calling the overridden Speak methods
        myDog.Speak();   // Output: The dog barks.
        myCat.Speak();   // Output: The cat meows.
    }
}

In this example, the Dog and Cat classes override the Speak method of the Animal class. When you call Speak on a Dog object, the method defined in the Dog class is executed. The same goes for the Cat class.

b) Interfaces and Polymorphism

Interfaces in C# are another way to implement run-time polymorphism. An interface defines a contract that classes must follow, and classes can implement multiple interfaces. The benefit here is that you can treat different classes that implement the same interface as instances of that interface.

Example:

public interface IShape
{
    void Draw();
}

public class Circle : IShape
{
    public void Draw()
    {
        Console.WriteLine("Drawing a circle.");
    }
}

public class Rectangle : IShape
{
    public void Draw()
    {
        Console.WriteLine("Drawing a rectangle.");
    }
}

class Program
{
    static void Main(string[] args)
    {
        IShape shape1 = new Circle();    // Polymorphic behavior
        IShape shape2 = new Rectangle(); // Polymorphic behavior

        // Calling the Draw method from different classes
        shape1.Draw();   // Output: Drawing a circle.
        shape2.Draw();   // Output: Drawing a rectangle.
    }
}

In this example, both Circle and Rectangle implement the IShape interface. When you call the Draw method on an object of type IShape, the appropriate implementation (either from Circle or Rectangle) is executed at runtime.

Real-World Use Case of Run-Time Polymorphism

Suppose you’re developing a graphics application that allows users to draw various shapes (circles, rectangles, triangles, etc.). Instead of writing separate code to handle each shape, you can define a common interface (IShape) with a Draw method. Then, you can create different classes for each shape that implement this method. This approach makes it easy to add new shapes in the future without changing the existing code.

Key Differences Between Compile-Time and Run-Time Polymorphism

AspectCompile-Time PolymorphismRun-Time Polymorphism
When determinedAt compile timeAt runtime
Achieved throughMethod overloading, operator overloadingMethod overriding, interfaces
FlexibilityLess flexible, method must be known at compile timeThe less flexible, method must be known at compile time
PerformanceGenerally faster, since the method is fixed at compile timeSlightly slower due to runtime decision making

Conclusion

Polymorphism is a powerful feature in C# that helps you write clean, reusable, and maintainable code. Whether you use compile-time polymorphism through method overloading or run-time polymorphism through method overriding and interfaces, you can significantly enhance the flexibility and scalability of your applications.

To summarize the key takeaways:

  • Polymorphism allows methods and functions to take many forms, making code more versatile.
  • Compile-time polymorphism is determined at compile time and can be achieved using method and operator overloading.
  • Run-time polymorphism is determined during runtime and can be achieved through method overriding and interfaces.
  • Polymorphism helps create reusable code, which is easier to maintain and extend.


Spread the love