ZetCode

C# polymorphism

last modified October 16, 2026

In this article we show how to use polymorphism in C#.

Polymorphism (from Greek, "many forms") is the ability of objects of different types to be used through a common interface, each responding in its own way. When we call a method on a base class reference, the method of the actual object's type is executed.

C# supports two kinds of polymorphism:

Run-time polymorphism builds on inheritance. It allows us to write code that works with base types and is not aware of the concrete derived types. New types can be added without changing such code.

C# virtual methods

A method declared with the virtual modifier can be overridden in derived classes with the override modifier.

Program.cs
Animal[] animals = [new Dog(), new Cat(), new Cow(), new Animal()];

foreach (var animal in animals)
{
    animal.MakeSound();
}

class Animal
{
    public virtual void MakeSound() => Console.WriteLine("...");
}

class Dog : Animal
{
    public override void MakeSound() => Console.WriteLine("Woof");
}

class Cat : Animal
{
    public override void MakeSound() => Console.WriteLine("Meow");
}

class Cow : Animal
{
    public override void MakeSound() => Console.WriteLine("Moo");
}

We have an array of animals. Each element is accessed through a variable of the Animal type.

foreach (var animal in animals)
{
    animal.MakeSound();
}

The loop calls the same method on every element; the runtime executes the implementation of the actual type of each object. The base implementation is used for the Animal object.

$ dotnet run
Woof
Meow
Moo
...

C# abstract classes

An abstract class cannot be instantiated. It can declare abstract members, which have no implementation and must be overridden in every non-abstract derived class.

Program.cs
Shape[] shapes = [new Circle(2), new Rectangle(3, 4), new Square(5)];

foreach (var shape in shapes)
{
    Console.WriteLine($"{shape.Name}: area {shape.Area():F2}");
}

double total = shapes.Sum(s => s.Area());
Console.WriteLine($"Total area: {total:F2}");

abstract class Shape
{
    public abstract double Area();

    public virtual string Name => GetType().Name;
}

class Circle(double radius) : Shape
{
    public override double Area() => Math.PI * radius * radius;
}

class Rectangle(double width, double height) : Shape
{
    public double Width => width;

    public override double Area() => width * height;
}

class Square(double side) : Rectangle(side, side)
{
    public override string Name => $"Square {Width}x{Width}";
}

The Shape class defines the abstract Area method. The derived classes are declared with primary constructors.

public abstract double Area();

Each concrete shape must provide its own implementation of the Area method.

double total = shapes.Sum(s => s.Area());

The code that computes the total area does not know anything about circles or rectangles; it works with the abstract Shape type only.

class Square(double side) : Rectangle(side, side)
{
    public override string Name => $"Square {Width}x{Width}";
}

The Square class inherits the Area method from Rectangle and overrides only the Name property. It uses the inherited Width property rather than its own side parameter, so that the value is not stored twice.

$ dotnet run
Circle: area 12.57
Rectangle: area 12.00
Square 5x5: area 25.00
Total area: 49.57

C# polymorphism with interfaces

Interfaces provide polymorphism among unrelated types. Any class or struct that implements an interface can be used where the interface is expected.

Program.cs
List<INotifier> notifiers =
[
    new EmailNotifier("admin@example.com"),
    new SmsNotifier("+421 900 123 456"),
    new ConsoleNotifier()
];

foreach (var notifier in notifiers)
{
    notifier.Notify("Server is down");
}

interface INotifier
{
    void Notify(string message);
}

class EmailNotifier(string address) : INotifier
{
    public void Notify(string message) =>
        Console.WriteLine($"Email to {address}: {message}");
}

class SmsNotifier(string phone) : INotifier
{
    public void Notify(string message) =>
        Console.WriteLine($"SMS to {phone}: {message}");
}

struct ConsoleNotifier : INotifier
{
    public void Notify(string message) =>
        Console.WriteLine($"Console: {message}");
}

Three types implement the INotifier interface; one of them is a struct. The calling code depends only on the interface.

$ dotnet run
Email to admin@example.com: Server is down
SMS to +421 900 123 456: Server is down
Console: Server is down

C# base type in method parameters

Polymorphism is most useful in method parameters. A method that accepts a base type works with all current and future derived types.

Program.cs
var payroll = new Payroll();

payroll.Pay(new Manager("Roger Roe", 4500m));
payroll.Pay(new Developer("John Doe", 3800m, 12));
payroll.Pay(new Intern("Lucy Smith"));

Console.WriteLine($"Total paid: {payroll.Total:N2}");

class Payroll
{
    public decimal Total { get; private set; }

    public void Pay(Employee e)
    {
        decimal salary = e.Salary();
        Total += salary;

        Console.WriteLine($"{e.Name,-12} {salary,10:N2}");
    }
}

abstract class Employee(string name)
{
    public string Name => name;
    public abstract decimal Salary();
}

class Manager(string name, decimal baseSalary) : Employee(name)
{
    public override decimal Salary() => baseSalary * 1.2m;
}

class Developer(string name, decimal baseSalary, int overtime) : Employee(name)
{
    public override decimal Salary() => baseSalary + overtime * 25m;
}

class Intern(string name) : Employee(name)
{
    public override decimal Salary() => 800m;
}

The Pay method takes an Employee parameter. Each type of employee computes its salary differently.

$ dotnet run
Roger Roe      5,400.00
John Doe       4,100.00
Lucy Smith       800.00
Total paid: 10,300.00

C# override vs new

Only overriding is polymorphic. If a derived class hides a base method with the new modifier, the method is chosen by the type of the variable at compile time.

Program.cs
Base obj = new Derived();

obj.VirtualMethod();
obj.HiddenMethod();

((Derived) obj).HiddenMethod();

class Base
{
    public virtual void VirtualMethod() => Console.WriteLine("Base.VirtualMethod");
    public void HiddenMethod() => Console.WriteLine("Base.HiddenMethod");
}

class Derived : Base
{
    public override void VirtualMethod() => Console.WriteLine("Derived.VirtualMethod");
    public new void HiddenMethod() => Console.WriteLine("Derived.HiddenMethod");
}

We have a Derived object in a variable of the Base type.

$ dotnet run
Derived.VirtualMethod
Base.HiddenMethod
Derived.HiddenMethod

The overridden virtual method of the Derived class is called. For the hidden method, the Base version is called, unless we cast the variable to the Derived type.

C# type patterns

Sometimes we need to perform an action specific to a derived type. The switch expression with type patterns tests the run-time type of an object.

Program.cs
object[] items = [new Circle(1.5), new Rectangle(2, 3), new Rectangle(4, 4), "text", 42];

foreach (var item in items)
{
    var desc = item switch
    {
        Circle c => $"circle with radius {c.Radius}",
        Rectangle { Width: var w, Height: var h } when w == h => $"square {w}x{h}",
        Rectangle r => $"rectangle {r.Width}x{r.Height}",
        _ => $"unknown: {item}"
    };

    Console.WriteLine(desc);
}

record Circle(double Radius);
record Rectangle(double Width, double Height);

The switch expression matches the run-time type of each element.

$ dotnet run
circle with radius 1.5
rectangle 2x3
square 4x4
unknown: text
unknown: 42

Type checks are useful at the boundaries of a program; for example, when processing data of unknown types. If we control the types, virtual methods are usually a cleaner solution, because adding a new type does not require changing the switch expressions.

C# compile-time polymorphism

Method overloading allows us to define several methods with the same name but different parameters. The compiler chooses the method based on the types of the arguments.

Program.cs
Console.WriteLine(Formatter.Describe(42));
Console.WriteLine(Formatter.Describe(3.14));
Console.WriteLine(Formatter.Describe("falcon"));
Console.WriteLine(Formatter.Describe(1, 2));

static class Formatter
{
    public static string Describe(int x) => $"int: {x}";
    public static string Describe(double x) => $"double: {x}";
    public static string Describe(string s) => $"string: {s}";
    public static string Describe(int x, int y) => $"two ints: {x}, {y}";
}

We have four overloads of the Describe method. Note that local functions in top-level statements cannot be overloaded; therefore, the methods are defined in a static class.

$ dotnet run
int: 42
double: 3.14
string: falcon
two ints: 1, 2

Source

Polymorphism - C# fundamentals

In this article we have worked with polymorphism in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.