Back to Blog
C#

c# base class derived class: How Inheritance Works

Understand how to define and extend a c# base class derived class, covering constructors, virtual members, and practical inheritance patterns.

C# inheritancebase keywordvirtual methodspolymorphismabstract classesobject-oriented programming
Illustration of a code editor with a C# class inheritance diagram, showing a base class and derived class relationship.

c# base class derived class requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you write class Derived : Base in C#, you are making a promise about substitutability: every Derived instance is a Base instance. The compiler enforces that promise in several ways. Constructors chain from derived to base, virtual members can be overridden, and non-virtual members are simply inherited. The details of this relationship matter when you design for extensibility or need to reason about runtime dispatch. This article walks through the essential mechanics of a c# base class derived class relationship and highlights the decisions you will actually face in production code.

Declaring a Base Class and Derived Class

A base class defines the common members and behavior that derived classes share. A derived class declares its inheritance using a colon after the class name. Here is the minimal form:

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($"{Name} is barking."); } }

In this example, Dog inherits Name and Eat from Animal. You can call dog.Eat() even though Eat is not defined in Dog. The derived class adds Bark. This is the basic mechanism: code reuse through inheritance. However, inheritance is more than a way to avoid copying methods. It establishes a type hierarchy that affects how variables, collections, and method parameters behave.

How Constructors Chain in an Inheritance Hierarchy

When a derived class is instantiated, the base class constructor runs first. If you do not explicitly call a base constructor, C# invokes the parameterless constructor of the base class. If the base class has no parameterless constructor, the derived class must call one of the base constructors using the base keyword. Consider this example:

public class Vehicle { protected string RegistrationNumber; public Vehicle(string registrationNumber) { RegistrationNumber = registrationNumber; } } public class Car : Vehicle { public Car(string registrationNumber) : base(registrationNumber) { } }

The : base(registrationNumber) syntax ensures the base constructor initializes RegistrationNumber before the Car constructor body runs. This is not optional. If you omit it and Vehicle has no parameterless constructor, the code will not compile. This chaining is deterministic and happens in a fixed order: base constructor, then derived constructor. Understanding this order prevents subtle initialization bugs, especially when constructors call virtual methods.

When to Use Virtual and Override

A virtual method in a base class allows a derived class to provide a different implementation. The derived class uses the override keyword to replace the base implementation. This is the core of polymorphism in C#. For example:

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

Without the virtual keyword, the derived class cannot override the method—it can only hide it with the new keyword, which is usually not what you want. Calling Area() on a Shape reference that actually points to a Circle will dispatch to Circle.Area(). This is runtime polymorphism. The decision to mark a method virtual is a design choice. It invites derived classes to change behavior, which can be powerful but also requires you to consider how derived classes might alter invariants. In a public base class, avoid making every method virtual. Only expose virtual members when extension is an explicit goal.

The base Keyword: Calling Base Class Members

Inside a derived class, the base keyword gives you direct access to the base class implementation. You use it in three common situations:

  1. Calling a base constructor, as shown earlier.
  2. Invoking a base method that has been overridden.
  3. Accessing a base property or field.

Here is an example of calling an overridden base method:

public class BaseLogger { public virtual void Log(string message) { Console.WriteLine($"[Base] {message}"); } } public class TimestampedLogger : BaseLogger { public override void Log(string message) { string timestampedMessage = $"{DateTime.UtcNow}: {message}"; base.Log(timestampedMessage); } }

Using base.Log(...) preserves the base formatting and adds timestamping. This pattern is common in layered logging, where each derived logger enriches the message before passing it up the chain. The base keyword is not a reference to an instance; it is a syntactic device that calls the base implementation directly, bypassing the derived override. That distinction matters when you are debugging recursion or unexpected dispatch.

Choosing Between Abstract Base Classes and Derived Classes

An abstract base class cannot be instantiated and is designed to be inherited. It can define abstract members that have no implementation, forcing derived classes to provide one. This is different from a concrete base class, which you can instantiate directly. Consider a payment processor:

public abstract class PaymentProcessor { public abstract bool ProcessPayment(decimal amount); public void LogPayment(decimal amount) { Console.WriteLine($"Processing {amount:C}"); } } public class CreditCardProcessor : PaymentProcessor { public override bool ProcessPayment(decimal amount) { // Simulate processing. return true; } }

Use an abstract base class when you want to define a contract and share some common implementation. Use a concrete base class when you want to provide a fully usable default implementation that derived classes can extend. The abstract approach forces derived classes to implement critical behavior, which is useful for consistency but also introduces a strict contract that may be too rigid for some scenarios.

Common Pitfalls with Runtime Type and Casting

A derived class instance can be treated as its base type. This is called upcasting and is always safe. The reverse, downcasting, requires an explicit cast or the as keyword. A common mistake is assuming a base reference is actually a derived reference without checking. This leads to InvalidCastException. Example:

Animal animal = new Dog(); Dog dog = animal as Dog; if (dog != null) { dog.Bark(); }

Using as returns null if the cast fails. The is pattern can be used for a more concise check:

if (animal is Dog dog) { dog.Bark(); }

Another pitfall is calling a base method that is not virtual and expecting polymorphic behavior. Non-virtual methods are resolved at compile time based on the variable type, not the runtime type. If you need runtime dispatch, the method must be declared virtual.

Runtime Dispatch and Performance Considerations

Virtual method calls have a small overhead compared to non-virtual calls because the runtime must look up the actual implementation in the type's virtual method table. In practice, modern .NET inlining reduces this cost significantly, and you should not avoid virtual methods for micro‑optimization reasons. However, if a method is in a hot loop and its behavior is never overridden, making it non-virtual allows the JIT to inline it more aggressively, which can improve performance. There is also a memory cost: each instance of a derived class carries a reference to its type information, but the method table is shared across all instances of the same type. For most applications, the clarity of using virtual methods far outweighs the negligible performance penalty.

Maintainability: Knowing When to Prefer Composition

Inheritance creates a tight coupling between the base and derived classes. A change to the base class can affect all derived classes, sometimes unexpectedly. This is why many development guidelines recommend composition over inheritance when the relationship is not strictly "is-a." For a true c# base class derived class relationship, inheritance is appropriate when the derived class genuinely is a more specific version of the base class and you can guarantee that the base contract is stable. When you simply want to reuse behavior without implying a type relationship, composition is usually cleaner. For example, a DataService that uses a Repository is more flexible than a DataService that inherits from Repository, because you can swap the repository implementation without changing the service's type. Inheritance is not inherently bad; it is a tool that remains valuable when applied to a genuine specialization hierarchy, such as a Dog being an Animal. But designing for inheritance requires discipline. Mark members virtual only when you intend them to be overridden. Keep the base class small and focused. Avoid deep inheritance trees, because each level complicates the mental model of where a behavior comes from. The decision to use inheritance should come from the shape of the problem, not from a habit of object-oriented modeling.

Sealing a Derived Class When Further Extension Is Unwanted

Sometimes you want to stop an inheritance chain. The sealed modifier on a class prevents any further derivation. This is useful when you have designed a class with a specific behavior and do not want to support overriding that behavior in an unpredictable way. For example, a sealed class SecureConnection : Connection guarantees that no further derived class can break the security-related logic. Sealing also allows the JIT to devirtualize calls, which can improve performance. In a public library, sealing a class is a commitment: it tells consumers they cannot extend it, which may be appropriate for value types or for classes that contain critical invariants.

A Practical Example: Building an Inheritance Hierarchy

Consider a simple order processing system. Start with a base class for all orders and a derived class for express orders:

public class Order { public decimal Total { get; set; } public virtual decimal CalculateShipping() { return Total > 50 ? 0 : 5; } } public class ExpressOrder : Order { public override decimal CalculateShipping() { // Express base shipping is higher, regardless of total. return Total > 100 ? 10 : 20; } }

This hierarchy works because both order types are conceptually orders and share the Total property. The virtual method allows each type to implement its own shipping logic. If later you need a GiftCardOrder, you can add another derived class. The base class stays unchanged, and existing code that works with Order references automatically works with any new derived class. This is the primary value of inheritance: extending behavior without modifying existing callers. The same principle applies in libraries where you expose a public base class and allow customization through overridable methods.

Compatibility and Versioning Concerns

When you modify a base class, you must consider how existing derived classes will behave. Adding a new virtual method to a base class is backwards compatible, because derived classes simply inherit the new default implementation. Changing an existing method from non-virtual to virtual is a breaking change because derived classes may already have a method with the same signature that uses the new keyword, and the runtime dispatch will change. Removing a method from a base class breaks derived classes that call it. If you run into a situation where you must change a base method's contract, it is often safer to add a new virtual method and leave the old one intact, or to seal the base class and start a new abstraction. This is a practical concern for any shared library or long-lived codebase, where the cost of breaking changes is high. Understanding these constraints helps you design base classes that survive refactoring and evolution.

c# base class derived class: How Inheritance Works | RYUSLOG DEV