.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:
- Compile-time (static) polymorphism: Achieved through method overloading and operator overloading
- 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.
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:
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.
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:
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.
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:
// 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.
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:
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:
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:
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:
// 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:
// 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:
- Compile-time polymorphism through method overloading and operator overloading
- Runtime polymorphism through method overriding
- Polymorphism with interfaces and abstract classes
- 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
-
Create a
Vehicle
hierarchy with a base class and derived classes likeCar
,Motorcycle
, andTruck
. Implement methods likeStart()
,Stop()
, andCalculateFuelEfficiency()
that behave differently for each vehicle type. -
Design a notification system with an
INotification
interface and various implementations likeEmailNotification
,SMSNotification
, andPushNotification
. -
Extend the payment system example to include additional payment methods like
BankTransfer
andCryptoCurrency
.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)