C# Abstract Class: When and How to Use It
c# abstract class: Learn how to declare and use C# abstract classes, when they beat interfaces, and how to avoid common design mistakes.
c# abstract class requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
An abstract class in C# is a class that cannot be instantiated directly. It exists to be inherited from, and it can contain both fully implemented members and abstract members that derived classes must implement. This design is useful when you want to define a common contract and shared behavior for a family of related types.
Consider a logging system that must support different output targets. You might have a FileLogger, a ConsoleLogger, and a DatabaseLogger. All of them share a common structure: they need a method to write a message, and they may need a way to initialize or clean up resources. A c# abstract class lets you capture that shared structure once and force each derived logger to provide its own implementation of the details that differ.
The Core Purpose of an Abstract Class
An abstract class sits between a plain base class and an interface. Like a plain base class, it can provide concrete implementations and fields. Unlike a plain base class, it cannot be instantiated on its own. You cannot write new Logger() if Logger is abstract. Instead, you must create an instance of a derived class.
The key feature is the abstract member. An abstract member has no implementation in the base class; it declares a signature that every derived class must override. This gives you a contract that is enforced at compile time, while still allowing the base class to contain shared logic that all derived classes can use or override.
public abstract class Logger { public abstract void Log(string message); public void LogWithTimestamp(string message) { Log($"{DateTime.UtcNow:o} {message}"); } }
In this example, Log is abstract. Any class that inherits from Logger must provide an implementation for Log. The LogWithTimestamp method is concrete; it relies on Log to do the actual output. This pattern lets you add cross-cutting behavior in the base class without duplicating it in every derived class.
Declaring Abstract Classes and Members
To declare an abstract class, use the abstract keyword in the class declaration. You can declare abstract methods, properties, events, and indexers. An abstract member cannot have a body; it ends with a semicolon. The abstract keyword on a member is only allowed inside an abstract class.
public abstract class Shape { public abstract double Area { get; } public abstract double Perimeter(); public string Name { get; set; } = "Shape"; }
Here, Area is an abstract property with only a getter, and Perimeter is an abstract method. The Name property is concrete and can be used as-is by derived classes. A derived class must override both Area and Perimeter; otherwise it will not compile unless it is also declared abstract.
Abstract members are implicitly virtual. That means they can be overridden in any level of inheritance. However, you cannot use the virtual keyword on an abstract member; it is redundant. You also cannot mark an abstract member as private or sealed.
Overriding Abstract Members in Derived Classes
A derived class that is not abstract must provide an override for every abstract member inherited from its base class. The override keyword is required. The signature must match exactly, including the return type and parameter list.
public class Circle : Shape { private readonly double _radius; public Circle(double radius) { _radius = radius; } public override double Area => Math.PI * _radius * _radius; public override double Perimeter() => 2 * Math.PI * _radius; }
The Circle class overrides both abstract members. The Area property uses an expression-bodied getter, which is a concise way to return a computed value. The Perimeter method also uses an expression body. This is valid C# and keeps the code readable.
If a derived class does not want to implement an abstract member, it can declare itself abstract. That forces the next level down to handle the implementation. This is useful when you have an intermediate base class that adds some behavior but still leaves certain details open.
public abstract class Polygon : Shape { public abstract int NumberOfSides { get; } public override double Perimeter() { // A polygon's perimeter requires summing side lengths, // which depends on the specific polygon type. return SumSideLengths(); } protected abstract double SumSideLengths(); }
Polygon overrides Perimeter with a concrete implementation that calls another abstract method SumSideLengths. Derived classes like Triangle or Rectangle must implement Area, NumberOfSides, and SumSideLengths. This shows how abstract classes can create a hierarchy where each level refines the contract.
Abstract Class vs Interface: Decision Criteria
A common design question is when to use an abstract class instead of an interface. The answer depends on what you need to share and how your types relate.
Use an abstract class when:
- You need to share fields, constructors, or concrete method implementations.
- You want to provide a default behavior that derived classes can optionally override.
- Your types share a clear "is-a" relationship and a common base implementation.
- You need to control access to members with
protectedorinternalmodifiers.
Use an interface when:
- You need to define a contract that unrelated types can implement.
- You want to support multiple inheritance, which C# does not allow for classes.
- You need to describe capabilities rather than a common identity.
- You are working with value types, which cannot inherit from classes but can implement interfaces.
Consider the Logger example. Different loggers share the same core behavior and often need a shared field like a minimum log level or a destination path. That makes an abstract class a natural fit. In contrast, if you had an IComparable or IDisposable contract, an interface is better because many unrelated types might need that behavior.
C# 8.0 added default interface methods, which allow interfaces to contain concrete implementations. This narrows the gap, but interfaces still cannot have instance fields or constructors. If you need those, an abstract class is the only option.
When an Abstract Class Is the Wrong Choice
An abstract class is not always the best tool. Overusing it can lead to rigid hierarchies that are hard to modify. If you add a new abstract member to a base class, every derived class must be updated, which can be a breaking change. In a large codebase, that ripple effect can be costly.
If your goal is simply to share a few utility methods, consider a static class or composition instead. For example, a Logger base class might be overkill if you only need a single method that formats a message. A static helper class can achieve the same without forcing inheritance.
Another mistake is using an abstract class to force a particular inheritance structure when the derived types do not truly share a common identity. If you have a Dog and a Car, both might be able to Move(), but that does not mean they should inherit from a common Mover base. An interface like IMovable is more appropriate.
Deep inheritance chains are also a maintainability risk. If your abstract class hierarchy is more than two or three levels deep, it becomes difficult to trace where a particular behavior is defined or overridden. Prefer shallow hierarchies and favor composition over inheritance where possible.
Common Pitfalls and How to Avoid Them
One common pitfall is trying to instantiate an abstract class. The compiler will reject this, but the error message can be confusing if you are not expecting it. For example, new Shape() produces an error like "Cannot create an instance of the abstract class or interface 'Shape'." The fix is to instantiate a concrete derived class.
Another issue is forgetting to override an abstract member. If a derived class is not abstract and does not override all abstract members, the compiler reports an error listing the missing member. This is a compile-time check, so it is caught early. Still, it can be annoying when you add a new abstract member to a base class and many derived classes break. To avoid this, think carefully before adding abstract members to an existing base class; consider whether a virtual member with a default implementation would be less disruptive.
A subtle mistake is using new instead of override when implementing an abstract member. The new keyword hides the base member rather than overriding it. This can lead to unexpected behavior when calling the method through a base-class reference. Always use override for abstract members.
public class BrokenLogger : Logger { public new void Log(string message) // Wrong: hides instead of overrides { Console.WriteLine(message); } }
If you call Log through a Logger reference, the base class's abstract method has no implementation, so this code will not compile because the abstract member is not overridden. The new keyword does not satisfy the abstract contract. The correct approach is public override void Log(string message).
Runtime Behavior and Virtual Dispatch
Abstract members are virtual, so calling them incurs virtual dispatch overhead. When you call an abstract method on a reference typed as the base class, the runtime looks up the actual implementation in the derived class's vtable. This is a small cost, but it is the same cost as any virtual method call. If you are calling such methods in a tight loop, the overhead might matter, but in typical application code it is negligible.
There is no extra memory allocation for an abstract class itself. The object created is an instance of the concrete derived class; the abstract class only contributes its fields and method table entries. The runtime cost is identical to that of a non-abstract base class with virtual members.
One performance-related consideration is that abstract methods cannot be inlined by the JIT compiler as easily as non-virtual methods, because the actual target is unknown until runtime. However, modern JIT optimizations can sometimes devirtualize calls when the concrete type is known at the call site. In practice, you should not avoid abstract classes for performance reasons unless profiling shows a specific hot path that is affected.
Another runtime behavior to keep in mind is that constructors in an abstract class are called when a derived instance is created. If the abstract class has a constructor that requires parameters, every derived class must call it via base(...). This ensures that the shared state is initialized correctly. Failing to do so results in a compile-time error.
public abstract class Repository { private readonly string _connectionString; protected Repository(string connectionString) { _connectionString = connectionString; } } public class UserRepository : Repository { public UserRepository(string connectionString) : base(connectionString) { } }
This pattern is common in data access layers. The abstract base class holds the connection string and any common logic for opening connections, while derived classes implement specific queries. The constructor chain ensures that the connection string is always provided.
When designing an abstract class, keep the constructor simple. Avoid calling abstract methods from the constructor. If you do, the derived class's fields may not be initialized yet, leading to null references or other subtle bugs. The base constructor runs before the derived constructor body, so any abstract method invoked during base construction will see the derived object in an incomplete state. This is a well-known anti-pattern; prefer to initialize derived state in the derived constructor and call abstract methods only after the object is fully constructed.
Abstract classes are a core part of C#'s object-oriented toolkit. They let you model shared behavior and enforce a contract without allowing direct instantiation. Used judiciously, they improve maintainability by centralizing common logic. The key is to recognize when a base class with shared implementation is the right fit, and when an interface or a composition-based approach would be simpler and more flexible.