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 — virtual methods, abstract classes, and interfaces; the method to call is chosen at run time based on the type of the object
- Compile-time polymorphism — method overloading and generics; the method is chosen by the compiler
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.
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.
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.
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.
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.
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.
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.
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
List all C# tutorials.