C# Class Inheritance: Syntax and Usage
c# class inheritance: Learn the practical side of C# class inheritance: syntax, base constructors, method overriding, and common pitfalls in real-world code.
C# class inheritance lets a derived class reuse the members of a base class while adding or refining its own behavior. The syntax is direct, but the runtime implications and constructor rules often surprise developers who are new to it. This article covers the practical mechanics of inheritance in C#, including how base constructors are invoked, how overriding works, and where inheritance is the wrong tool.
Declaring a Derived Class in C#
The core syntax uses a colon after the derived class name, followed by the base class. Here is the minimal pattern:
public class Animal { public string Name { get; set; } public void Eat() { Console.WriteLine($"{Name} is eating."); } } public class Dog : Animal { public void Bark() { Console.WriteLine("Woof!"); } }
The Dog class inherits the Name property and the Eat() method from Animal. Any instance of Dog can call Eat() as if it were defined directly on Dog. This is the core value of inheritance: model an "is-a" relationship and reuse common logic.
When you create an instance of Dog, it is also an Animal in the type system. That means you can assign it to an Animal variable:
Animal pet = new Dog { Name = "Rex" }; pet.Eat(); // works because Eat is on Animal
This is often called polymorphism—the variable's static type is Animal, but the runtime object is a Dog.
Base Constructor Execution Order
Inheritance introduces a strict initialization order. When a derived class instance is created, the base class constructor runs first, then the derived class constructor. If the base class has no parameterless constructor, the derived class must explicitly call a base constructor using the base keyword.
public class Animal { protected Animal(string name) { Name = name; } public string Name { get; } } public class Dog : Animal { public Dog(string name) : base(name) { } }
Here, Dog cannot be constructed without providing a name to the Animal constructor. The base(name) syntax passes the argument up the chain. If you omit this, the compiler fails because there is no parameterless Animal constructor to call.
The order matters because the base class may depend on its own state being initialized before the derived class runs. For example, if Animal sets up a protected field that Dog uses in its constructor body, that field is ready by the time Dog's constructor executes.
Virtual and Override Members
A base class member is not automatically overridable. In C#, you must mark a method as virtual to allow a derived class to override it. The derived class then uses the override keyword.
public class Animal { public virtual void Speak() { Console.WriteLine("Some sound"); } } public class Dog : Animal { public override void Speak() { Console.WriteLine("Woof!"); } }
When you call Speak() on a variable typed as Animal, the runtime dispatches to the most derived override. That means:
Animal pet = new Dog(); pet.Speak(); // prints "Woof!"
Without virtual and override, the derived class could use the new keyword to hide the base method, but that bypasses polymorphism. The method resolution then depends on the static type, which is rarely what you want.
Method Hiding with new Versus override
The new keyword hides a base method rather than overriding it. Consider:
public class Dog : Animal { public new void Speak() { Console.WriteLine("Woof!"); } }
Now the behavior depends on the variable type:
Dog dog = new Dog(); dog.Speak(); // "Woof!" Animal animal = dog; animal.Speak(); // "Some sound"
This is usually unintended. Use new only when you deliberately want to hide a member and you are certain callers will not treat the object through a base-type reference. In practice, override is almost always the right choice when a logical "is-a" relationship exists.
Access Modifiers and Inheritance
Inheritance also governs what a derived class can see from its base. private members are accessible only within the declaring class, not in derived classes. protected members are accessible in the base class and any derived class. public members are accessible everywhere.
public class Animal { private int _id; protected string Name { get; set; } public void Eat() { } } public class Dog : Animal { public void PrintName() { Console.WriteLine(Name); // ok: protected // Console.WriteLine(_id); // compile error: private } }
In practice, protected members are useful when you want derived classes to share logic or state, but you still want to hide it from external callers. Avoid overusing protected because it increases coupling between the base and derived classes.
Abstract Classes: Enforcing Contracts
An abstract class cannot be instantiated directly. It can contain abstract methods—declarations without an implementation—that all non-abstract derived classes must override.
public abstract class Animal { public abstract void Speak(); } public class Dog : Animal { public override void Speak() { Console.WriteLine("Woof!"); } }
Abstract methods are implicitly virtual; you cannot mark them virtual explicitly. A derived class that does not implement all abstract members must itself be abstract.
Use an abstract class when you want to provide some shared implementation but still require derived classes to fill in specific behavior. If you need no shared implementation at all, an interface is usually a better fit.
Sealed Classes: Preventing Further Inheritance
The sealed modifier stops other classes from deriving from a given class. It also applies to method overrides, preventing further overriding in deeper descendants.
public sealed class Dog : Animal { public override void Speak() { Console.WriteLine("Woof!"); } } // public class Puppy : Dog { } // compile error
Sealing a class can improve performance in some JIT scenarios because the compiler can safely devirtualize calls, but the gain is usually minor. More importantly, sealing communicates design intent—you do not want anyone to extend the class. It also reduces the chance of unexpected virtual dispatch bugs in large codebases.
Relationship to Interfaces
Inheritance is not the only way to share behavior. Interfaces define a contract without providing implementation. A class can implement multiple interfaces but inherit from only one base class. This distinction matters when you need to model capabilities that are not strictly hierarchical.
| Kind of reuse | Inheritance | Interface |
|---|---|---|
| Single base allowed | Yes | No (multiple allowed) |
| Shared implementation | Possible | No (default interface methods) |
| Contract enforcement | Abstract members | All members |
| Best used when | "Is-a" relationship exists | "Can-do" capability is needed |
For instance, a Dog can inherit from Animal, but it might also implement ITrainable and IPet because those are capabilities, not taxonomic categories. Relying on inheritance to model every form of reuse leads to deep, rigid hierarchies that are hard to change.
Common Pitfalls and How to Avoid Them
One recurring mistake is calling a virtual method from a base class constructor. Because the derived class constructor has not run yet, the override sees an object in a partially initialized state. This can throw a null reference exception or produce inconsistent behavior.
public class Animal { public Animal() { Speak(); // dangerous: virtual call in constructor } public virtual void Speak() { } } public class Dog : Animal { private string _sound = "Woof"; public Dog() { _sound = "Bark"; } public override void Speak() { Console.WriteLine(_sound); // may be null or "Woof" } }
Here, _sound is assigned its initial value before the Dog constructor body runs, but after the base constructor calls Speak(). The output is not what a developer might expect. Avoid virtual dispatch from constructors entirely.
Another issue is overusing inheritance when composition would be simpler. Inheritance exposes all public and protected members of the base class, whether or not the derived class needs them. That broad surface area makes changes to the base class ripple through every derived type. Composition—holding a reference to a helper object—limits the exposed API and gives you finer control.
When Inheritance Is the Right Choice
Choose inheritance when all of these hold:
- The derived class is a true subtype of the base—an "is-a" relationship.
- The base class genuinely contributes shared implementation or state.
- You are comfortable exposing the same public surface to callers.
Avoid inheritance when the only reason is code reuse across unrelated classes. In that case, extract a helper class or use an interface. Also avoid inheritance for value objects or simple data containers; a record or a plain class with composition is often cleaner.
For example, modeling a Rectangle and a Square as derived classes is a classic mistake because a square is not a substitutable rectangle: changing the width of a square would affect the height, violating the behavior expected from a rectangle. The "is-a" test must be behavioral, not just semantic.
Inheritance and Memory Layout
Inheritance affects how objects are laid out in memory. A derived object contains all the fields from its base class plus its own. When you cast from a derived type to a base type, the runtime uses that layout to locate the base portion of the object. This is one reason the CLR supports type checks and casts—it must verify that the actual object can be treated as the target type.
For most business applications, this memory overhead is negligible. But in performance-sensitive code, understand that a deeply nested inheritance chain means more fields per object and potentially more indirection when calling virtual methods. Virtual method calls require a lookup through the type's vtable. In hot loops, micro-optimizations such as sealing classes or preferring non-virtual calls may matter, but you should measure before changing design for performance.
Overriding ToString and Other Object Members
Every class derives from System.Object, which provides virtual methods like ToString(), Equals(), and GetHashCode(). Overriding ToString() is a common way to produce meaningful diagnostics.
public class Dog : Animal { public override string ToString() { return $"Dog: {Name}"; } }
If you implement Equals(), you must also override GetHashCode() to keep the contract consistent. Inheritance can complicate Equals() because you need to decide how derived classes participate in equality. A common pattern is to define equality based on the base class's key fields and require derived classes to call base.Equals() when appropriate.
Compatibility and Maintainability Considerations
Adding a new member to a base class is a binary breaking change if a derived class already has a member with the same name and signature. The compiler treats it as a warning—the derived member hides the base member—and the behavior depends on how the derived member is defined. Removing a member from a base class is a breaking change that will fail compilation anywhere that member is used. Changing a method signature in the base class may break derived classes that override it.
In library design, adding an abstract member to an abstract base class is a source-breaking change for all existing derived classes—they will not compile until they implement the new member. In contrast, adding a non-abstract virtual member is binary-compatible, but it may change behavior if a derived class happens to have a same-named method.
These constraints matter when you ship a public API. If you expect external consumers to derive from your classes, treat the base class as a public contract. Prefer adding new members as non-virtual and consider using an interface with default implementation if you need to expand capability without breaking implementations.
Runtime Type Checking with is and as
Inheritance often leads to code that checks the concrete type of an object. The is operator performs a type test, and as performs a cast that returns null on failure.
Animal pet = new Dog(); if (pet is Dog dog) { dog.Bark(); }
This is safe, but overusing type checks suggests that polymorphism should be doing more of the work. Prefer calling a virtual method that each derived type implements rather than switching on type. Type checks are appropriate when you need to handle a special case that is not part of the common contract—for example, invoking a method that exists only on a specific derived class.
The as operator is useful when the conversion might fail and you want to avoid an exception:
Dog dog = pet as Dog; if (dog != null) { dog.Bark(); }
For value types, as does not work because they cannot be null. Use is with a pattern when you need to handle both reference and value types.
The Fragile Base Class Problem
Changes to a base class can break derived classes in subtle ways. This is known as the fragile base class problem. A simple change, such as altering the order of field initialization or adding a call to a virtual method, can produce unexpected behavior in derived classes. Even if the base class's public API is unchanged, its internal behavior is coupled to derived classes through virtual members.
To reduce this, minimize the number of virtual members in a base class. Each virtual member is an extension point that derived classes can rely on. If the base class calls a virtual method internally, ensure the documentation clearly states the order and context in which that call occurs. Also consider making fields private and exposing protected methods only when necessary.
Inheritance vs. Composition in Real-World Code
Many inheritance hierarchies start clean but grow tangled as features are added. A common replacement is composition: have a class hold a reference to another object that encapsulates behavior.
public class Dog { private readonly ISoundPlayer _soundPlayer; public Dog(ISoundPlayer soundPlayer) { _soundPlayer = soundPlayer; } public void Speak() { _soundPlayer.PlaySound("Woof"); } }
This approach makes the dependency explicit, simplifies testing (you can mock ISoundPlayer), and does not expose unrelated members. The tradeoff is that you must manually delegate methods that the base class would provide automatically. Use inheritance when the relationship is genuinely hierarchical; use composition when you need flexibility or when the "is-a" test fails.
Handling Inheritance in Large Codebases
In a large codebase, inheritance can become a maintenance bottleneck if every new feature requires modifying the base class. A common pattern is the template method: the base class defines the skeleton of an algorithm and lets derived classes fill in steps.
public abstract class DataParser { public void Parse(string input) { Validate(input); var data = ExtractData(input); Process(data); } protected abstract void Validate(string input); protected abstract object ExtractData(string input); protected abstract void Process(object data); }
This keeps the control flow in one place while allowing derived classes to vary the details. However, be aware that adding a new step to the template method changes behavior for all derived classes, so the extension point design must be stable.
Final Code Example: A Practical Inheritance Setup
Combine the concepts into a complete example:
public abstract class Shape { public abstract double GetArea(); public override string ToString() { return $"{GetType().Name} with area {GetArea():F2}"; } } public class Circle : Shape { public double Radius { get; } public Circle(double radius) { Radius = radius; } public override double GetArea() { return Math.PI * Radius * Radius; } } public sealed class Square : Shape { public double Side { get; } public Square(double side) { Side = side; } public override double GetArea() { return Side * Side; } }
Here, Shape defines an abstract contract, Circle and Square provide the overrides, and Square is sealed because no sensible subclass exists. The ToString() method in the base class uses the abstract GetArea() polymorphically. This example shows how inheritance can centralize common behavior while still relying on derived types to supply the specifics. When you later add a Triangle class, it must implement GetArea() to satisfy the contract—no other change is needed.