Inheritance & Polymorphism
A school has teachers, clerks and a principal. They are all employees with a name and a salary, but each one's pay is worked out a little differently. Inheritance lets you write the shared parts once in a base class (Employee) and create derived classes (Manager, Intern) that reuse those parts and change only what is different.
The real power comes from polymorphism: code written for Employee automatically works with every kind of employee, each behaving in its own way. In this lesson you will build a small staff payroll and learn the keywords that make inheritance work safely.
Base and derived classes
A derived class names its base class after a colon. It inherits all the base class's members, and can add new ones or replace virtual ones with override.
List<Employee> staff =
[
new Employee("Priya", 40000m),
new Manager("Rohan", 65000m, 15000m),
new Intern("Ananya", 15000m)
];
foreach (Employee e in staff)
{
Console.WriteLine(e.Describe());
}
Console.WriteLine($"Monthly payroll: Rs {staff.Sum(e => e.MonthlyPay())}");
class Employee
{
public string Name { get; }
protected decimal BaseSalary { get; }
public Employee(string name, decimal baseSalary)
{
Name = name;
BaseSalary = baseSalary;
}
public virtual decimal MonthlyPay() => BaseSalary;
public string Describe() => $"{GetType().Name,-8} {Name,-7} Rs {MonthlyPay()}";
}
class Manager : Employee
{
private readonly decimal _allowance;
public Manager(string name, decimal baseSalary, decimal allowance)
: base(name, baseSalary)
{
_allowance = allowance;
}
public override decimal MonthlyPay() => BaseSalary + _allowance;
}
class Intern : Employee
{
public Intern(string name, decimal stipend) : base(name, stipend) { }
public override decimal MonthlyPay() => base.MonthlyPay() + 2000m; // travel allowance
}Employee Priya Rs 40000 Manager Rohan Rs 80000 Intern Ananya Rs 17000 Monthly payroll: Rs 137000
Here is what each piece does:
class Manager : Employeemeans "a Manager is an Employee". It inheritsName,BaseSalary,MonthlyPayandDescribe.: base(name, baseSalary)calls the base class constructor, so the shared fields are set up in one place.protectedmembers are visible to the class and its derived classes, but not to outside code.BaseSalaryis protected soManagercan use it whileProgram.cscannot.virtualin the base class says "derived classes may replace this method";overridein the derived class replaces it.base.MonthlyPay()calls the base class version from inside an override, so the intern's pay builds on the normal calculation.
Polymorphism
Look again at the loop. It only knows about Employee, yet each object used its own MonthlyPay. That is polymorphism ("many forms"): the method that runs depends on the object's real type at run time, not on the variable's type. Describe lives only in Employee, but its call to MonthlyPay() still reaches the override. GetType().Name shows that real type.
This is why inheritance is useful. You can add a Principal class next year, and the payroll loop works without a single change.
Constructor order and sealed classes
When you create a derived object, the base constructor always runs first, then the derived constructor. A sealed class cannot be inherited from at all.
var b = new SleeperBerth();
class Berth
{
public Berth() => Console.WriteLine("Berth constructor");
}
sealed class SleeperBerth : Berth
{
public SleeperBerth() => Console.WriteLine("SleeperBerth constructor");
}Berth constructor SleeperBerth constructor
Trying class UpperBerth : SleeperBerth would fail with error CS0509, because SleeperBerth is sealed. Seal classes that were not designed for inheritance; it prevents surprises and lets the runtime optimise calls. Every class ultimately inherits from object, which is where ToString, Equals and GetType come from.
Checking types: is and as
Sometimes you need to know whether a base-type variable holds a particular derived type. The is pattern tests the type and gives you a typed variable in one step. The as operator tries the conversion and returns null if it fails.
Employee[] team = [new Manager("Rohan", 65000m, 15000m), new Intern("Ananya", 15000m)];
foreach (Employee e in team)
{
if (e is Manager m)
{
Console.WriteLine($"{m.Name} is a manager");
}
else
{
Console.WriteLine($"{e.Name} is not a manager");
}
}
Manager? maybe = team[1] as Manager;
Console.WriteLine(maybe is null);Rohan is a manager Ananya is not a manager True
A cast like (Manager)team[1] would throw InvalidCastException, so prefer is when you are not sure. If you find yourself checking types often, it usually means the behaviour belongs in a virtual method instead.
override vs new
If a derived class declares a method with the same name as a non-virtual base method, it hides it rather than overriding it. The compiler warns you, and you can silence the warning with the new modifier. The difference shows up when you call through a base-type variable:
Notification n = new SmsNotification();
n.Send("OTP 4821");
n.Log();
class Notification
{
public virtual void Send(string text) => Console.WriteLine($"Generic: {text}");
public void Log() => Console.WriteLine("Logged by Notification");
}
class SmsNotification : Notification
{
public override void Send(string text) => Console.WriteLine($"SMS: {text}");
public new void Log() => Console.WriteLine("Logged by SmsNotification");
}SMS: OTP 4821 Logged by Notification
Send is overridden, so the SMS version runs. Log is only hidden, so a Notification variable still calls the base version. Hiding is almost never what you want; use virtual and override.
Common mistakes
- Overriding a method that is not virtual. You get
CS0506: cannot override inherited member ... because it is not marked virtual, abstract, or override. Mark the base methodvirtual. - Forgetting
override. Without it, the method hides the base one and you get warningCS0114; polymorphism silently stops working. - Forgetting the base constructor. If the base class has no parameterless constructor, the derived constructor must call
: base(...). - Making everything
public. Useprotectedfor members only derived classes should see. - Using inheritance just to reuse code. If the "is a" sentence sounds wrong, use composition.
Key points
class Derived : Baseinherits the base class's members; C# allows one base class.: base(...)calls the base constructor, which runs before the derived one.- Mark methods
virtualin the base andoverridethem in derived classes. - Polymorphism: a base-type variable calls the real object's override at run time.
protectedmembers are visible to derived classes;sealedstops further inheritance.- Use
ispatterns to test types safely; avoid hiding methods withnew.
Exercise
Create a base class Ticket with Passenger and BaseFare properties and a virtual method Fare() that returns the base fare. Derive TatkalTicket, which adds 30% to the base fare, and SeniorTicket, which gives 40% off. Override ToString in the base class to show the type name, passenger and fare. Put three tickets (Kabir normal Rs 800, Meera tatkal Rs 800, Isha's grandfather senior Rs 800) in a list and print them and the total collected. (Rates are invented for practice.)
Show solution
List<Ticket> tickets =
[
new Ticket("Kabir", 800m),
new TatkalTicket("Meera", 800m),
new SeniorTicket("Mr Verma", 800m)
];
foreach (Ticket t in tickets)
{
Console.WriteLine(t);
}
Console.WriteLine($"Total: Rs {tickets.Sum(t => t.Fare())}");
class Ticket(string passenger, decimal baseFare)
{
public string Passenger { get; } = passenger;
public decimal BaseFare { get; } = baseFare;
public virtual decimal Fare() => BaseFare;
public override string ToString() => $"{GetType().Name}: {Passenger}, Rs {Fare():F2}";
}
class TatkalTicket(string passenger, decimal baseFare) : Ticket(passenger, baseFare)
{
public override decimal Fare() => BaseFare * 1.30m;
}
class SeniorTicket(string passenger, decimal baseFare) : Ticket(passenger, baseFare)
{
public override decimal Fare() => BaseFare * 0.60m;
}Ticket: Kabir, Rs 800.00 TatkalTicket: Meera, Rs 1040.00 SeniorTicket: Mr Verma, Rs 480.00 Total: Rs 2320.00
Primary constructors pass their parameters straight to the base class with : Ticket(passenger, baseFare). ToString lives only in the base class, but it calls the overridden Fare(), so each ticket shows its own price. The total adds 800 + 1040.00 + 480.00.
Quick Quiz
1. What does virtual on a base class method allow?
Only virtual, abstract or override methods can be overridden.
2. Employee e = new Manager(...); e.MonthlyPay(); runs which version if Manager overrides it?
Polymorphism picks the override based on the object's real type.
3. Which constructor runs first when you create a derived object?
The base part of the object is built first, then the derived part.
4. What does x as Manager return if x is not a Manager?
as returns null when the conversion is not possible; a cast would throw.
Learn C# with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
