C# - Polymorphism
C# - Polymorphism
Section titled “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, andabstractkeywords, often in combination with inheritance and interfaces.
1. Static Polymorphism
Section titled “1. Static Polymorphism”Also known as early binding, static polymorphism is resolved by the compiler.
Method Overloading
Section titled “Method Overloading”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}"); }}
// Usagevar 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.Operator Overloading
Section titled “Operator Overloading”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 structurespublic 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); }}
// Usagevar vector1 = new Vector(2, 3);var vector2 = new Vector(5, 1);
// The overloaded + operator is called hereVector 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 }2. Dynamic Polymorphism
Section titled “2. Dynamic Polymorphism”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 methodpublic class Shape{ public virtual void Draw() { Console.WriteLine("Drawing a generic shape."); }}
// Derived class overriding the methodpublic class Circle : Shape{ public override void Draw() { Console.WriteLine("Drawing a circle."); }}
// Another derived classpublic class Square : Shape{ public override void Draw() { Console.WriteLine("Drawing a square."); }}
// Demonstrate polymorphismvar 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.Abstract Classes and Methods
Section titled “Abstract Classes and Methods”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!";}
// UsageAnimal myDog = new Dog();Animal myCat = new Cat();
myDog.Speak();myCat.Speak();
// Output:// The animal says: Woof!// The animal says: Meow!Polymorphism with Interfaces
Section titled “Polymorphism with Interfaces”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 sentpublic interface ISendable{ void Send();}
// Email class implements the interfacepublic class Email : ISendable{ public void Send() => Console.WriteLine("Sending an email...");}
// SMS class also implements the interfacepublic class Sms : ISendable{ public void Send() => Console.WriteLine("Sending an SMS...");}
// This method works with ANY object that implements ISendablepublic void SendNotification(ISendable message){ message.Send();}
// UsageSendNotification(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.,
Dogis anAnimal). - You need to provide
protectedorinternalmembers.
- Use an Interface when:
- You expect that unrelated classes will implement the interface. It defines a capability, a “can-do” relationship (e.g., a
Birdand aPlanecan bothFly). - 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.
- You expect that unrelated classes will implement the interface. It defines a capability, a “can-do” relationship (e.g., a