C# Polymorphism

Polymorphism is one of the four fundamental principles of Object-Oriented Programming (OOP), along with Encapsulation, Inheritance, and Abstraction. The term polymorphism is derived from the Greek words "poly" (many) and "morph" (form), meaning the ability to take many forms. In C#, polymorphism allows objects to be treated as instances of their base type rather than their actual derived type.

1. Purpose of Polymorphism

The primary purposes of polymorphism include:

- Flexibility: Write code that works with objects of different types in a uniform manner.

- Extensibility: Add new classes that implement existing interfaces or inherit from base classes without changing existing code.

- Maintainability: Reduce code duplication by utilizing base class references.

2. Types of Polymorphism in C#

In C#, polymorphism can be categorized into two main types:

- Compile-Time Polymorphism (Static Polymorphism): Achieved through method overloading and operator overloading.

- Runtime Polymorphism (Dynamic Polymorphism): Achieved through method overriding using virtual methods and interfaces.

3. Compile-Time Polymorphism

Compile-time polymorphism is resolved during the compilation process. It is achieved through:

- Method Overloading: Defining multiple methods with the same name but different parameters.

- Operator Overloading: Defining custom behavior for operators when applied to user-defined types.

3.1 Method Overloading Method overloading allows a class to have multiple methods with the same name but different signatures (parameter lists).

Example of Method Overloading:
using System;

class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }

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

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

class Program
{
    static void Main()
    {
        Calculator calc = new Calculator();
        Console.WriteLine("Add(int, int): " + calc.Add(1, 2));          // Output: 3
        Console.WriteLine("Add(double, double): " + calc.Add(1.5, 2.5)); // Output: 4
        Console.WriteLine("Add(int, int, int): " + calc.Add(1, 2, 3));   // Output: 6
    }
}

Output:
Add(int, int): 3
Add(double, double): 4
Add(int, int, int): 6


3.2 Operator Overloading Operator overloading allows you to redefine the way operators work with user-defined types (classes and structs).

Example of Operator Overloading:
using System;

struct Complex
{
    public double Real { get; }
    public double Imaginary { get; }

    public Complex(double real, double imaginary)
    {
        Real = real;
        Imaginary = imaginary;
    }

    // Overloading the + operator
    public static Complex operator +(Complex a, Complex b)
    {
        return new Complex(a.Real + b.Real, a.Imaginary + b.Imaginary);
    }

    public override string ToString()
    {
        return $"{Real} + {Imaginary}i";
    }
}

class Program
{
    static void Main()
    {
        Complex c1 = new Complex(1.0, 2.0);
        Complex c2 = new Complex(3.0, 4.0);
        Complex sum = c1 + c2;
        Console.WriteLine("Sum: " + sum); // Output: Sum: 4 + 6i
    }
}

Output:
Sum: 4 + 6i

4. Runtime Polymorphism

Runtime polymorphism is achieved through inheritance and interfaces, and it is resolved during runtime. It allows derived classes to override methods of the base class to provide specific implementations.

- Method Overriding: Using the virtual and override keywords.

- Interfaces: Implementing methods defined in an interface.

4.1 Method Overriding
Method overriding allows a derived class to provide a specific implementation of a method that is already defined in its base class.

Example of Method Overriding:
using System;

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal speaks");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Dog barks");
    }
}

class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Cat meows");
    }
}

class Program
{
    static void Main()
    {
        Animal myAnimal;

        myAnimal = new Dog();
        myAnimal.Speak(); // Output: Dog barks

        myAnimal = new Cat();
        myAnimal.Speak(); // Output: Cat meows
    }
}

Output:
Dog barks
Cat meows


4.2 Using Interfaces
Interfaces define a contract that implementing classes must follow. This allows different classes to be treated uniformly when they implement the same interface.

Example of Polymorphism with Interfaces:
using System;
using System.Collections.Generic;

interface IShape
{
    double CalculateArea();
}

class Circle : IShape
{
    public double Radius { get; }

    public Circle(double radius)
    {
        Radius = radius;
    }

    public double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

class Rectangle : IShape
{
    public double Width { get; }
    public double Height { get; }

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public double CalculateArea()
    {
        return Width * Height;
    }
}

class Program
{
    static void Main()
    {
        List<IShape> shapes = new List<IShape>
        {
            new Circle(5),
            new Rectangle(4, 6)
        };

        foreach (IShape shape in shapes)
        {
            Console.WriteLine("Area: " + shape.CalculateArea());
        }
        // Output:
        // Area: 78.53981633974483
        // Area: 24
    }
}

Output:
Area: 78.53981633974483
Area: 24

5. The virtual, override, and abstract Keywords

- virtual: Used in the base class to indicate that a method can be overridden.

- override: Used in the derived class to override a virtual method.

- abstract: Used to declare a method without implementation in an abstract class; derived classes must override it.

Example with abstract Method:
using System;

abstract class Shape
{
    public abstract double CalculateArea();
}

class Triangle : Shape
{
    public double BaseLength { get; }
    public double Height { get; }

    public Triangle(double baseLength, double height)
    {
        BaseLength = baseLength;
        Height = height;
    }

    public override double CalculateArea()
    {
        return 0.5 * BaseLength * Height;
    }
}

class Program
{
    static void Main()
    {
        Shape triangle = new Triangle(5, 10);
        Console.WriteLine("Area of Triangle: " + triangle.CalculateArea()); // Output: Area of Triangle: 25
    }
}

Output:
Area of Triangle: 25

6. Upcasting and Downcasting

- Upcasting: Converting a derived class object to a base class reference.

- Downcasting: Converting a base class reference back to a derived class object (requires explicit casting).

Example of Upcasting and Downcasting:
using System;

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

class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Dog barks");
    }

    public void Fetch()
    {
        Console.WriteLine("Dog fetches the ball");
    }
}

class Program
{
    static void Main()
    {
        // Upcasting
        Animal animal = new Dog();
        animal.Speak(); // Output: Dog barks

        // Downcasting
        Dog dog = (Dog)animal;
        dog.Fetch(); // Output: Dog fetches the ball
    }
}

Output:
Dog barks
Dog fetches the ball

7. Polymorphism with Collections

Polymorphism is particularly useful when working with collections of objects of different types that share a common base class or interface.

Example:
using System;
using System.Collections.Generic;

abstract class Employee
{
    public string Name { get; set; }
    public abstract void Work();
}

class Manager : Employee
{
    public override void Work()
    {
        Console.WriteLine($"{Name} is managing the team.");
    }
}

class Developer : Employee
{
    public override void Work()
    {
        Console.WriteLine($"{Name} is writing code.");
    }
}

class Designer : Employee
{
    public override void Work()
    {
        Console.WriteLine($"{Name} is designing interfaces.");
    }
}

class Program
{
    static void Main()
    {
        List<Employee> employees = new List<Employee>
        {
            new Manager { Name = "Alice" },
            new Developer { Name = "Bob" },
            new Designer { Name = "Charlie" }
        };

        foreach (Employee employee in employees)
        {
            employee.Work();
        }
        // Output:
        // Alice is managing the team.
        // Bob is writing code.
        // Charlie is designing interfaces.
    }
}

Output:
Alice is managing the team.
Bob is writing code.
Charlie is designing interfaces.

8. Real-World Example: Polymorphism in a Payment System

Consider a payment processing system where different payment methods (credit card, PayPal, bank transfer) need to be handled uniformly.

Example:
using System;
using System.Collections.Generic;

abstract class PaymentMethod
{
    public abstract void ProcessPayment(decimal amount);
}

class CreditCard : PaymentMethod
{
    public override void ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Processing credit card payment of {amount:C}");
    }
}

class PayPal : PaymentMethod
{
    public override void ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Processing PayPal payment of {amount:C}");
    }
}

class BankTransfer : PaymentMethod
{
    public override void ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Processing bank transfer of {amount:C}");
    }
}

class Program
{
    static void Main()
    {
        List<PaymentMethod> payments = new List<PaymentMethod>
        {
            new CreditCard(),
            new PayPal(),
            new BankTransfer()
        };

        foreach (PaymentMethod payment in payments)
        {
            payment.ProcessPayment(100m);
        }
        // Output:
        // Processing credit card payment of $100.00
        // Processing PayPal payment of $100.00
        // Processing bank transfer of $100.00
    }
}

Output:
Processing credit card payment of $100.00
Processing PayPal payment of $100.00
Processing bank transfer of $100.00

9. Summary

Polymorphism in C# allows for writing flexible and maintainable code by enabling objects to be treated as instances of their base types. By leveraging method overloading, method overriding, and interfaces, developers can create applications that are extensible and easier to manage.

Key points

- Compile-Time Polymorphism is achieved through method overloading and operator overloading.

- Runtime Polymorphism is achieved through method overriding and interfaces.

- The virtual keyword enables methods to be overridden in derived classes.

- The override keyword is used to override base class methods.

- The abstract keyword defines methods that must be implemented in derived classes.

Understanding and effectively applying polymorphism is essential for creating robust and scalable object-oriented applications in C#.

Previous: C# Inheritance | Next: C# Namespaces

<
>