Skip to content
Inheritance and virtual/override
step 1/7

Reading — step 1 of 7

Learn

~3 min readInheritance, Interfaces, and Exceptions

C# is single-inheritance for classes (with multiple-interface support). The Microsoft C# docs treat inheritance as a core building block, but the recommendation in modern C# is the same as Effective Java: prefer composition; use inheritance only for genuine is-a relationships.

Basic inheritance

public class Animal
{
    public string Name { get; set; }
    
    public Animal(string name)
    {
        Name = name;
    }
    
    public virtual string Speak() => $"{Name} makes a sound";
}

public class Dog : Animal
{
    public Dog(string name) : base(name) { }
    
    public override string Speak() => $"{Name} barks";
}

var rex = new Dog("Rex");
Console.WriteLine(rex.Speak());     // Rex barks
  • class Dog : Animal — inheritance syntax (colon)
  • : base(name) — call the parent constructor
  • virtual on the parent method allows override
  • override on the child method actually overrides

virtual, override, new

Three keywords that often confuse:

public class Base
{
    public virtual string Show() => "Base.Show";
}

public class Sub : Base
{
    public override string Show() => "Sub.Show";        // proper override
}

public class Hider : Base
{
    public new string Show() => "Hider.Show";           // SHADOWS, not overrides
}

Base b1 = new Sub();
Base b2 = new Hider();
Console.WriteLine(b1.Show());     // Sub.Show — virtual dispatch
Console.WriteLine(b2.Show());     // Base.Show — `new` HIDES, doesn't override

new in this context (different from object creation) hides the parent method without overriding. Almost always a mistake — the IDE warns. Use override for proper polymorphism.

abstract — must override

public abstract class Shape
{
    public abstract double Area();          // no body
    public string Describe() => $"Shape with area {Area()}";
}

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

var c = new Circle(5);
Console.WriteLine(c.Describe());

Abstract classes can't be instantiated. Subclasses must implement all abstract members.

sealed — prevent further inheritance

public sealed class FinalDog : Dog
{
    // ...
}

// public class Puppy : FinalDog { }   // ERROR — sealed

Apply sealed to classes that aren't designed for further extension. C# defaults to non-sealed (unlike Kotlin's default-final). Mark sealed when there's a real reason.

protected access

public class Account
{
    protected decimal Balance { get; set; }
    
    public void Deposit(decimal amount) => Balance += amount;
}

public class SavingsAccount : Account
{
    public void AddInterest(decimal rate)
    {
        Balance *= (1 + rate);          // OK — protected accessible from subclass
    }
}

protected = visible to this class AND derived classes. private = this class only. internal = same assembly.

base — explicit parent calls

public class Dog : Animal
{
    public Dog(string name) : base(name) { }
    
    public override string Speak()
    {
        var parent = base.Speak();
        return $"{parent} (and barks)";
    }
}

base.Method() calls the parent's version. Useful when extending — call parent's behavior, then add to it.

Common mistakes

  • Forgetting virtual — child's override doesn't dispatch dynamically. Methods are NON-virtual by default in C# (different from Java where everything is virtual unless final).
  • Using new instead of override — silent shadowing. Calls through base reference go to base.
  • Deep inheritance hierarchies — Effective Java's advice applies: prefer composition.
  • Constructor without : base(...) — uses parameterless base ctor; if none exists, compile error.
  • Mixing inheritance with structs — structs cannot inherit (only implement interfaces).

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…