Skip to content

C# - Polymorphism

The word polymorphism means “many forms”. In object-oriented programming, it allows objects of different classes to be treated as objects of a common superclass. It’s often expressed as “one interface, multiple implementations.”

Polymorphism enables you to write more flexible, decoupled, and maintainable code. There are two main types in C#:

  • Static Polymorphism (Compile-time): The decision of which method to call is made at compile time. This is achieved through method overloading and operator overloading.
  • Dynamic Polymorphism (Run-time): The decision of which method to call is deferred until run time. This is achieved through method overriding using virtual, override, and abstract keywords, often in combination with inheritance and interfaces.

Also known as early binding, static polymorphism is resolved by the compiler.

Method overloading allows you to define multiple methods with the same name in the same class, as long as their parameter lists are different. The difference can be in the number of parameters or the type of parameters. The return type alone cannot be used to overload a method.

public class Logger
{
// Overload 1: Takes a string
public void Log(string message)
{
Console.WriteLine($"LOG: {message}");
}
// Overload 2: Takes a string and an exception
public void Log(string message, Exception ex)
{
Console.WriteLine($"ERROR: {message} | Exception: {ex.Message}");
}
// Overload 3: Takes an integer
public void Log(int code)
{
Console.WriteLine($"Status Code: {code}");
}
}
// Usage
var logger = new Logger();
logger.Log("User logged in.");
logger.Log(404);
logger.Log("Failed to load data.", new InvalidOperationException());
// Output:
// LOG: User logged in.
// Status Code: 404
// ERROR: Failed to load data. | Exception: Operation is not valid due to the current state of the object.

C# allows you to redefine the behavior of operators (like +, -, *, /, ==, !=) for your custom types. This is done by declaring a static method with the operator keyword.

Here, we define a Vector type and overload the + operator to perform vector addition. We use a record struct for a concise, immutable value type.

// A record struct is great for simple, immutable data structures
public record struct Vector(int X, int Y);
{
// Overload the + operator for two Vector objects
public static Vector operator +(Vector v1, Vector v2)
{
return new Vector(v1.X + v2.X, v1.Y + v2.Y);
}
}
// Usage
var vector1 = new Vector(2, 3);
var vector2 = new Vector(5, 1);
// The overloaded + operator is called here
Vector resultVector = vector1 + vector2;
Console.WriteLine($"Vector 1: {vector1}");
Console.WriteLine($"Vector 2: {vector2}");
Console.WriteLine($"Resultant Vector: {resultVector}");
// Output:
// Vector 1: Vector { X = 2, Y = 3 }
// Vector 2: Vector { X = 5, Y = 1 }
// Resultant Vector: Vector { X = 7, Y = 4 }

Also known as late binding, dynamic polymorphism is resolved at runtime and is a cornerstone of flexible and extensible object-oriented design.

Method Overriding with virtual and override

Section titled “Method Overriding with virtual and override”

A base class can declare a method as virtual, allowing derived classes to provide their own specific implementation using the override keyword. The decision on which method to execute is based on the object’s actual type at runtime.

// Base class with a virtual method
public class Shape
{
public virtual void Draw()
{
Console.WriteLine("Drawing a generic shape.");
}
}
// Derived class overriding the method
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a circle.");
}
}
// Another derived class
public class Square : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a square.");
}
}
// Demonstrate polymorphism
var shapes = new List<Shape>
{
new Shape(),
new Circle(),
new Square()
};
foreach (var shape in shapes)
{
shape.Draw(); // The correct Draw() method is called at runtime
}
// Output:
// Drawing a generic shape.
// Drawing a circle.
// Drawing a square.

An abstract class cannot be instantiated and serves as a base for other classes. It can contain abstract members (methods, properties) which have no implementation and must be overridden by any non-abstract derived class.

public abstract class Animal
{
// An abstract property - derived classes must implement it
public abstract string Sound { get; }
// A regular method that uses the abstract property
public void Speak()
{
Console.WriteLine($"The animal says: {Sound}");
}
}
public class Dog : Animal
{
// Implementing the abstract property
public override string Sound => "Woof!";
}
public class Cat : Animal
{
// Implementing the abstract property
public override string Sound => "Meow!";
}
// Usage
Animal myDog = new Dog();
Animal myCat = new Cat();
myDog.Speak();
myCat.Speak();
// Output:
// The animal says: Woof!
// The animal says: Meow!

An interface defines a contract of public members that a class or struct can implement. It’s a powerful way to achieve polymorphism without class inheritance, enabling a form of multiple inheritance.

// Interface defining a contract for things that can be sent
public interface ISendable
{
void Send();
}
// Email class implements the interface
public class Email : ISendable
{
public void Send() => Console.WriteLine("Sending an email...");
}
// SMS class also implements the interface
public class Sms : ISendable
{
public void Send() => Console.WriteLine("Sending an SMS...");
}
// This method works with ANY object that implements ISendable
public void SendNotification(ISendable message)
{
message.Send();
}
// Usage
SendNotification(new Email());
SendNotification(new Sms());
// Output:
// Sending an email...
// Sending an SMS...

Abstract Class vs. Interface: When to Use What?

Section titled “Abstract Class vs. Interface: When to Use What?”

A common question is when to use an abstract class versus an interface. Here’s a quick guide:

  • Use an Abstract Class when:
    • You want to share code (method implementations, fields) among several closely related classes.
    • You expect that classes that extend your abstract class have a common base, an “is-a” relationship (e.g., Dog is an Animal).
    • You need to provide protected or internal members.
  • Use an Interface when:
    • You expect that unrelated classes will implement the interface. It defines a capability, a “can-do” relationship (e.g., a Bird and a Plane can both Fly).
    • You want to specify the behavior of a data type, but are not concerned about who implements its behavior.
    • You want to take advantage of multiple inheritance of type.