iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Inheritance

C# inheritance: a derived class extends a base class, getting its members and adding or overriding behavior. Used right, it expresses true IS-A relationships; used wrong, it creates fragile hierarchies. Modern C# prefers composition + interfaces, with inheritance reserved for sealed type families.

virtual, override, sealed, abstract

EXAMPLE
// 1) Basic inheritance
public class Animal
{
    public string Name { get; }
    public Animal(string name) => Name = name;

    public virtual string Sound() => "...";
    public override string ToString() => $"{GetType().Name} {Name}";
}

public class Dog : Animal
{
    public Dog(string name) : base(name) { }
    public override string Sound() => "woof";
}

public class Puppy : Dog
{
    public Puppy(string name) : base(name) { }
    public override string Sound() => $"{base.Sound()} (small)";
}

// 2) abstract — must be implemented by derived classes
public abstract class Shape
{
    public abstract double Area();
    public virtual string Describe() => $"{GetType().Name} area={Area():F2}";
}

public class Circle : Shape
{
    public double Radius { get; }
    public Circle(double radius) => Radius = radius;
    public override double Area() => Math.PI * Radius * Radius;
}

public class Rectangle : Shape
{
    public double Width  { get; }
    public double Height { get; }
    public Rectangle(double w, double h) { Width = w; Height = h; }
    public override double Area() => Width * Height;
}

// 3) sealed — prevent further extension
public sealed class Square : Rectangle
{
    public Square(double side) : base(side, side) { }
}

// Sealing methods
public class Specific : Rectangle
{
    public Specific(double w, double h) : base(w, h) { }
    public sealed override double Area() => Width * Height;     // can't be re-overridden
}

// Use sealed when:
//   • Performance: virtual call → direct call when the JIT sees a sealed type
//   • Safety: clients can't insert misbehaving subclasses
//   • Express intent: 'this leaf is final'

// 4) Polymorphism — pattern matching beats long inheritance chains
static double Total(IEnumerable<Shape> shapes) => shapes.Sum(s => s.Area());

static string Describe(Shape s) => s switch
{
    Circle    c    => $"circle r={c.Radius}",
    Square    sq   => $"square side={sq.Width}",
    Rectangle r    => $"rect {r.Width}x{r.Height}",
    _              => "unknown",
};

// 5) Constructors — chain to base, validate at the boundary
public class Employee : Person
{
    public string Department { get; }
    public Employee(string name, int age, string dept) : base(name, age)
    {
        Department = dept ?? throw new ArgumentNullException(nameof(dept));
    }
}

// 6) Protected members — for subclasses, not the public API
public class Repository<T>
{
    protected readonly DbContext Db;
    public Repository(DbContext db) => Db = db;

    protected virtual IQueryable<T> Query() => Db.Set<T>();
}

public class ActiveRepository<T> : Repository<T> where T : class, IActiveFlag
{
    public ActiveRepository(DbContext db) : base(db) { }
    protected override IQueryable<T> Query() => base.Query().Where(x => x.IsActive);
}

// 7) Interfaces — usually a better fit than base classes
public interface ILogger
{
    void Info(string msg);
    void Error(string msg, Exception? ex = null);
}

public class ConsoleLogger : ILogger
{
    public void Info(string msg)  => Console.WriteLine($"[info] {msg}");
    public void Error(string msg, Exception? ex = null) =>
        Console.WriteLine($"[err]  {msg} {ex}");
}

// Multiple interfaces — composition without diamond problem
public class FileLogger : ILogger, IDisposable
{
    private readonly StreamWriter _w;
    public FileLogger(string path) => _w = new StreamWriter(path, append: true);
    public void Info(string msg) { _w.WriteLine($"[info] {msg}"); }
    public void Error(string msg, Exception? ex = null) { _w.WriteLine($"[err]  {msg} {ex}"); }
    public void Dispose() => _w.Dispose();
}

// 8) Default interface methods (C# 8+) — add behavior without breaking implementations
public interface ILogger2
{
    void Log(LogLevel level, string msg);
    void Info(string msg)  => Log(LogLevel.Info, msg);   // default impl
    void Error(string msg) => Log(LogLevel.Error, msg);
}

// 9) Covariant return types (C# 9+)
public class Animal2 { public virtual Animal2 Clone() => new Animal2(); }
public class Dog2 : Animal2 { public override Dog2 Clone() => new Dog2(); }
// Caller of Dog2.Clone() gets Dog2, not Animal2 — strongly typed override.

// 10) Records — value-based equality + concise inheritance
public record Person(string Name, int Age);
public record Employee(string Name, int Age, string Department) : Person(Name, Age);

// Records implement value equality automatically: with-expressions for non-destructive mutation.
var emp = new Employee("mara", 30, "ops");
var promoted = emp with { Department = "platform" };

// 11) Liskov red flags
//   • Override that throws NotSupportedException → caller can't substitute safely
//   • Subclass that tightens preconditions or weakens postconditions
//   • Square : Rectangle — classic LSP violation when SetWidth/SetHeight allow independent updates
//   • Need to type-check (if obj is X) on a polymorphic call → polymorphism failing

// 12) When to favour what
//   composition + interfaces  → most apps, most teams
//   abstract base class       → sharing concrete impl AND requiring subclass-supplied steps (Template Method)
//   sealed base class          → leaf type families with pattern matching (Shape, Result<T>)
//   inheritance chain ≥ 3     → reconsider; deep hierarchies almost always regret it

// 13) Common bugs
//   • new instead of override — hides the base method, breaks polymorphism silently
//   • Calling virtual method from a constructor — subclass code runs on a half-built object
//   • Forgetting base(...) call when the base ctor has required args — compile error or wrong defaults
//   • Declaring fields public on a base class — every subclass touches them, no encapsulation
//   • Inheriting just to reuse code — prefer composition (private field of the helper)
//   • Treating interfaces as 'one method, then add more later' without versioning strategy

Why it matters

C# inheritance still has its place — abstract bases with virtual hooks, sealed leaves for pattern-matched switches — but in 2025 most teams default to interfaces plus composition and records for data. If your hierarchy is more than two levels deep, the next refactor is almost always “flatten and replace inheritance with delegation.”

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
public class Animal { public virtual string Speak() => "…"; }
public class Dog : Animal { public override string Speak() => "woof"; }
Try it Yourself »

Exercise

Inherit from Animal.

public class Dog Animal { }

Discussion

Loading…