Polymorphism is one of the core principles of object-oriented programming (OOP) and is crucial for writing flexible, reusable, and extensible code.
The term means "many shapes," and in C#, it allows objects of different classes to be treated as objects of a common base class or interface. C# achieves this in two primary ways: compile-time polymorphism and runtime polymorphism.
Compile-time polymorphism (Static polymorphism)
Compile-time polymorphism is also known as static polymorphism or early binding, because the compiler determines which method to call at compile time. This is achieved through method overloading and operator overloading.
Method overloading
Method overloading involves creating multiple methods within the same class that share the same name but have different signatures. The signature is determined by the number, type, and order of the parameters. The compiler uses this information to bind the correct method call during compilation.
**Example:**In this example, the Calculator class has three Add methods. The compiler can differentiate them based on the number and type of parameters passed.
using System;
public class Calculator
{
// Method to add two integers
public int Add(int a, int b)
{
return a + b;
}
// Overloaded method to add three integers
public int Add(int a, int b, int c)
{
return a + b + c;
}
// Overloaded method to add two doubles
public double Add(double a, double b)
{
return a + b;
}
}
public class Program
{
public static void Main()
{
Calculator calculator = new Calculator();
Console.WriteLine(calculator.Add(5, 10)); // Calls Add(int, int) -> Output: 15
Console.WriteLine(calculator.Add(5, 10, 15)); // Calls Add(int, int, int) -> Output: 30
Console.WriteLine(calculator.Add(5.5, 10.5)); // Calls Add(double, double) -> Output: 16
}
}
Use code with caution.
Operator overloading
Operator overloading allows you to define how an operator, such as + or *, should behave for user-defined types (like classes or structs). The compiler resolves the correct operator implementation based on the types of the operands at compile time.
**Example:**In this example, the + operator is overloaded to add two Vector2 objects.
public class Vector2
{
public int X { get; set; }
public int Y { get; set; }
public Vector2(int x, int y)
{
X = x;
Y = y;
}
// Overloading the + operator
public static Vector2 operator +(Vector2 a, Vector2 b)
{
return new Vector2(a.X + b.X, a.Y + b.Y);
}
}
public class Program
{
public static void Main()
{
Vector2 v1 = new Vector2(2, 3);
Vector2 v2 = new Vector2(4, 5);
Vector2 v3 = v1 + v2; // Calls the overloaded + operator
Console.WriteLine($"New Vector: ({v3.X}, {v3.Y})"); // Output: New Vector: (6, 8)
}
}
Use code with caution.
Runtime polymorphism (Dynamic polymorphism)
Runtime polymorphism is also known as dynamic polymorphism or late binding, because the decision about which method to call is made at runtime, not at compile time. This is achieved through method overriding and relies on inheritance, virtual members, and interfaces.
Method overriding
Method overriding allows a derived class to provide its own specific implementation of a method that is already defined in its base class. For this to work, the base class method must be marked with the virtual keyword, and the derived class method must be marked with the override keyword.
The key to runtime polymorphism is the ability to use a base class reference to refer to an object of a derived class. When the method is called on the base class reference, the Common Language Runtime (CLR) invokes the correct overridden method based on the actual object's type at runtime.
**Example:**Here, we create a base class Animal with a virtual method MakeSound(). The derived classes, Dog and Cat, override this method to provide their unique sounds.
using System;
// Base class
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("The animal makes a generic sound.");
}
}
// Derived class
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("The dog barks.");
}
}
// Derived class
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("The cat meows.");
}
}
public class Program
{
public static void Main()
{
// Use a base class reference for a derived class object
Animal myAnimal = new Dog();
myAnimal.MakeSound(); // At runtime, the Dog's MakeSound() is called. Output: The dog barks.
// Reassign the same reference to a different derived class object
myAnimal = new Cat();
myAnimal.MakeSound(); // At runtime, the Cat's MakeSound() is called. Output: The cat meows.
}
}
Use code with caution.
Abstract classes and interfaces
Runtime polymorphism is also achieved through abstract classes and interfaces.
- Abstract classes: An abstract class can have abstract methods (declared with the
abstractkeyword) that have no implementation. Any non-abstract class that inherits from an abstract class must provide an implementation for all abstract methods. - Interfaces: An interface defines a contract of methods, but provides no implementation. Any class that implements an interface must provide an implementation for all interface members.
In both cases, a variable of the abstract class or interface type can hold a reference to an object of a concrete class that implements it. This enables the same polymorphic behavior as with virtual methods.
**Example with an interface:**The ICharger interface defines a Charge() method. Both Smartphone and Laptop implement this interface, providing their own Charge() logic.
using System;
// Interface
public interface ICharger
{
void Charge();
}
// Class implementing the interface
public class Smartphone : ICharger
{
public void Charge()
{
Console.WriteLine("Charging the smartphone.");
}
}
// Another class implementing the interface
public class Laptop : ICharger
{
public void Charge()
{
Console.WriteLine("Charging the laptop.");
}
}
public class Program
{
public static void Main()
{
ICharger phone = new Smartphone();
ICharger laptop = new Laptop();
phone.Charge(); // Output: Charging the smartphone.
laptop.Charge(); // Output: Charging the laptop.
}
}
Use code with caution.
Key differences summarized
| Feature | Compile-time polymorphism | Runtime polymorphism |
|---|---|---|
| Binding | Static binding (Early binding). The decision of which method to call is made at compile time. | Dynamic binding (Late binding). The decision of which method to call is deferred until runtime. |
| Flexibility | Less flexible. Behavior is determined by the parameter types and number at compile time. | More flexible. The behavior is determined by the actual object type during program execution. |
| Performance | Generally faster execution because the compiler has already resolved the method call. | Slightly slower execution due to the runtime lookup of the correct method implementation. |
| Mechanism | Method overloading and operator overloading. | Method overriding using virtual and override keywords, abstract classes, and interfaces. |
| Involvement of Inheritance | Not directly dependent on inheritance. Occurs within a single class. | Heavily reliant on inheritance or interface implementation to provide a base for different object types. |