Easy Learn C#

Polymorphism in C#

Introduction to Polymorphism

Polymorphism is one of the core principles of object-oriented programming. The word "polymorphism" means "many forms," and in programming, it refers to the ability of an object to take on many forms or be treated as different types.

In C#, polymorphism allows you to:

  • Use a single interface to represent different underlying forms (data types)
  • Process objects differently depending on their data type or class
  • Treat a derived class as an object of its base class
  • Implement the same method in multiple ways

Types of Polymorphism in C#

C# supports two main types of polymorphism:

1. Compile-time Polymorphism (Static Binding)

Also known as method overloading, it allows multiple methods with the same name but different parameters in the same class.


public class Calculator
{
    // Method with two integer parameters
    public int Add(int a, int b)
    {
        return a + b;
    }
    
    // Method with three integer parameters
    public int Add(int a, int b, int c)
    {
        return a + b + c;
    }
    
    // Method with two double parameters
    public double Add(double a, double b)
    {
        return a + b;
    }
    
    // Method with an array of integers
    public int Add(params int[] numbers)
    {
        int sum = 0;
        foreach (int number in numbers)
        {
            sum += number;
        }
        return sum;
    }
}

// Usage
Calculator calc = new Calculator();
Console.WriteLine(calc.Add(5, 10));           // Calls Add(int, int)
Console.WriteLine(calc.Add(5, 10, 15));       // Calls Add(int, int, int)
Console.WriteLine(calc.Add(5.5, 10.5));       // Calls Add(double, double)
Console.WriteLine(calc.Add(5, 10, 15, 20));   // Calls Add(params int[])

Characteristics of method overloading:

  • Methods must have the same name but different parameter lists
  • Parameter lists can differ by:
    • Number of parameters
    • Type of parameters
    • Order of parameters
  • Return type alone is not enough to distinguish overloaded methods
  • Resolved at compile time, not at runtime

2. Runtime Polymorphism (Dynamic Binding)

Also known as method overriding, it allows a derived class to provide a specific implementation of a method that is already defined in its base class.


public class Animal
{
    public string Name { get; set; }
    
    public Animal(string name)
    {
        Name = name;
    }
    
    // Virtual method that can be overridden
    public virtual void MakeSound()
    {
        Console.WriteLine("The animal makes a sound");
    }
    
    // Virtual method to get information
    public virtual string GetInfo()
    {
        return $"This is {Name}, an animal";
    }
}

public class Dog : Animal
{
    public string Breed { get; set; }
    
    public Dog(string name, string breed) : base(name)
    {
        Breed = breed;
    }
    
    // Override the MakeSound method
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} barks: Woof! Woof!");
    }
    
    // Override the GetInfo method
    public override string GetInfo()
    {
        return $"This is {Name}, a {Breed} dog";
    }
}

public class Cat : Animal
{
    public bool IsIndoor { get; set; }
    
    public Cat(string name, bool isIndoor) : base(name)
    {
        IsIndoor = isIndoor;
    }
    
    // Override the MakeSound method
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} meows: Meow! Meow!");
    }
    
    // Override the GetInfo method
    public override string GetInfo()
    {
        string type = IsIndoor ? "an indoor" : "an outdoor";
        return $"This is {Name}, {type} cat";
    }
}

// Usage
Animal[] animals = 
{
    new Animal("Generic Animal"),
    new Dog("Rex", "German Shepherd"),
    new Cat("Whiskers", true)
};

// Polymorphic behavior - the correct method is called based on the actual type
foreach (Animal animal in animals)
{
    Console.WriteLine(animal.GetInfo());
    animal.MakeSound();
    Console.WriteLine();
}

/* Output:
This is Generic Animal, an animal
The animal makes a sound

This is Rex, a German Shepherd dog
Rex barks: Woof! Woof!

This is Whiskers, an indoor cat
Whiskers meows: Meow! Meow!
*/

Key points about method overriding:

  • The method in the base class must be marked with the virtual keyword
  • The method in the derived class must use the override keyword
  • Both methods must have the same name, return type, and parameter list
  • Resolved at runtime, not at compile time
  • Enables objects to be treated as instances of their base class, but still use their own methods

Polymorphism Through Interfaces

One of the most powerful forms of polymorphism in C# is achieved through interfaces. An interface defines a contract that classes must implement, allowing objects of different classes to be treated uniformly.

Implementing Interfaces for Polymorphic Behavior


// Interface definition
public interface IShape
{
    double CalculateArea();
    double CalculatePerimeter();
    void Draw();
}

// Circle class implementing IShape
public class Circle : IShape
{
    public double Radius { get; set; }
    
    public Circle(double radius)
    {
        Radius = radius;
    }
    
    public double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
    
    public double CalculatePerimeter()
    {
        return 2 * Math.PI * Radius;
    }
    
    public void Draw()
    {
        Console.WriteLine($"Drawing a circle with radius {Radius}");
    }
}

// Rectangle class implementing IShape
public class Rectangle : IShape
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }
    
    public double CalculateArea()
    {
        return Width * Height;
    }
    
    public double CalculatePerimeter()
    {
        return 2 * (Width + Height);
    }
    
    public void Draw()
    {
        Console.WriteLine($"Drawing a rectangle with width {Width} and height {Height}");
    }
}

// Triangle class implementing IShape
public class Triangle : IShape
{
    public double SideA { get; set; }
    public double SideB { get; set; }
    public double SideC { get; set; }
    
    public Triangle(double a, double b, double c)
    {
        SideA = a;
        SideB = b;
        SideC = c;
    }
    
    public double CalculateArea()
    {
        // Heron's formula
        double s = (SideA + SideB + SideC) / 2;
        return Math.Sqrt(s * (s - SideA) * (s - SideB) * (s - SideC));
    }
    
    public double CalculatePerimeter()
    {
        return SideA + SideB + SideC;
    }
    
    public void Draw()
    {
        Console.WriteLine($"Drawing a triangle with sides {SideA}, {SideB}, and {SideC}");
    }
}

// Usage with polymorphism
public class ShapeProcessor
{
    public void ProcessShape(IShape shape)
    {
        Console.WriteLine("Processing shape...");
        shape.Draw();
        Console.WriteLine($"Area: {shape.CalculateArea():F2}");
        Console.WriteLine($"Perimeter: {shape.CalculatePerimeter():F2}");
        Console.WriteLine();
    }
}

// Client code
public void Main()
{
    ShapeProcessor processor = new ShapeProcessor();
    
    // Process different shapes polymorphically
    processor.ProcessShape(new Circle(5));
    processor.ProcessShape(new Rectangle(4, 6));
    processor.ProcessShape(new Triangle(3, 4, 5));
    
    // Create a collection of different shapes
    List shapes = new List
    {
        new Circle(3),
        new Rectangle(2, 7),
        new Triangle(5, 7, 9)
    };
    
    // Process all shapes in the collection
    foreach (IShape shape in shapes)
    {
        processor.ProcessShape(shape);
    }
}

Advantages of interface-based polymorphism:

  • Classes can implement multiple interfaces (unlike inheritance, which is limited to one base class)
  • Provides a clean way to define behaviors that can be implemented by unrelated classes
  • Decouples the interface from the implementation
  • Makes it easy to add new classes that implement the same interface
  • Allows for more flexible design than inheritance alone

Type Casting and Polymorphism

When working with polymorphism, you often need to cast objects from one type to another. C# provides several ways to do this.

Upcasting and Downcasting


public class Animal
{
    public virtual void Eat()
    {
        Console.WriteLine("Animal is eating");
    }
}

public class Dog : Animal
{
    public override void Eat()
    {
        Console.WriteLine("Dog is eating");
    }
    
    public void Bark()
    {
        Console.WriteLine("Dog is barking");
    }
}

public class Cat : Animal
{
    public override void Eat()
    {
        Console.WriteLine("Cat is eating");
    }
    
    public void Meow()
    {
        Console.WriteLine("Cat is meowing");
    }
}

// Usage
public void Main()
{
    // Upcasting (implicit) - Converting a derived type to a base type
    Dog dog = new Dog();
    Animal animal1 = dog;  // Implicit upcasting
    
    animal1.Eat();  // Output: "Dog is eating" (polymorphic behavior)
    // animal1.Bark();  // Error: Animal doesn't have Bark method
    
    // Downcasting (explicit) - Converting a base type to a derived type
    Animal animal2 = new Cat();
    
    // Need explicit cast for downcasting
    if (animal2 is Cat)
    {
        Cat cat = (Cat)animal2;  // Explicit cast
        cat.Meow();  // Now we can call Cat-specific methods
    }
    
    // Using as operator for safe casting
    Dog dogObj = animal1 as Dog;
    if (dogObj != null)
    {
        dogObj.Bark();  // Safe to call Dog-specific method
    }
    
    Cat catObj = animal1 as Cat;
    if (catObj != null)
    {
        catObj.Meow();  // This won't execute since animal1 is a Dog
    }
    else
    {
        Console.WriteLine("The animal is not a cat");
    }
    
    // Pattern matching (C# 7.0+)
    if (animal1 is Dog d)
    {
        d.Bark();  // Using the pattern variable
    }
    
    // Switch expression with pattern matching (C# 8.0+)
    string message = animal2 switch
    {
        Dog d => $"This is a dog that can {(d.GetType().GetMethod("Bark") != null ? "bark" : "not bark")}",
        Cat c => $"This is a cat that can {(c.GetType().GetMethod("Meow") != null ? "meow" : "not meow")}",
        _ => "This is some other animal"
    };
    
    Console.WriteLine(message);
}

Key concepts for type casting in polymorphism:

  • Upcasting: Converting a derived class to a base class (always safe, implicit)
  • Downcasting: Converting a base class to a derived class (requires explicit cast, may throw InvalidCastException)
  • is operator: Tests if an object is compatible with a given type
  • as operator: Performs a safe cast that returns null if the cast fails
  • Pattern matching: A modern C# feature that combines type checking and casting

Abstract Classes vs. Interfaces

Both abstract classes and interfaces are used to achieve polymorphism in C#, but they have different purposes and capabilities.

Comparison Table

Feature Abstract Class Interface
Instance creation Cannot be instantiated Cannot be instantiated
Implementation Can have method implementations Cannot have method implementations (prior to C# 8.0)
Fields Can have fields Can only have constants
Multiple inheritance A class can inherit only one abstract class A class can implement multiple interfaces
Access modifiers Can have access modifiers All members are implicitly public
Constructor Can have constructors Cannot have constructors

When to Use Each

Use Abstract Classes when:

  • You want to share code among related classes
  • The classes that will inherit from the abstract class have common behaviors or attributes
  • You want to provide default implementations that subclasses can override if needed
  • You need to declare non-public members
  • You want to take advantage of constructor injection

Use Interfaces when:

  • You want to define a contract that unrelated classes can implement
  • You need multiple inheritance
  • You want to separate what a class does from how it does it
  • You need to use polymorphism across different inheritance hierarchies
  • You want to support component-based architecture

Advanced Polymorphism Techniques

Operator Overloading

C# allows you to define custom behavior for standard operators when applied to your classes.


public class Complex
{
    public double Real { get; set; }
    public double Imaginary { get; set; }
    
    public Complex(double real, double imaginary)
    {
        Real = real;
        Imaginary = imaginary;
    }
    
    // Overload the + operator
    public static Complex operator +(Complex a, Complex b)
    {
        return new Complex(a.Real + b.Real, a.Imaginary + b.Imaginary);
    }
    
    // Overload the - operator
    public static Complex operator -(Complex a, Complex b)
    {
        return new Complex(a.Real - b.Real, a.Imaginary - b.Imaginary);
    }
    
    // Overload the * operator
    public static Complex operator *(Complex a, Complex b)
    {
        return new Complex(
            a.Real * b.Real - a.Imaginary * b.Imaginary,
            a.Real * b.Imaginary + a.Imaginary * b.Real);
    }
    
    // Overload the == operator
    public static bool operator ==(Complex a, Complex b)
    {
        // Need to handle null cases
        if (ReferenceEquals(a, null))
            return ReferenceEquals(b, null);
            
        if (ReferenceEquals(b, null))
            return false;
            
        return a.Real == b.Real && a.Imaginary == b.Imaginary;
    }
    
    // When you overload ==, you should also overload !=
    public static bool operator !=(Complex a, Complex b)
    {
        return !(a == b);
    }
    
    // Override Equals and GetHashCode for consistency
    public override bool Equals(object obj)
    {
        if (obj is Complex complex)
            return this == complex;
        return false;
    }
    
    public override int GetHashCode()
    {
        return Real.GetHashCode() ^ (Imaginary.GetHashCode() * 17);
    }
    
    // Override ToString for better representation
    public override string ToString()
    {
        if (Imaginary >= 0)
            return $"{Real} + {Imaginary}i";
        else
            return $"{Real} - {Math.Abs(Imaginary)}i";
    }
}

// Usage
public void Main()
{
    Complex a = new Complex(3, 4);
    Complex b = new Complex(1, 2);
    
    Complex sum = a + b;
    Complex difference = a - b;
    Complex product = a * b;
    
    Console.WriteLine($"a = {a}");
    Console.WriteLine($"b = {b}");
    Console.WriteLine($"a + b = {sum}");
    Console.WriteLine($"a - b = {difference}");
    Console.WriteLine($"a * b = {product}");
    
    Console.WriteLine($"a == b: {a == b}");
    Console.WriteLine($"a != b: {a != b}");
}

Covariance and Contravariance

C# supports covariance and contravariance in generics and delegates, which are advanced forms of polymorphism.


// Covariance example (out parameter)
// IEnumerable is covariant (notice the out keyword)
public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

// Contravariance example (in parameter)
// IComparer is contravariant (notice the in keyword)
public interface IComparer
{
    int Compare(T x, T y);
}

// Example with delegates
class Program
{
    // Base class
    public class Animal { }
    
    // Derived classes
    public class Dog : Animal { }
    public class Cat : Animal { }
    
    // Covariant delegate
    public delegate T Factory();
    
    // Contravariant delegate
    public delegate void Action(T obj);
    
    static void Main()
    {
        // Covariance in delegates
        Factory dogFactory = () => new Dog();
        Factory animalFactory = dogFactory;  // This works because of covariance
        
        // Contravariance in delegates
        Action feedAnimal = (animal) => Console.WriteLine("Feeding animal");
        Action feedDog = feedAnimal;  // This works because of contravariance
        
        // Covariance in collections
        List dogs = new List { new Dog(), new Dog() };
        IEnumerable animals = dogs;  // This works because IEnumerable is covariant
        
        foreach (Animal animal in animals)
        {
            Console.WriteLine("Found an animal");
        }
    }
}

Understanding covariance and contravariance:

  • Covariance (out): Allows you to use a more derived type than originally specified
  • Contravariance (in): Allows you to use a more base type than originally specified
  • These concepts enable greater flexibility when working with generic interfaces and delegates
  • They are particularly useful in collections, LINQ, and event handlers

Polymorphism Best Practices

Guidelines for Effective Polymorphism

  • Follow the Liskov Substitution Principle - Objects of derived classes should be able to replace objects of their base class without affecting program correctness
  • Design for extension - Make classes open for extension but closed for modification (Open/Closed Principle)
  • Use interfaces for cross-cutting concerns - Interfaces are better for behaviors that cut across different inheritance hierarchies
  • Keep interfaces focused - Design small, cohesive interfaces rather than large, monolithic ones
  • Consider composition over inheritance - Sometimes composition provides better flexibility than inheritance
  • Use abstract methods only when necessary - If a base implementation makes sense, use virtual methods instead
  • Document expected overriding behavior - Make it clear how derived classes should implement or override methods
  • Test polymorphic behavior - Ensure each implementation behaves correctly in the context of the base type

Common Polymorphism Mistakes to Avoid

  • Breaking the contract - Overridden methods should honor the contract defined by the base method
  • Method hiding instead of overriding - Using new instead of override can lead to unexpected behavior
  • Excessive casting - Frequent need for casting may indicate a design flaw
  • Not validating type before casting - Always check types before downcasting to avoid exceptions
  • Overusing polymorphism - Sometimes simpler designs without polymorphism are clearer
  • Ignoring base implementation - Consider whether to call the base implementation in overridden methods
  • Deep inheritance hierarchies - More than 2-3 levels can become difficult to understand and maintain