İçeriğe geç / Skip to content / Zum Inhalt
Ahmet Balaman LogoAhmet Balaman

C# Inheritance and Polymorphism: virtual, override and new

Ahmet Balaman

7 min read

.NETC#OOPInheritancePolymorphismExam Questions
C# Inheritance and Polymorphism: virtual, override and new

Inheritance lets a class take over the members of another class and add its own behaviour on top. Polymorphism means the same method call runs different code depending on the real type of the object. Together they solve a specific problem: even with ten kinds of employee, you write the salary loop once and never touch it again when a new kind is added. This guide covers base, constructor chaining, virtual/override versus new, sealed, type conversions, and the exam evergreen "what does this code print?".

The Basics: Move What Is Shared Upwards

The access modifiers post showed a small inheritance example while explaining protected. Let's build the topic properly now. Picture a payroll system: every employee has a name and a base salary, but the salary calculation depends on the role.

public class Employee
{
    public string Name { get; }
    protected decimal BaseSalary { get; }

    public Employee(string name, decimal baseSalary)
    {
        Name = name;
        BaseSalary = baseSalary;
    }

    public virtual decimal CalculateSalary() => BaseSalary;

    public override string ToString() => $"{Name}: {CalculateSalary():N0}";
}

public class Manager : Employee
{
    public decimal Bonus { get; }

    public Manager(string name, decimal baseSalary, decimal bonus)
        : base(name, baseSalary)
    {
        Bonus = bonus;
    }

    public override decimal CalculateSalary() => base.CalculateSalary() + Bonus;
}

Manager : Employee reads as "a manager is an employee". Manager gets Name and BaseSalary without rewriting them. Because BaseSalary is protected, the derived class can see it while outside code cannot.

base and Constructor Chaining

The base keyword has two uses. In a constructor, : base(name, baseSalary) calls the base class constructor. Inside a method, base.CalculateSalary() runs the base version, so you extend that code instead of copying it.

The part exams love is the execution order. When an object is created, the base class constructor runs first, then the derived one:

public class Vehicle
{
    public Vehicle() => Console.WriteLine("1) Vehicle constructor");
}

public class Car : Vehicle
{
    public Car() => Console.WriteLine("2) Car constructor");
}

var car = new Car();
// 1) Vehicle constructor
// 2) Car constructor

The reasoning is simple: the derived class relies on the base class fields, so those have to be ready first. If you leave out : base(...), the compiler tries to call the parameterless constructor of the base class. The syntax is the same as the : this(...) you saw in the constructor post; one goes to a constructor in the same class, the other to the base class.

This order has a practical consequence: do not call a virtual method inside a base class constructor. The call goes to the override in the derived class, but at that moment the derived constructor body has not run yet, so the method sees the values assigned there in their default state (null, 0). The compiler does not stop you; the bug shows up quietly at run time.

virtual and override: Real Polymorphism

For a method to be replaceable in a derived class, it has to be marked virtual (or abstract) in the base class and written with override in the derived class. Then the code that runs is chosen by the real type of the object, not by the type of the variable:

Employee employee = new Manager("Emma", 60000m, 15000m);
Console.WriteLine(employee.CalculateSalary());   // 75000

The variable is an Employee, but the object in memory is a Manager. The call is resolved at run time, so Manager.CalculateSalary runs. The CalculateSalary() call inside ToString follows the same rule; the fact that it was written in the base class changes nothing.

Method Hiding with new and the Classic Exam Question

If the derived class declares a method of the same name with new instead of override, it does not replace the base method, it only hides it. Which method runs is then decided at compile time from the type of the variable. Here is the question that appears in almost every OOP exam:

public class A
{
    public virtual void Print() => Console.WriteLine("A.Print");
    public void Show() => Console.WriteLine("A.Show");
}

public class B : A
{
    public override void Print() => Console.WriteLine("B.Print");
    public new void Show() => Console.WriteLine("B.Show");
}

A obj = new B();
obj.Print();
obj.Show();
((B)obj).Show();

What does it print? The answer:

B.Print
A.Show
B.Show

Print is virtual, so the real type of the object (B) decides. Show is not virtual; the variable is of type A, so A.Show runs. Cast the same object to B and the compiler now sees B.Show. The rule worth memorising: override looks at the object, new looks at the variable.

There is a harder variant too:

public class A2 { public virtual void F() => Console.WriteLine("A"); }
public class B2 : A2 { public override void F() => Console.WriteLine("B"); }
public class C2 : B2 { public new virtual void F() => Console.WriteLine("C"); }
public class D2 : C2 { public override void F() => Console.WriteLine("D"); }

A2 a = new D2();
a.F();   // B

C2 c = new D2();
c.F();   // D

The new virtual in C2 cuts the override chain and starts a new one. A variable of type A2 sees the old chain, whose last link is B2. A variable of type C2 sees the new chain and reaches D2. Do not write code like this in a real project, but you should be able to trace it on paper. That tracing skill pays off in algorithm exams in exactly the same way; the insertion and traversal exercises in the binary search tree exam questions post are good practice.

sealed: Stopping Inheritance

Put sealed on a class and nothing can derive from it. Put it on an override method and classes further down can no longer override that method:

public sealed class Intern : Employee
{
    public Intern(string name) : base(name, 20000m) { }

    public override decimal CalculateSalary() => BaseSalary * 0.8m;
}

// public class SeniorIntern : Intern { }
// CS0509: 'SeniorIntern': cannot derive from sealed type 'Intern'

The string class in .NET is sealed as well. Sealing classes you do not expect anyone to derive from makes your intent explicit.

Upcasting, Downcasting, is and as

Converting from a derived type to the base type (upcasting) is always safe and happens implicitly. The other direction (downcasting) is risky, because not every employee is a manager:

Employee employee = new Manager("Emma", 60000m, 15000m);   // upcasting
Employee intern = new Intern("Noah");

// Manager oops = (Manager)intern;   // InvalidCastException at run time

Manager? maybe = intern as Manager;   // Does not throw, returns null

if (employee is Manager manager)      // Check + conversion in one step
    Console.WriteLine($"{manager.Name} bonus: {manager.Bonus:N0}");

In modern C#, pattern matching is the preferred way. A switch expression can check type and properties together:

static string Title(Employee e) => e switch
{
    Manager { Bonus: > 10000m } => "Senior manager",
    Manager => "Manager",
    Intern => "Intern",
    _ => "Employee"
};

One warning: if your code checks types all over the place, that usually points to a missing virtual method. Moving the behaviour into the class is cleaner most of the time.

Mini Scenario: The Payroll List

Let's put the pieces together. The code that calculates the whole team's pay at the end of the month:

List<Employee> team =
[
    new Employee("Liam", 40000m),
    new Manager("Emma", 60000m, 15000m),
    new Intern("Noah")
];

decimal total = 0m;
foreach (Employee person in team)
{
    Console.WriteLine(person);
    total += person.CalculateSalary();
}
Console.WriteLine($"Total payroll: {total:N0}");

Output:

Liam: 40,000
Emma: 75,000
Noah: 16,000
Total payroll: 131,000

There is not a single if in the loop. Each object knows how to calculate its own salary. If a Consultant class arrives tomorrow, you write only that class; the loop, the report and the total stay as they are.

When to Use It and When Not To

Inheritance is the right tool when two classes have a genuine "is a kind of" (is-a) relationship and the derived class can stand in anywhere the base class is used. Building inheritance just to avoid duplicated code is the most common design mistake: every change in the base class hits all derived classes, and in hierarchies three or four levels deep it gets hard to follow who overrides what.

The alternative is composition, a "has a" relationship. That is where the advice "favour composition over inheritance" comes from. If the bonus calculation depends on company policy rather than on the role, pass it in from outside instead of baking it into the class hierarchy:

public interface IBonusPolicy
{
    decimal Calculate(decimal baseSalary);
}

public class FixedBonus(decimal amount) : IBonusPolicy
{
    public decimal Calculate(decimal baseSalary) => amount;
}

public class Payroll(IBonusPolicy bonusPolicy)
{
    public decimal Calculate(decimal baseSalary) =>
        baseSalary + bonusPolicy.Calculate(baseSalary);
}

Payroll has a bonus policy; when the policy changes you supply a new policy object, not a new subclass. For a capability shared by unrelated types you also want an interface rather than inheritance; I explain that distinction in interface vs abstract class.

Common Mistakes

1. Forgetting override

Symptom: not an error but warning CS0108, or CS0114 for virtual methods ("hides inherited member"); the program compiles, yet through a variable of type Employee the base method runs and the salaries come out wrong. Fix: if you mean to replace the method, write override. Do not ignore warnings.

2. Overriding a method that is not virtual

Symptom: CS0506: 'Manager.CalculateSalary()': cannot override inherited member 'Employee.CalculateSalary()' because it is not marked virtual, abstract, or override. Fix: add virtual to the method in the base class.

3. Forgetting the base class constructor

public class Consultant : Employee
{
    public Consultant(string name) { }
    // CS7036: There is no argument given that corresponds to the required parameter 'name' of 'Employee.Employee(string, decimal)'
}

public class Consultant : Employee
{
    public Consultant(string name) : base(name, 50000m) { }   // Correct
}

If the base class has no parameterless constructor, : base(...) is mandatory.

4. Unchecked downcasting

Symptom: System.InvalidCastException at run time. Fix: use if (employee is Manager m) instead of (Manager)employee; if you use as, check the result for null.

Frequently Asked Questions

Can a C# class inherit from more than one class?

No, a class can have only one base class. To give it several capabilities, implement several interfaces.

What is the difference between override and new in one sentence?

override replaces the virtual method of the base class and the call is resolved by the real type of the object. new merely declares a separate method with the same name, and the call is resolved by the type of the variable.

Are private members inherited?

They are part of the object, but the derived class's code cannot access them. If the derived class needs access, make the member protected or expose it through a property or method.

Should I make every method virtual?

No. virtual is a promise that the behaviour may be replaced, and it makes changing that method harder later. Make only those methods virtual that derived classes genuinely need to vary.

Comments