Introduction: What Is Polymorphism?
Object-oriented programming relies on three core principles: encapsulation, inheritance, and polymorphism. Among these, polymorphism refers to the ability of a method or function to exhibit different behaviors based on different implementations. The term originates from the Greek words “polys”, meaning “many,” and “morphe”, meaning “form.”
Polymorphism enables us to write code that behaves differently depending on the data type being used. Typically, when people think of polymorphism, they associate it with different implementations of abstract or virtual instance methods inherited from a base class or interface. These behaviors, determined at runtime, decide which implementation to execute based on the type in use. This is called runtime polymorphism or dynamic polymorphism. Because the decision is made during the program’s execution, it is also referred to as late binding.
However, polymorphism isn’t limited to runtime. There’s another form known as compile-time polymorphism.
Compile Time And Static Polymorphism
Compile-time polymorphism, also known as early binding, resolves which code to execute at compile time rather than at runtime. By leveraging mechanisms built into the compiler, compile-time polymorphism avoids the overhead of determining behavior during program execution.
Sometimes, compile-time polymorphism is referred to as static polymorphism. While static polymorphism is a subset of compile-time polymorphism—where the behavior is tied to types rather than instances—not all compile-time polymorphism is strictly static.
Below, we will see the two different ways with some examples we can have compile-time and static polymorphism in C#.
Method Overloading
The most common form of compile-time polymorphism, and one you’ve likely used without realizing it, is method overloading. When you define multiple methods with the same name but different parameters, the compiler determines at compile time which method to invoke. This eliminates any additional overhead at runtime. For example:
public class Foo
{
public void Bar(int parameter)
=> Console.WriteLine($"Bar with an int parameter with value: {parameter}");
public void Bar(string parameter)
=> Console.WriteLine($"Bar with a string parameter with value: {parameter}");
public void Bar(string parameter, int parameter2)
=> Console.WriteLine($"Bar with a string and an int parameters with values: {parameter} and {parameter2}");
}
Foo foo = new();
foo.Bar(42);
foo.Bar("forty two");
foo.Bar("forty two", 42);
result:
Bar with an int parameter with value: 42
Bar with a string parameter with value: forty two
Bar with a string and an int parameters with values: forty two and 42
Parameterization And Static Polymorphism
Another way to achieve compile-time polymorphism is through parameterization. Among the three primary methods for creating dependencies between classes —inheritance, composition, and parameterization— parameterization, implemented in C# using generics, is often overlooked.
Generics offer the advantage of being resolved at compile time when used with static members, making them an excellent tool for implementing static polymorphism. Starting with C# 11, developers can declare static virtual and abstract members in interfaces. This enables static polymorphism because static members are tied to the type itself rather than to its instances.
Here’s an example:
Let’s suppose that we have a generic interface that creates enemies through a static method:
public interface ICreateEnemy<out T> where T: ICreateEnemy<T>
{
static abstract T GetEnemy();
}
and two classes that implement it:
public class Goblin(int level) : ICreateEnemy<Goblin>
{
public int Level { get; } = level;
public static Goblin GetEnemy()
{
Random random = new Random();
return new Goblin(random.Next(1, 100));
}
}
public class Skeleton(int level) : ICreateEnemy<Skeleton>
{
public int Level { get; } = level;
public static Skeleton GetEnemy()
{
Random random = new Random();
return new Skeleton(random.Next(1, 100));
}
}
We can create an EnemyFactory
class that will call this GetEnemy
method polymorphically through one of its own methods, let’s call it RandomEnemyLevel
:
public class EnemyFactory
{
public static T RandomEnemyLevel<T>(T _) where T: ICreateEnemy<T>
{
return T.GetEnemy();
}
}
Now we can use it like this:
var enemy1 = EnemyFactory.RandomEnemyLevel(Goblin.GetEnemy());
var enemy2 = EnemyFactory.RandomEnemyLevel(Skeleton.GetEnemy());
Console.WriteLine($"I am a {enemy1} with level {enemy1.Level}");
Console.WriteLine($"I am a {enemy2} with level {enemy2.Level}");
Here the RandomEnemyLevel
method uses polymorphism by accepting any ICreateEnemy<T>
parameter, however this is resolved at compile-time. If this pattern seems familiar, or the constrain public interface ICreateEnemy<out T> where T: ICreateEnemy<T>
appears unusual, it is because it utilizes the Curiously Recurring Template Pattern (CRTP). You can read more about this pattern in my post: Curiously Recurring Template Pattern in C#
Conclusion
These are the two primary ways to achieve compile-time polymorphism in C#: method overloading and static abstract members. While C++ supports additional compile-time polymorphism through templates, in C#, generics that rely on instance members are resolved at runtime. This can be verified by inspecting the generated IL code, where generic instance members use the callvirt
operation.
As always, thank you for reading, and if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.