Polymorphism in C#

Polymorphism in C# is an object oriented programming concept where the same operation can behave differently depending on the object or method signature being used. The word polymorphism means many forms. In programming, it helps one interface, method name, or reference type work with different implementations.

Polymorphism makes C# programs flexible. Instead of writing separate logic for every specific class, code can work with a common base type or interface. At runtime, the correct derived behavior can run automatically. This is heavily used in application architecture, frameworks, dependency injection, testing, UI systems, game objects, and business logic.

In C#, polymorphism usually appears in two main forms: compile-time polymorphism and runtime polymorphism. Compile-time polymorphism is commonly achieved through method overloading. Runtime polymorphism is commonly achieved through inheritance, virtual methods, overriding, abstract classes, and interfaces.


What Is Polymorphism in C#?

Polymorphism allows the same name, call, or reference to represent different behavior. A simple example is a base class named Shape with different derived classes such as Circle, Rectangle, and Triangle. Each shape can draw itself differently.

class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("Drawing shape");
    }
}

class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing circle");
    }
}

class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing rectangle");
    }
}

The method name is the same, but the behavior changes based on the actual object type.

Types of Polymorphism in C#

TypeAlso Known AsCommon Technique
Compile-time polymorphismStatic polymorphismMethod overloading, operator overloading
Runtime polymorphismDynamic polymorphismMethod overriding, virtual methods, interfaces

Compile-time polymorphism is resolved by the compiler before the program runs. Runtime polymorphism is resolved while the program is running, based on the actual object type.

Compile-Time Polymorphism in C#

Compile-time polymorphism happens when multiple methods have the same name but different parameter lists. This is called method overloading.

class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }

    public double Add(double a, double b)
    {
        return a + b;
    }

    public int Add(int a, int b, int c)
    {
        return a + b + c;
    }
}

The method name Add is the same, but the parameter types or parameter count are different. The compiler decides which method to call based on the arguments passed by the caller.

Runtime Polymorphism in C#

Runtime polymorphism happens when a base class reference points to a derived class object, and an overridden method runs based on the actual object type.

Shape shape1 = new Circle();
Shape shape2 = new Rectangle();

shape1.Draw();
shape2.Draw();

Even though both variables are of type Shape, the first call runs Circle.Draw and the second call runs Rectangle.Draw. This is runtime polymorphism.

virtual and override Keywords

For runtime polymorphism with classes, the base class method must usually be marked as virtual. The derived class uses override to provide a new implementation.

class Notification
{
    public virtual void Send()
    {
        Console.WriteLine("Sending notification");
    }
}

class EmailNotification : Notification
{
    public override void Send()
    {
        Console.WriteLine("Sending email notification");
    }
}

class SmsNotification : Notification
{
    public override void Send()
    {
        Console.WriteLine("Sending SMS notification");
    }
}

The base class defines the common operation. Each derived class decides how that operation should work for its own type.

Polymorphism with Base Class Reference

A powerful part of polymorphism is that you can store different derived objects in variables, arrays, or lists of the base class type.

List<Notification> notifications = new List<Notification>
{
    new EmailNotification(),
    new SmsNotification()
};

foreach (Notification notification in notifications)
{
    notification.Send();
}

The loop does not need to know the exact notification type. It only knows that every item is a Notification. The correct Send method runs automatically at runtime.

Polymorphism with Interfaces

Interfaces are another common way to achieve polymorphism. Different classes can implement the same interface, and code can work with the interface type instead of concrete classes.

interface IPaymentProcessor
{
    void ProcessPayment(decimal amount);
}

class CreditCardPayment : IPaymentProcessor
{
    public void ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Paid {amount} by credit card");
    }
}

class UpiPayment : IPaymentProcessor
{
    public void ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Paid {amount} by UPI");
    }
}
void Checkout(IPaymentProcessor processor, decimal amount)
{
    processor.ProcessPayment(amount);
}

Checkout(new CreditCardPayment(), 500);
Checkout(new UpiPayment(), 750);

The Checkout method does not care which payment class is used. It only depends on the interface. This is a clean and flexible design because new payment types can be added without changing checkout logic.

Polymorphism vs Method Overloading

Method overloading is sometimes called compile-time polymorphism, but it is different from runtime overriding. Overloading chooses a method based on parameters. Overriding chooses behavior based on the actual object type.

PointOverloadingOverriding
ResolvedCompile timeRuntime
Requires inheritanceNoYes
Method signatureDifferent parametersSame signature
KeywordsNo special keyword requiredvirtual and override
PurposeSame action with different inputsDifferent behavior for derived types

Real World Example of Polymorphism

Imagine a reporting system that exports reports in PDF, Excel, and CSV formats. Each exporter has the same job: export a report. The implementation is different for each format.

interface IReportExporter
{
    void Export(string reportName);
}

class PdfExporter : IReportExporter
{
    public void Export(string reportName)
    {
        Console.WriteLine($"Exporting {reportName} as PDF");
    }
}

class ExcelExporter : IReportExporter
{
    public void Export(string reportName)
    {
        Console.WriteLine($"Exporting {reportName} as Excel");
    }
}
void ExportReport(IReportExporter exporter, string reportName)
{
    exporter.Export(reportName);
}

ExportReport(new PdfExporter(), "Sales Report");
ExportReport(new ExcelExporter(), "Sales Report");

The method accepts IReportExporter, so it works with any exporter implementation. This is practical polymorphism. It reduces duplicated code and makes the system easier to extend.

Why Polymorphism Is Useful

  • It allows code to work with general types instead of specific types.
  • It makes applications easier to extend with new implementations.
  • It supports clean architecture and dependency injection.
  • It reduces repeated conditional logic such as large switch statements.
  • It makes unit testing easier because fake implementations can be passed in.

Instead of asking, “which exact class is this?” polymorphic code asks, “can this object perform the required behavior?” That shift leads to cleaner and more flexible designs.

Common Mistakes with Polymorphism

  • Using inheritance when an interface would be cleaner.
  • Overusing virtual methods without a clear extension reason.
  • Breaking expected base class behavior in overridden methods.
  • Using type checks everywhere instead of polymorphic behavior.
  • Creating large base classes that force derived classes to inherit unwanted members.

If a method contains many if or switch statements checking object types, polymorphism may be a better design. Each type can own its own behavior instead of forcing one method to know every possible type.

Best Practices for Polymorphism in C#

  • Use polymorphism when different types share the same operation but implement it differently.
  • Prefer interfaces for capabilities that unrelated classes can share.
  • Use abstract classes when derived classes need shared state or shared base logic.
  • Keep base classes small and focused.
  • Do not make every method virtual by default.
  • Make overridden behavior respect the meaning promised by the base type.

Polymorphism and Dependency Injection

Modern C# applications use polymorphism heavily through dependency injection. A service may depend on an interface such as IEmailSender. At runtime, the application can provide a real email sender, a test email sender, or a mock sender without changing the service code.

class UserService
{
    private readonly IEmailSender emailSender;

    public UserService(IEmailSender emailSender)
    {
        this.emailSender = emailSender;
    }

    public void RegisterUser(string email)
    {
        emailSender.Send(email, "Welcome");
    }
}

The service does not create a specific email sender. It depends on behavior. This is one of the main reasons polymorphism is important in professional C# development.

Polymorphism Interview Points

For interviews, remember that polymorphism means one operation can have many forms. Method overloading is compile-time polymorphism. Method overriding is runtime polymorphism. Runtime polymorphism requires inheritance or interface-based dispatch, and the actual method is selected based on the runtime object type.

Also remember the roles of virtual, override, abstract, and interfaces. The base class defines a common contract, while derived classes or implementing classes provide specific behavior.

Polymorphism Design Rule

A good use of polymorphism removes unnecessary knowledge from the calling code. The caller should not need to know whether it is dealing with an email notification, SMS notification, PDF exporter, or Excel exporter. It should only depend on the common behavior that all implementations promise.

This keeps new features easier to add. If a new payment type, report format, or notification channel is introduced, you can create a new class that follows the same contract. Existing code that works with the base type or interface often does not need to change.

The design becomes weak when derived classes do not truly follow the same meaning. If one implementation behaves in a surprising way, the caller can no longer trust the common contract. Strong polymorphism depends on clear behavior, not only matching method names.

Polymorphism and Testing

Polymorphism also helps testing. If a class depends on an interface, tests can pass a fake implementation instead of a real database, payment gateway, file system, or email service. This makes tests faster, safer, and more focused because the class can be tested without triggering external systems.

That is why polymorphism appears so often in clean C# architecture: it separates what code needs from how that need is fulfilled.

Good contracts make that separation reliable across real projects, especially when teams add features without rewriting older code.

FAQs on Polymorphism in C#

What is polymorphism in C#?

Polymorphism in C# means the same method, operation, or reference type can represent different behavior depending on the object or method signature.

What are the types of polymorphism in C#?

The two common types are compile-time polymorphism and runtime polymorphism. Overloading is compile-time polymorphism, while overriding is runtime polymorphism.

What is runtime polymorphism?

Runtime polymorphism happens when the method that runs is selected based on the actual object type at runtime, usually through virtual and overridden methods.