On this page
Inheritance and interfaces in C#
Inheritance in C#
Inheritance lets you create new classes based on existing ones, reusing and extending their functionality. In C#, a class can inherit from only one class (single inheritance) but can implement multiple interfaces.
// Inheritance syntax: ChildClass : ParentClass
public class Animal
{
public string Name { get; init; } = string.Empty;
public string Sound() => "...";
public virtual string Describe() => $"{Name} makes '{Sound()}'";
}
public class Dog : Animal
{
public new string Sound() => "Woof"; // hides (not overrides)
}
public class Cat : Animal
{
public override string Describe() // overrides the virtual
=> base.Describe() + " [is a cat]";
}virtual, override, and abstract
| Keyword | Meaning |
|---|---|
virtual |
The method can be overridden in subclasses |
override |
Overrides a virtual or abstract method from the base class |
abstract |
The method must be implemented in subclasses (class must be abstract) |
sealed |
Prevents the method from being overridden in further subclasses |
new |
Hides the base class member (different from override) |
public abstract class Shape
{
// Abstract: NO implementation, MUST be implemented
public abstract double Area();
public abstract double Perimeter();
// Virtual: WITH implementation, MAY be overridden
public virtual void Draw()
=> Console.WriteLine($"Drawing {GetType().Name}...");
// Concrete: cannot be overridden unless marked virtual
public string TypeName() => GetType().Name;
}
public class Circle : Shape
{
public double Radius { get; init; }
public override double Area() => Math.PI * Radius * Radius;
public override double Perimeter() => 2 * Math.PI * Radius;
// Override and then seal — no subclass can override this further
public sealed override void Draw()
=> Console.WriteLine($"○ Circle with radius {Radius:F2}");
}
public class Rectangle : Shape
{
public double Width { get; init; }
public double Height { get; init; }
public override double Area() => Width * Height;
public override double Perimeter() => 2 * (Width + Height);
}Polymorphism
Polymorphism allows treating objects of different types uniformly through their base type or interface:
List<Shape> shapes = new()
{
new Circle { Radius = 5 },
new Rectangle { Width = 4, Height = 6 },
new Circle { Radius = 3 },
};
double totalArea = 0;
foreach (Shape s in shapes)
{
s.Draw(); // specific overridden method
totalArea += s.Area(); // polymorphism in action
Console.WriteLine($" Area: {s.Area():F2}");
}
Console.WriteLine($"Total area: {totalArea:F2}");Interfaces
An interface defines a contract without implementation (except for C# 8+ default methods). Any type implementing the interface guarantees it has those members:
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id);
Task<List<T>> ListAsync();
Task<T> CreateAsync(T entity);
Task<T> UpdateAsync(T entity);
Task<bool> DeleteAsync(int id);
}
public interface IValidatable
{
bool IsValid();
IEnumerable<string> GetErrors();
}Multiple interface implementation
public class Employee : IRepository<Employee>, IValidatable, IComparable<Employee>
{
public int Id { get; init; }
public string Name { get; set; } = string.Empty;
public decimal Salary { get; set; }
// IValidatable
public bool IsValid()
=> !string.IsNullOrEmpty(Name) && Salary > 0;
public IEnumerable<string> GetErrors()
{
if (string.IsNullOrEmpty(Name)) yield return "Name is required";
if (Salary <= 0) yield return "Salary must be positive";
}
// IComparable<Employee>
public int CompareTo(Employee? other)
=> other is null ? 1 : Name.CompareTo(other.Name);
// IRepository<Employee> — simplified implementation
public Task<Employee?> GetByIdAsync(int id) => Task.FromResult<Employee?>(null);
public Task<List<Employee>> ListAsync() => Task.FromResult(new List<Employee>());
public Task<Employee> CreateAsync(Employee e) => Task.FromResult(e);
public Task<Employee> UpdateAsync(Employee e) => Task.FromResult(e);
public Task<bool> DeleteAsync(int id) => Task.FromResult(true);
}Default interface methods (C# 8+)
public interface ILogger
{
void Log(string message, string level);
// Default method implementations
void LogInfo(string message) => Log(message, "INFO");
void LogWarning(string message) => Log(message, "WARNING");
void LogError(string message) => Log(message, "ERROR");
}
public class ConsoleLogger : ILogger
{
// Only the abstract method needs implementation
public void Log(string message, string level)
=> Console.WriteLine($"[{level}] {DateTime.Now:HH:mm:ss} — {message}");
}
ILogger logger = new ConsoleLogger();
logger.LogInfo("Application started"); // uses default
logger.LogError("System error"); // uses default
logger.Log("Custom message", "DEBUG"); // direct implementationLiskov Substitution Principle
A subclass instance must be able to substitute its base class without breaking the program:
// BAD: violates Liskov — a square is not correctly a rectangle
public class BadRectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area() => Width * Height;
}
public class BadSquare : BadRectangle
{
public override int Width { set => base.Width = base.Height = value; }
public override int Height { set => base.Width = base.Height = value; }
}
// GOOD: model reality with interfaces
public interface IShape { double Area(); }
public class GoodRectangle : IShape { public double Area() => Width * Height; public double Width; public double Height; }
public class GoodSquare : IShape { public double Area() => Side * Side; public double Side; }Practice
- Shape hierarchy: Create
abstract class ShapewithArea()andPerimeter(). ImplementCircle,Rectangle, andTriangle. Create a list of shapes and calculate the total area. - IExportable interface: Define an
IExportableinterface withExportCsv()andExportJson()methods. Implement it in aReportclass. - Polymorphism: Create a
PrintShapes(IEnumerable<Shape> shapes)method that iterates and prints the type, area, and perimeter of each shape.
In the next lesson we will explore generics and collections: List
Prefer composition over inheritance
Inheritance creates tight coupling. In many cases it is better to inject interfaces (composition) than to inherit. Use inheritance when the relationship is genuinely 'is-a' (an ElectricCar IS a Vehicle). For reusable behavior, prefer interfaces.
Interfaces vs abstract classes
Use interfaces when you want to define a contract that multiple unrelated types can implement. Use abstract classes when you share base implementation and there is a clear hierarchy. In C# you can implement multiple interfaces but only inherit from one class.
Do not abuse deep inheritance
Inheritance hierarchies deeper than 2-3 levels become hard to maintain. The Liskov Substitution Principle says a subclass should be substitutable for its base class without breaking the expected behavior.
// Abstract base class
public abstract class Vehicle
{
public string Make { get; init; }
public string Model { get; init; }
public int Year { get; init; }
protected Vehicle(string make, string model, int year)
{
Make = make;
Model = model;
Year = year;
}
// Abstract method — MUST be implemented in subclasses
public abstract string EngineType();
// Virtual method — MAY be overridden
public virtual string Describe()
=> $"{Make} {Model} ({Year}) — {EngineType()}";
// Non-virtual concrete method
public string Identifier() => $"{Make}-{Model}-{Year}";
}
// Concrete subclass
public class ElectricCar : Vehicle
{
public int BatteryCapacity { get; init; } // kWh
public ElectricCar(string make, string model, int year, int battery)
: base(make, model, year)
{
BatteryCapacity = battery;
}
public override string EngineType() => "Electric";
// sealed — no subclass can override this further
public sealed override string Describe()
=> base.Describe() + $" | Battery: {BatteryCapacity} kWh";
}
var tesla = new ElectricCar("Tesla", "Model 3", 2024, 82);
Console.WriteLine(tesla.Describe());
// Tesla Model 3 (2024) — Electric | Battery: 82 kWh
Sign in to track your progress