Last Updated: June 6, 2026
An abstract method is a method declared without a body, terminated by a semicolon, that every concrete subclass must implement. It's the language's way of saying "this operation exists for the family, but only the specific subclass knows how to do it." This lesson is about the methods themselves, their rules, and the compiler errors that come up when those rules are broken.
The declaration looks like a regular method signature with two changes: the abstract modifier replaces the body, and a semicolon stands in where a { ... } block would normally go.
The Apply declaration on Discount ends in a semicolon, not braces. The two subclasses use override, not new. The loop variable is typed Discount, and the virtual dispatch picks the right body for each runtime type. Abstract methods participate in the same virtual chain as virtual methods, except the base has no fallback body to provide.
The rule the compiler enforces first: an abstract method can only live inside an abstract class. Trying to declare one in a regular class fails immediately.
What's wrong with this code?
The compiler emits CS0513:
The reason is structural. An abstract method has no body, which means an instance of the containing class would have a method that the runtime can't actually call. C# closes that loophole by requiring the containing class to itself be abstract, which makes the class non-instantiable. The class becomes a contract, and the abstract method becomes one of the clauses.
Fix: mark the class abstract.
Now Product cannot be instantiated directly, and any concrete subclass must provide GetTax. The two restrictions move in lockstep: you can't have an abstract method without an abstract class, and an abstract class has limited use without at least one abstract member to enforce.
The matching rule on the derived side is just as strict. A concrete subclass must provide an override for every abstract method inherited from its parent. Forgetting one is a compile error, not a warning.
What's wrong with this code?
The compiler emits CS0534:
The error is identifying a hole in the contract. StandardShipping claims to be a ShippingMethod by inheriting from it, but doesn't fulfill the part of the contract that says "every shipping method calculates cost." The compiler refuses to let the class compile until that hole is filled.
Fix: add the override.
The keyword is override, not new. This is the same override used with virtual methods, and the dispatch behavior is identical. Using new here doesn't satisfy the abstract method; the compiler still emits CS0534 because hiding doesn't put the new body on the virtual chain that the abstract member rooted.
A subtle mistake learners make is to add abstract to an existing method without removing the body. The compiler catches it.
What's wrong with this code?
The compiler emits CS0500:
An abstract method's whole purpose is to be empty. The body comes from the subclass. Allowing both a body and the abstract modifier would create two competing definitions for the same method on the base class, which the language explicitly forbids.
Fix: drop the body, terminate with a semicolon.
When the base class has useful default behavior to offer, use virtual (which has a body and makes overrides optional), not abstract (which has no body and makes overrides mandatory). Mixing the two by trying to give abstract a body misunderstands what each keyword is for.
abstract Implies Virtual, Don't Combine ThemA common reflex when learning these keywords is to write public virtual abstract void M();, thinking it's somehow more explicit. The compiler rejects it.
What's wrong with this code?
The compiler emits CS0503:
An abstract method is already a virtual method without a body. Marking it virtual on top of abstract is redundant from the language's point of view, and the compiler treats redundancy as a sign of confusion rather than emphasis. The fix is to pick one. If you want subclasses to be forced to provide a body, use abstract. If you want a default body that subclasses can replace, use virtual.
The same prohibition applies to abstract static and abstract private. The next two sections cover why.
Two access combinations are forbidden because they contradict what abstract means.
The first is private. An abstract method exists to be overridden by a subclass, and a private member is invisible to subclasses. Declaring a method as both private and abstract creates an obligation that nothing can fulfill.
What's wrong with this code?
The compiler emits CS0621:
Abstract members need accessibility wide enough that a subclass can see them and write an override. public, protected, protected internal, and internal are all valid (with the usual caveat that internal only allows overrides from the same assembly). private and the default private for class members are not.
The second is static. Static members belong to the type itself, not to instances, and the virtual dispatch machinery that makes overrides work operates on instances. A static abstract method in a regular class has no instance to dispatch through.
What's wrong with this code?
The compiler emits CS0112:
There is a special exception for static abstract members in interfaces, introduced in C# 11, but that's an interface feature. Inside a regular abstract class, the rule stays: abstract methods are instance methods. They participate in virtual dispatch through the object's method table, and a static member has no method table entry to participate in.
Fix for both cases: widen access to at least protected for private, or remove static.
Properties can be abstract too. The syntax replaces the property's body block with { get; set; } (or just { get; } for read-only), and the subclass supplies the actual implementation through override.
The interesting detail is that the abstract base declares only the shape of each property. Name is read-only because it's { get; }. Price is read-write because it's { get; set; }. InStock is computed because the base says { get; } and subclasses are free to implement it as an auto-property, a computed expression, or a full property with backing field. Book uses an expression-bodied InStock that reads from a non-abstract Stock property, while DigitalBook hard-codes true. Both satisfy the contract because both supply a get.
You can also widen accessors in the override, but not narrow them. If the abstract declares { get; set; }, the override must provide both. If the abstract declares { get; }, the override may add set but doesn't have to.
The same pattern works for indexers. An indexer is a property that takes parameters and lets you use [] syntax on an instance. Declaring one as abstract follows the same rules: no body, semicolon on the accessors, override required in subclasses.
The abstract indexer says "every catalog can look up a price by product code." StoreCatalog implements that with a dictionary, but a different subclass could implement it with a database call, a cache lookup, or a static price table. Through the Catalog reference, the caller doesn't know or care which.
Events can be abstract as well, though this comes up less often. The declaration uses the standard event syntax with abstract in place of an explicit accessor block.
The base class doesn't store the delegate or define how subscribers are tracked. The concrete subclass does. The pattern is useful when different implementations need to manage subscribers differently, for example a tracker that holds subscribers in memory versus one that persists them to a database.
Abstract members add no measurable overhead at the call site compared to a regular virtual method. Both go through the same method table lookup. The real cost is design: abstract members lock the base into a contract that's hard to change later without breaking every subclass.
Both abstract and virtual participate in the same virtual dispatch mechanism. The difference is whether the base class provides a body and whether overrides are mandatory.
| Question | virtual | abstract |
|---|---|---|
| Does the base class have a body? | Yes | No |
| Can the base class be instantiated? | Yes (if not abstract for other reasons) | No (the class must be abstract) |
| Must subclasses override? | No, override is optional | Yes, every concrete subclass must override |
| Modifier on the override | override | override |
| Dispatch behavior | Runtime, picks the most-derived override | Runtime, picks the most-derived override |
Can it be called via base.Method()? | Yes, the base body runs | No, there is no base body to call |
The dispatch column is identical because both go through the same virtual chain. The other rows are the design choice. virtual says "here's a reasonable default; replace it if you have a better one." abstract says "I refuse to guess what makes sense here; you must decide." Picking between the two is a question of whether a sensible default exists.
Send is abstract because there is no sensible default for how to send a notification; the whole point is that each channel does it differently. FormatTimestamp is virtual because 24-hour HH:mm is a reasonable default that most channels can use, but SMS chooses to format times differently. EmailNotification doesn't override FormatTimestamp, and that's fine because the keyword was virtual, not abstract.
A subclass doesn't have to implement every abstract member it inherits. It can stay abstract itself, passing the obligation down to its own subclasses. This is useful when you want an intermediate layer in the hierarchy that adds some concrete behavior but leaves other parts open.
CardPayment implements Authorize because all card payments authorize the same way, but leaves Capture abstract because the capture step differs between credit and debit. The compiler is happy with this because CardPayment itself is marked abstract, which means it can hold unimplemented abstract members without violating the contract. The concrete classes at the leaves still have to implement everything that's left.
A diagram captures the chain:
The cyan node is the root. The orange node is an intermediate abstract class that has implemented one method but not the other. The green leaves are concrete and must implement what's still abstract above them. The teal BankTransferPayment lives at a different position in the hierarchy and has to implement both methods itself because it inherits directly from the root.
The rule is mechanical: a class can be concrete (instantiable) only when every abstract member from its inheritance chain has been overridden somewhere above or at this class. If even one abstract member is still unfulfilled, the class must be marked abstract.
What's wrong with this code?
The compiler emits CS0534 again:
CardPayment was fine because it was abstract. GiftCardPayment is concrete (no abstract modifier), so the compiler walks up the chain looking for unimplemented members and finds Capture. The fix is either to implement Capture in GiftCardPayment, or mark GiftCardPayment abstract too and push the obligation further down.
Abstract methods follow the same virtual dispatch rules as virtual + override, but it's worth seeing them in action with a base reference because the absence of a base body makes the call routing more obvious.
Print is a concrete method on the abstract base. It calls Render() on this, which is a virtual call. The runtime picks the most-derived override based on the actual object's type, so each call to Print routes through the correct Render. This is a common pattern in abstract classes: provide concrete template methods that call abstract steps, letting subclasses fill in the variable parts while the structure stays fixed.
The diagram shows the routing. The caller invokes Print on a Report reference. Print is concrete, so the call goes to Report.Print directly. Inside Print, the call to Render() is a virtual call (since Render is abstract, which is virtual-without-body). The runtime consults the actual object's type and dispatches to the matching override. The static type of the reference at the original call site (Report) plays no role in choosing which Render runs.
Abstract classes can hold everything a regular class can: fields, properties, methods, constructors, events. The only difference is that the class is non-instantiable, so its constructors run only as part of constructing a derived class. Fields and helper methods live for the lifetime of each derived instance.
(The timestamp will reflect whatever time the program runs.)
The pattern shows three things working together. Order holds shared state (Id, CreatedAt) and shared behavior (Summary). The abstract method Total is the variable part, deferred to subclasses. Each subclass calls the base constructor with : base(id) to populate the shared state, then adds its own state and implements the abstract method. The base constructor is protected because there's no reason for outside code to call it; only derived constructors should.
This pattern of "abstract base with shared state and helpers plus abstract steps" is one of the most common uses of abstract classes. It captures the "common skeleton, varying details" idea in a way that purely abstract contracts can't.
Three recurring mistakes are worth naming explicitly.
The first is declaring an abstract method on a non-abstract class and getting CS0513. The fix is to mark the class abstract, but pause first and consider whether that's the intent. When only some instances need to leave the method unimplemented, an abstract class isn't the fit; a virtual method with a default body or an interface usually works better.
The second is forgetting to override an abstract method in a concrete subclass, getting CS0534. The error is informative because it names the unfulfilled member. The fix is either to add the override or to mark the subclass abstract and push the obligation downward.
The third is trying to call an abstract method via `base.Method()`:
What's wrong with this code?
The compiler emits CS0205:
There's nothing in the base to call. The abstract declaration has no body, and base.Apply is asking to invoke that nonexistent body. If you need a base body to chain into, the base method should be virtual, not abstract. The fix is to either restructure so Apply is virtual with a default, or have the subclass not try to call base.Apply at all.
9 quizzes