OOP in C#: Inheritance, Polymorphism and Abstraction
Codevity Admin · 10/25/2026
Codevity Admin · 10/25/2026
Object-oriented programming (OOP) is the largest topic in C# interviews. This article follows the questions in the order interviewers usually ask them.
| Pillar | Idea | How C# expresses it |
|---|---|---|
| Encapsulation | Hide internal state and expose a controlled interface | Access modifiers, properties (get, set, init) |
| Inheritance | Reuse and extend a class | class Dog : Animal (single base class) |
| Polymorphism | One call, different behaviour depending on the actual object | virtual and override, interfaces |
| Abstraction | Expose what an object does, not how | abstract classes and interfaces |
virtual in the base, override in the derived class). The run-time type of the object picks the method. This is runtime polymorphism.Animal a = new Dog();
Console.WriteLine(a.Speak()); // Woof (override: the run-time type decides)
Console.WriteLine(a.Name()); // Animal (new only hides: the compile-time type decides)
Dog d = new Dog();
Console.WriteLine(d.Name()); // Dog
public class Animal
{
public virtual string Speak() => "...";
public string Name() => "Animal";
}
public class Dog : Animal
{
public override string Speak() => "Woof";
public new string Name() => "Dog"; // hides Animal.Name; this is not polymorphism
}override takes part in polymorphism. new hides a member: calls through a Dog variable see the new method, calls through an Animal variable still see the old one. Method hiding is almost always a design smell. If you see a compiler warning about hiding, decide whether you meant override.
Try it in the virtual, override and new snippet.
| Abstract class | Interface | |
|---|---|---|
| Purpose | A partly built base class for related types | A contract that unrelated types can implement |
| State (fields) | Yes | No instance state |
| Constructors | Yes | No |
| Implementation | Can mix implemented and abstract members | Members are abstract by default. Default implementations exist (C# 8) but cannot hold state |
| Inheritance | A class inherits one class | A class can implement many interfaces |
| Instantiate directly | No | No |
Use an interface for a capability (, , ) and an abstract class when derived classes share code or state.
Can an abstract class have a constructor? Yes. It runs when a derived class is created, so it can initialize shared state. It is usually , because you can never call on the abstract class itself.
| Modifier | Visible to |
|---|---|
| Everyone | |
| The containing type only (the default for members) | |
| The containing type and derived classes | |
| The same assembly (the default for top-level types) | |
| Derived classes or the same assembly | |
| Derived classes and the same assembly |
A quick way to remember the last two: is the wider one ("or"), is the narrower one ("and").
Expose behaviour through properties, not public fields. A property can add validation later without breaking callers:
Inheritance means is-a (a is an ). Composition means has-a (a has an ). Prefer composition: it is more flexible, does not lock you into a hierarchy, and is easier to test because the parts can be replaced. Deep inheritance trees are brittle, because a change in the base class can break every derived class.
IDisposableIComparable<T>INotifierprotectednewpublicprivateprotectedinternalprotected internalprivate protectedprotected internalprivate protectedDogAnimalCarEnginenewDog.Name()Name()overrideEmployeeCalculatePay()Shape shape = new Circle(2);
Console.WriteLine($"{shape.Name}: {shape.Area():F2}"); // Circle: 12.57
public abstract class Shape
{
protected Shape(string name) => Name = name;
public string Name { get; }
public abstract double Area();
}
public class Circle : Shape
{
private readonly double _radius;
public Circle(double radius) : base("Circle") => _radius = radius;
public override double Area() => Math.PI * _radius * _radius;
}var account = new Account();
account.Deposit(50);
Console.WriteLine(account.Balance); // 50
public class Account
{
public decimal Balance { get; private set; } // readable by anyone, writable only inside
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount));
Balance += amount;
}
}