Page Content

Tutorials

C# Polymorphism Explained: Flexibility, Extensibility in OOP

C# Polymorphism

OOP, or object-oriented programming, is based on the core idea of polymorphism. “Polymorphism” is derived from a Greek word that means “one name many forms,” signifying that an entity can have more than one form or function. A class can have more than one implementation with the same name with this feature.

Types of polymorphism

There are primarily two categories of polymorphism:

Static Polymorphism (Compile-Time Polymorphism / Early Binding / Overloading)

  • Compile time determines the precise function to be used in static polymorphism.
  • Operator overloading and function (or method) overloading are the main ways in which this is accomplished.
  • Method overloading is the practice of defining several methods with the same name but distinct signatures (number, type, order, or type of parameters) inside a class. In order to identify and connect the method call to its definition during compilation, the compiler looks for the method’s signature.
  • By providing flexibility, overloading enables a method to be applied to many data types without the need for generics or explicit type conversion.

Dynamic Polymorphism (Run-Time Polymorphism / Late Binding / Overriding)

  • The choice of which method to use is made at runtime in dynamic polymorphism.
  • Method overriding, which involves re-implementing a base class’s non-static method in a subclass with the same prototype (same name and signature), is how it is accomplished.
  • Use of particular keywords is necessary for this:
    • To expressly enable overriding of a method, property, indexer, or event in a derived class, the virtual keyword is used in the base class. C# methods aren’t always virtual. Interestingly, virtual methods are not private.
    • To extend or change the virtual or abstract method, property, indexer, or event that was inherited from the base class, the derived class uses the override keyword. Override is a must in C#; if you try to re-implement a virtual method without it, you will receive a compiler warning (CS0108). It is better to use the new keyword rather than override if the goal is to conceal a base class member that does not exhibit polymorphic behaviour.
  • All C# types, whether explicitly or implicitly, derive from System, which is the object class.Object (or object) is the most basic class. Several fundamental methods defined by this class, including ToString(), Equals(), and GetHashCode(), can be modified in derived classes to provide behaviour particular to a given type. A more useful textual representation is possible by altering the default ToString() method, which typically returns the whole type name. Console.WriteLine() implicitly invokes its ToString() function when you use it with an object.

Benefits of Dynamic Polymorphism

For software design and maintenance, polymorphism offers several benefits:

Substitutability: It makes it possible to consider derived class objects as base class objects. This implies that a subclass object can be positioned anywhere a superclass object is designated. Subclasses ensure substitutability by guaranteeing that they implement all of their superclass’s methods.

Extensibility: Without requiring modifications to already-existing software components, systems can be readily expanded by adding new specialised classes. This lowers software adaptation costs, time, and effort.

Code Simplification: Because polymorphism implicitly determines the right method implementation at run-time depending on the actual object type, developers can frequently avoid using complicated conditional if statements.

Code Example

Using a Shape example, where many forms can compute their area, let’s demonstrate polymorphism.

using System;
// Base abstract class demonstrating an abstract method (implicitly virtual)
// and a virtual method.
// Abstract classes cannot be instantiated directly.
public abstract class Shape
{
    public string Name { get; protected set; } // Property to hold the shape's name
    public Shape(string name)
    {
        Name = name;
    }
    // An abstract method declares that derived classes *must* provide an implementation.
    // Abstract methods are implicitly virtual.
    public abstract double CalculateArea(); // No implementation in base class
    // A virtual method provides a default implementation that can be overridden by derived classes.
    public virtual void DisplayInfo() // Marked as virtual
    {
        Console.WriteLine($"This is a generic shape: {Name}");
    }
    // Overriding the ToString() method from System.Object to provide a meaningful representation.
    // This is an example of polymorphism from the root Object class.
    public override string ToString()
    {
        return $"Shape Type: {Name}";
    }
}
// Derived class: Circle
public class Circle : Shape
{
    public double Radius { get; set; }
    public Circle(string name, double radius) : base(name) // Call base class constructor
    {
        Radius = radius;
    }
    // Override the abstract CalculateArea method.
    // The 'override' keyword is mandatory for re-implementing base class virtual/abstract methods.
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
    // Override the virtual DisplayInfo method to provide specific behavior for Circle.
    public override void DisplayInfo()
    {
        // Using :F2 to format the area to two decimal places for readability
        Console.WriteLine($"This is a Circle: {Name} with Radius {Radius:F2}. Area: {CalculateArea():F2}");
    }
}
// Derived class: Rectangle
public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    public Rectangle(string name, double width, double height) : base(name)
    {
        Width = width;
        Height = height;
    }
    // Override the abstract CalculateArea method.
    public override double CalculateArea()
    {
        return Width * Height;
    }
    // Override the virtual DisplayInfo method to provide specific behavior for Rectangle.
    public override void DisplayInfo()
    {
        // Using :F2 to format the area to two decimal places for readability
        Console.WriteLine($"This is a Rectangle: {Name} with Width {Width:F2} and Height {Height:F2}. Area: {CalculateArea():F2}");
    }
}
public class PolymorphismExample
{
    public static void Main(string[] args)
    {
        // Demonstrate Polymorphism:
        // We can create objects of derived classes and refer to them using the base class type (Shape).
        // This is known as "substitutability".
        // Correctly initialize the array with a size sufficient for the elements you intend to add.
        Shape[] shapes = new Shape[3]; // An array of the base type 'Shape' to hold 3 shapes
        // Assign instances of derived classes to the array elements at valid indices.
        shapes[0] = new Circle("Red Circle", 5);       // A Circle object
        shapes[1] = new Rectangle("Blue Rectangle", 4, 6); // A Rectangle object
        // Note: We cannot do new Shape("Generic"); because Shape is abstract.
        shapes[2] = new Circle("Green Oval", 3.5); // Another Circle object
        Console.WriteLine("--- Demonstrating Polymorphic Behavior ---");
        Console.WriteLine("Iterating through a collection of 'Shape' objects:");
        foreach (Shape shape in shapes) // Iterating through the base type
        {
            // At run-time, the Common Language Runtime (CLR) determines the actual type of the object
            // and invokes the correct overridden method implementation.
            shape.DisplayInfo(); // Calls the overridden DisplayInfo for Circle or Rectangle
            Console.WriteLine($"ToString() representation: {shape.ToString()}"); // Calls overridden ToString()
            Console.WriteLine("----------------------------------");
        }
        Console.WriteLine("\n--- Direct Call to base ToString() (for comparison) ---");
        // Although not an 'Object' class example directly,
        // it shows how ToString() is overridden from a common ancestor.
        object genericObject = new object();
        Console.WriteLine($"Default Object.ToString(): {genericObject.ToString()}"); // Outputs "System.Object" or similar
        
    }
}

Output

--- Demonstrating Polymorphic Behavior ---
Iterating through a collection of 'Shape' objects:
This is a Circle: Red Circle with Radius 5.00. Area: 78.54
ToString() representation: Shape Type: Red Circle
----------------------------------
This is a Rectangle: Blue Rectangle with Width 4.00 and Height 6.00. Area: 24.00
ToString() representation: Shape Type: Blue Rectangle
----------------------------------
This is a Circle: Green Oval with Radius 3.50. Area: 38.48
ToString() representation: Shape Type: Green Oval
----------------------------------
--- Direct Call to base ToString() (for comparison) ---
Default Object.ToString(): System.Object

You can also read Method Overriding In C#: Behaviour Growth in Derived Classes

Agarapu Geetha
Agarapu Geetha
My name is Agarapu Geetha, a B.Com graduate with a strong passion for technology and innovation. I work as a content writer at Govindhtech, where I dedicate myself to exploring and publishing the latest updates in the world of tech.
Index