Polymorphism is one of the four characteristic properties which are core to any object oriented programming language. The polymorphism is a phenomenon observed among base and derived types where in a base type can be used to invoke a method of its derived type, which is in contrast to the behavior of Inheritance where a derived type accesses its base type methods.
Polymorphism can be summarized as
- opposite of inheritance
- one object manipulated into many forms
However, when it comes to deciding which method needs to be invoked for a calling reference type, there are two kinds of behaviors which can be observed. And hence, there are two kinds of polymorphic behaviors which can be noticed in object oriented programming structures.
They are:
- Compile-time Polymorphism
- Runtime Polymorphism
To understand these behaviors, let’s take the example of a base Animal type which exposes a virtual method Sound(). And there are two derived types Cat and Dog classes which override this behavior with their own implementations of the Sound() method. The structure looks as below:
namespace WorkConsoleApp
{
public class Animal
{
public virtual void Sound()
{
Console.WriteLine("Animal makes Sound - ...");
}
}
public class Cat : Animal
{
public override void Sound()
{
Console.WriteLine("Cat makes Sound - Meow");
}
}
public class Dog : Animal
{
public override void Sound()
{
Console.WriteLine("Dog makes Sound - Bow");
}
}
}
And let’s assume we invoke these methods in the following fashion in our Main() method:
namespace WorkConsoleApp
{
public class NewProgram
{
public static void Main(string[] args)
{
// reference of Base Animal
Animal animal;
// Derived type Cat
// assigned to
// Base type Animal
animal = new Cat();
animal.Sound();
// Derived type Dog
// assigned to
// Base type Animal
animal = new Dog();
animal.Sound();
}
}
}
Here we declare a variable animal which is of type Animal, and we assign it sequentially to each of its derived types namely Cat and Dog. Now when we call the method animal.Sound() over the type Animal, the variable still being of type Animal calls its derived class methods. When we look at how things work here; the compiler doesn’t know what is stored for the variable animal. During runtime, the animal variable is dynamically assigned off values new Cat() and new Dog() which represent the object instances of the types Cat() and Dog() respectively. Later when the line of code animal.Sound() is executed, the runtime makes a decision for what needs to be called for animal.Sound(). Originally, one can expect the base class method Animal.Sound() be called, but the runtime decides to call its derived type Cat.Sound() and Dog.Sound() to be called henceforth.
Now because the decision is delayed till runtime and is made only during runtime, this we call as runtime polymorphism.
Under Runtime Polymorphism:
- Decision of invocation happens at runtime
- Occurs for an Abstract or Concrete base with a virtual method and its overriding derived types
- Occurs under the context of inheritance and method overriding
- Is also known as Lazy binding or Late Binding
- Is somewhat slow and might be prone to runtime errors
The output is printed as:
Cat makes Sound - Meow
Dog makes Sound - Bow
Alternatively, consider another class Beast which has two methods of same name Sound() and is defined as below:
namespace WorkConsoleApp
{
public class Beast
{
public void Sound()
{
Console.WriteLine("Beast makes a sound - grrrr");
}
public void Sound(string sound)
{
Console.WriteLine($"Beast makes a sound of passed string {sound}");
}
public void Sound(object sound)
{
Console.WriteLine($"Beast makes a sound of passed object {sound}");
}
}
}
And we have the Main() method in which we create an object of type Beast and call the Sound() method with various parameters as below:
namespace WorkConsoleApp
{
public class NewProgram
{
public static void Main(string[] args)
{
Beast b = new Beast();
// call the default
// Sound() method
b.Sound();
// call Sound()
// with a string param
b.Sound("PowPow");
// call Sound()
// with an int param
b.Sound(12345);
}
}
}
Notice that we have called all the three variations of the method Sound() with different parameter types. We have passed a string parameter "PowPow" and an integer 12345 to the method. Now the decision to call which variation of the method Sound() is taken up by the compiler while linking, and maps the references to the respective methods depending on the parameter type. Now since all this decision making happens even before the code is executed, we call this behavior as compile-time polymorphism.
Under Compile-time Polymorphism:
- Decision of invocation is taken and frozen during compilation
- Occurs for a variants of a method under a class definition and when that method is invoked with various parameters
- Occurs under the context of method or property overloading and when the derived type methods are invoked with their own instances
- Is also known as Early binding or Static Binding
- Is faster than what Late binding does but is not flexible
When we run this code, we get the below output:
Beast makes a sound - grrrr
Beast makes a sound of passed string PowPow
Beast makes a sound of passed object 12345
Observe that for the method invocation b.Sound("PowPow") calls the method overload with a string type parameter, which is in this case the best match for the passed input type, while for the second call b.Sound(12345) the method overload with object type parameter is invoked, since an integer is-an object and the best available match for the integer here is that of its base type object.