Skip to main content

.NET Polymorphism

Introduction

Polymorphism, derived from Greek words meaning "many forms," is one of the four fundamental pillars of object-oriented programming (OOP) alongside encapsulation, inheritance, and abstraction. In .NET, polymorphism allows objects of different classes to be treated through a common interface, enabling more flexible and reusable code.

At its core, polymorphism enables a single interface to represent different underlying forms (data types). This powerful concept allows developers to write code that can work with objects of various types, treating them as instances of their common parent class or interface.

In this tutorial, we'll explore polymorphism in .NET, understand its different types, and see how it's implemented in C# with practical examples.

Types of Polymorphism in .NET

There are two main types of polymorphism in .NET:

  1. Compile-time (static) polymorphism: Achieved through method overloading and operator overloading
  2. Runtime (dynamic) polymorphism: Achieved through method overriding and interfaces

Let's explore each type in detail.

Compile-time Polymorphism

Method Overloading

Method overloading allows multiple methods with the same name but different parameters in the same class.

csharp
public class Calculator
{
// Add two integers
public int Add(int a, int b)
{
return a + b;
}

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

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

Usage Example:

csharp
Calculator calc = new Calculator();

// Calls the first Add method
int sum1 = calc.Add(5, 7);
Console.WriteLine($"Sum of two integers: {sum1}");

// Calls the second Add method
int sum2 = calc.Add(5, 7, 3);
Console.WriteLine($"Sum of three integers: {sum2}");

// Calls the third Add method
double sum3 = calc.Add(5.5, 7.5);
Console.WriteLine($"Sum of two doubles: {sum3}");

Output:

Sum of two integers: 12
Sum of three integers: 15
Sum of two doubles: 13

The compiler determines which method to call based on the number and types of the arguments, hence this is known as compile-time polymorphism.

Operator Overloading

.NET also allows you to overload operators for custom types.

csharp
public class Complex
{
public double Real { get; set; }
public double Imaginary { get; set; }

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

// Overload the + operator
public static Complex operator +(Complex c1, Complex c2)
{
return new Complex(c1.Real + c2.Real, c1.Imaginary + c2.Imaginary);
}

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

Usage Example:

csharp
Complex num1 = new Complex(3, 4);
Complex num2 = new Complex(2, 3);

// Uses the overloaded + operator
Complex sum = num1 + num2;

Console.WriteLine($"First complex number: {num1}");
Console.WriteLine($"Second complex number: {num2}");
Console.WriteLine($"Sum: {sum}");

Output:

First complex number: 3 + 4i
Second complex number: 2 + 3i
Sum: 5 + 7i

Runtime Polymorphism

Method Overriding

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

csharp
public class Shape
{
// Virtual method that can be overridden
public virtual double CalculateArea()
{
return 0;
}

public virtual void Display()
{
Console.WriteLine("This is a shape");
}
}

public class Circle : Shape
{
public double Radius { get; set; }

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

// Override the base class method
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}

public override void Display()
{
Console.WriteLine($"This is a circle with radius {Radius}");
}
}

public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }

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

// Override the base class method
public override double CalculateArea()
{
return Width * Height;
}

public override void Display()
{
Console.WriteLine($"This is a rectangle with width {Width} and height {Height}");
}
}

Usage Example:

csharp
// Create an array of shapes
Shape[] shapes = new Shape[3];
shapes[0] = new Circle(5);
shapes[1] = new Rectangle(4, 6);
shapes[2] = new Shape();

// Loop through the shapes and calculate areas
foreach (Shape shape in shapes)
{
shape.Display();
Console.WriteLine($"Area: {shape.CalculateArea():F2}");
Console.WriteLine();
}

Output:

This is a circle with radius 5
Area: 78.54

This is a rectangle with width 4 and height 6
Area: 24.00

This is a shape
Area: 0.00

Polymorphism with Interfaces

Interfaces provide another powerful way to implement polymorphism in .NET.

csharp
public interface IDrawable
{
void Draw();
}

public class Square : IDrawable
{
public double Side { get; set; }

public Square(double side)
{
Side = side;
}

public void Draw()
{
Console.WriteLine($"Drawing a square with side {Side}");
}
}

public class Triangle : IDrawable
{
public void Draw()
{
Console.WriteLine("Drawing a triangle");
}
}

Usage Example:

csharp
List<IDrawable> shapes = new List<IDrawable>
{
new Square(5),
new Triangle()
};

foreach (IDrawable shape in shapes)
{
shape.Draw();
}

Output:

Drawing a square with side 5
Drawing a triangle

Abstract Classes and Polymorphism

Abstract classes provide a perfect base for polymorphic behavior:

csharp
public abstract class Animal
{
public string Name { get; set; }

public Animal(string name)
{
Name = name;
}

// Abstract method - must be implemented by derived classes
public abstract string MakeSound();

// Non-abstract method that can be inherited as-is
public void Introduce()
{
Console.WriteLine($"I am {Name} and I say: {MakeSound()}");
}
}

public class Dog : Animal
{
public Dog(string name) : base(name) { }

public override string MakeSound()
{
return "Woof!";
}
}

public class Cat : Animal
{
public Cat(string name) : base(name) { }

public override string MakeSound()
{
return "Meow!";
}
}

Usage Example:

csharp
List<Animal> animals = new List<Animal>
{
new Dog("Rex"),
new Cat("Whiskers")
};

foreach (Animal animal in animals)
{
animal.Introduce();
}

Output:

I am Rex and I say: Woof!
I am Whiskers and I say: Meow!

Real-World Application: Payment System

Here's a practical example of polymorphism in a payment system:

csharp
// Base class for payment methods
public abstract class PaymentMethod
{
public abstract bool ProcessPayment(decimal amount);
public abstract string GetPaymentDetails();
}

// Different payment implementations
public class CreditCardPayment : PaymentMethod
{
private string _cardNumber;
private string _cardholderName;

public CreditCardPayment(string cardNumber, string cardholderName)
{
_cardNumber = cardNumber;
_cardholderName = cardholderName;
}

public override bool ProcessPayment(decimal amount)
{
// In real-world, this would contact a payment gateway
Console.WriteLine($"Processing ${amount} payment via Credit Card {MaskCardNumber(_cardNumber)}");
return true;
}

public override string GetPaymentDetails()
{
return $"Credit Card: {MaskCardNumber(_cardNumber)} (Cardholder: {_cardholderName})";
}

private string MaskCardNumber(string cardNumber)
{
return "XXXX-XXXX-XXXX-" + cardNumber.Substring(cardNumber.Length - 4);
}
}

public class PayPalPayment : PaymentMethod
{
private string _email;

public PayPalPayment(string email)
{
_email = email;
}

public override bool ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing ${amount} payment via PayPal account: {_email}");
return true;
}

public override string GetPaymentDetails()
{
return $"PayPal: {_email}";
}
}

public class Order
{
public int OrderId { get; set; }
public decimal TotalAmount { get; set; }
public PaymentMethod PaymentMethod { get; set; }

public bool Checkout()
{
Console.WriteLine($"Processing order #{OrderId} for ${TotalAmount}");
Console.WriteLine($"Payment method: {PaymentMethod.GetPaymentDetails()}");
return PaymentMethod.ProcessPayment(TotalAmount);
}
}

Usage Example:

csharp
// Create orders with different payment methods
var order1 = new Order
{
OrderId = 1001,
TotalAmount = 125.99m,
PaymentMethod = new CreditCardPayment("1234567890123456", "John Smith")
};

var order2 = new Order
{
OrderId = 1002,
TotalAmount = 75.50m,
PaymentMethod = new PayPalPayment("[email protected]")
};

// Process both orders without caring about payment method details
order1.Checkout();
Console.WriteLine();
order2.Checkout();

Output:

Processing order #1001 for $125.99
Payment method: Credit Card: XXXX-XXXX-XXXX-3456 (Cardholder: John Smith)
Processing $125.99 payment via Credit Card XXXX-XXXX-XXXX-3456

Processing order #1002 for $75.50
Payment method: PayPal: [email protected]
Processing $75.50 payment via PayPal account: [email protected]

In this example, the Order class doesn't need to know the specific details of how each payment method is processed. It works with the abstract PaymentMethod type, relying on polymorphism to call the appropriate implementation at runtime.

Benefits of Polymorphism

  • Code reusability: Write once, use many times
  • Flexibility: Easily add new derived classes without changing existing code
  • Maintainability: Changes to base class behavior automatically propagate to derived classes
  • Extensibility: Systems can be extended with new functionality without modifying existing code

Common Pitfalls

  • Performance overhead: Virtual method calls are slightly slower than non-virtual calls
  • Complexity: Overuse can lead to complex inheritance hierarchies that are hard to understand
  • Tight coupling: Poorly designed inheritance can create tight coupling between classes

Summary

Polymorphism is a powerful concept in .NET that allows objects of different types to be treated through a common interface. This enables you to write more flexible, maintainable, and extensible code. In this tutorial, we explored:

  1. Compile-time polymorphism through method overloading and operator overloading
  2. Runtime polymorphism through method overriding
  3. Polymorphism with interfaces and abstract classes
  4. A practical example of polymorphism in a payment system

By leveraging polymorphism correctly, you can create flexible and maintainable applications that are easy to extend and modify over time.

Exercises

  1. Create a Vehicle hierarchy with a base class and derived classes like Car, Motorcycle, and Truck. Implement methods like Start(), Stop(), and CalculateFuelEfficiency() that behave differently for each vehicle type.

  2. Design a notification system with an INotification interface and various implementations like EmailNotification, SMSNotification, and PushNotification.

  3. Extend the payment system example to include additional payment methods like BankTransfer and CryptoCurrency.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)