AlgoMaster Logo

Inheritance Basics

Last Updated: May 22, 2026

High Priority
13 min read

Inheritance lets one class take on the fields, properties, and methods of another, so you don't rewrite the same code every time a new kind of thing shows up. In C#, a class can extend exactly one other class with the : BaseClass syntax, and the new class inherits every accessible member of the original. This chapter covers what inheritance actually is, why you'd use it, the syntax for declaring a derived class, which members carry over and which don't, the role of access modifiers (especially protected), the single-inheritance rule, and when you should reach for composition instead.

Why Inheritance Exists

Consider an online store that sells two kinds of products: books and electronics. Both have a name, a price, and a stock count. Both should be able to print a summary, calculate a discounted price, and check if they're in stock. Books also have an author and a page count. Electronics have a warranty period and a power rating. Written without inheritance, the two classes look like this:

Three fields, three methods, all identical, repeated in two classes. Add a Toy class next and you'll copy them a third time. Decide tomorrow that PrintSummary should include a currency symbol from a config setting, and you'll edit it everywhere. The problem isn't that the code is wrong, it's that the duplication invites bugs. One day you'll forget to update one of the copies.

Inheritance removes the duplication. You declare a base class called Product that holds the shared data and behavior, then declare Book and Electronics as classes that inherit from Product:

Book and Electronics each declared only their own extra fields. The shared Name, Price, Stock, PrintSummary, GetDiscountedPrice, and IsInStock carry over through inheritance, defined once in Product and reused by both. Adding a Toy class next is now a three-line declaration, not a fifty-line copy.

That captures the two reasons inheritance exists:

  • Code reuse. Write shared fields and behavior once in a base class. Every derived class gets them automatically. A change to the base class flows through every class that inherits from it.
  • Modeling "is-a" relationships. A book is a product. A laptop is a product. When that relationship is real in the domain, inheritance lets the type system reflect it. Code that accepts a Product can work with a Book, an Electronics, or anything else that inherits from Product. The full power of that idea is polymorphism.

There's a third benefit, less obvious at this stage: inheritance creates a vocabulary. Saying "everything that's a Product" is shorter and clearer than "everything that has a name, a price, and a stock." The base class gives a name to the abstraction.

The : BaseClass Syntax

To declare that one class inherits from another, put a colon and the base class name after the derived class name:

In this declaration, Product is the base class (sometimes called the parent class or superclass), and Book is the derived class (also called the child class or subclass). Reading the line aloud, "class Book derives from Product" or "class Book is a Product" both work.

A small picture of the relationship:

The arrows point from the base class to each derived class to show the inheritance chain. Every member declared on Product exists on Book, Electronics, and Toy, plus each derived class adds its own extras. The convention in many UML-style diagrams is to draw the arrow the other way (from derived to base), but the direction of the arrow doesn't matter for understanding. What matters is which class owns which member.

The derived class can be used wherever the syntax expects either its own type or its base type. Setting a field declared on the base class works through any instance of the derived class:

From the outside, a Book object looks like a single bag of fields: Name, Price, Author. The compiler knows which class declared each one, but at the usage site you just dot into them.

You can also assign a Book reference to a Product variable, because every Book is a Product:

The variable p is typed as Product, so the compiler only lets you reach members declared on Product or its ancestors. The underlying object is still a Book, just viewed through the narrower lens of the Product type. This is called upcasting, and it's the mechanism that makes polymorphism work. The Polymorphism section returns to it.

What Gets Inherited and What Doesn't

When you write class Book : Product, not every line inside Product is copied into Book blindly. The rule is precise: a derived class inherits all members of its base class except constructors, finalizers, and static constructors. Whether the derived class can actually see and use each inherited member depends on the member's access modifier.

Here's a table summarizing what carries over:

Member declared on baseInherited by derived?Accessible inside derived class?
public field, property, methodYesYes
protected field, property, methodYesYes
internal field, property, methodYesYes (within the same assembly)
private field, property, methodYes (technically)No
ConstructorNoN/A
FinalizerNoN/A
Static constructorNoN/A
static memberYes (callable through the derived class name)Yes

The "technically inherited but not accessible" row is the easy one to misread. A private field declared on the base class is part of every derived instance's data, but the derived class's code can't read or write it directly. It's there in memory; you just don't have the key. This matters because methods inherited from the base class can still touch those private fields (the base's own code has the access), so the inheritance is real even though the visibility isn't.

A short program makes the difference concrete:

The class Product declares three different visibility levels. Name is public, so anyone can read or set it. cost is protected, so Book can use it inside its own methods, but outside code can't. sku is private, so only Product's own code touches it directly; Book can only reach it through the public methods SetSku and GetSku. Each access modifier draws a different boundary, and inheritance respects each one.

The fact that constructors aren't inherited is worth pausing on. Writing class Book : Product { } does not automatically give Book a constructor that takes the same parameters as Product's constructor. The derived class always declares its own constructors, and they call into the base class's constructor as part of their setup. For this lesson, we're sticking with the implicit parameterless constructor that C# generates when you don't write one.

Inheritance doesn't make objects bigger than the union of all inherited fields. A Book object on the heap holds slots for Name, Price, Stock (from Product), plus Author and Pages (from Book). There's no per-class overhead beyond what each member needs. The virtual method table (covered with virtual/override) adds a small constant cost per object regardless of depth.

Access Modifiers in an Inheritance Context

You met public and private in the _Access Modifiers_ lesson. Inheritance is where the third common modifier, protected, becomes useful. A short refresher of all four, focused on what they mean once a base/derived relationship exists:

ModifierSame classSame assemblyDerived classAnywhere
publicYesYesYesYes
protectedYesNoYesNo
internalYesYesNo (unless also in the same assembly)No
privateYesNoNoNo
protected internalYesYesYesNo
private protectedYesNo (same assembly only)Yes (same assembly only)No

The two combined modifiers at the bottom (protected internal and private protected) are less common and exist to fine-tune access between assemblies. For the next several chapters, the three to focus on are public, private, and protected.

The rule of thumb most C# developers follow:

  • `private` for fields and helper methods that are pure implementation detail. Hide everything you can. If a future change inside the class shouldn't break any caller, keep it private.
  • `protected` for members that derived classes need but outside callers shouldn't touch. Think "extension points." If a base class manages some state and offers a hook for derived classes to use that state, the hook is protected.
  • `public` for the API surface that any code is allowed to use.
  • `internal` for helpers shared across files within the same assembly but not exposed to consumers. Useful in libraries.

protected shows its value when you have data that the base class wants to manage but derived classes need to read or update. Take a base Product that tracks its own stock and offers a hook for derived classes to reduce stock when something product-specific happens (a book is reserved for a library, an electronic item gets returned for repair):

Outside callers (the Main method here) can't change stock directly or call ReduceStock. The base class controls how stock moves through SetStock and the protected ReduceStock helper. Inside Book, the method ReserveForLibrary is allowed to call ReduceStock because Book inherits from Product. That's the slot protected fills: a member visible to the inheritance hierarchy but invisible to everyone else.

A common beginner mistake is to make everything public so the code "just works." That choice voids the contract a class is supposed to enforce. If stock were public, anyone could set it to -1000 and break invariants like "we never ship more than we have." Picking the narrowest modifier that still lets the necessary code compile is the discipline that keeps a codebase from rotting.

Single Inheritance

C# allows a class to inherit from exactly one base class. You can't write class Hybrid : Foo, Bar and expect to pick up members from both. Try it, and the compiler refuses:

This is called the single inheritance rule, and it's a deliberate design choice in C# (and many modern languages). Languages that allow multiple inheritance of classes have to answer awkward questions like "if both base classes declare a method with the same name, which one does the derived class inherit?" or "if a derived class inherits from two classes that both inherit from a shared ancestor, do you get one copy or two of the ancestor's data?" These problems have solutions, but the solutions add complexity that most code doesn't need.

The escape hatch is interfaces, which a class can implement in unlimited numbers. An interface describes what a class can do (a list of methods and properties) without providing any implementation. So you can write class Book : Product, IPrintable, ISearchable to inherit from one class and implement two interfaces. Interfaces are the topic of the Abstraction section. For now, just remember that "one base class, many interfaces" is the C# way.

Inheritance can chain. A class derived from a base can itself be a base for another class:

TextBook inherits everything from Book, which itself inherits from Product. So a TextBook ends up with four fields: Name, Price, Author, Subject. The chain can be as deep as you want, though most well-designed C# code keeps inheritance shallow (one or two levels). Deep hierarchies become hard to follow, because to understand what a leaf class can do, you have to read every ancestor up to the root.

The chain visualized:

The top node, object, deserves its own subsection.

The Default Base Class: System.Object

When you declare a class with no : BaseClass clause, C# silently inserts : System.Object for you. So this:

is exactly equivalent to:

Every class in C# ultimately inherits from System.Object, which is also written with its keyword alias object. There is no class in the .NET type system that doesn't trace back to object at the root. Even int and decimal, which are value types, share this root through a different mechanism (boxing).

object declares a handful of methods that every class therefore inherits:

Method on objectWhat it does
ToString()Returns a string that represents the object. Default is the type name.
Equals(object?)Compares two references (or values, after overriding).
GetHashCode()Returns an integer hash, used by Dictionary and HashSet.
GetType()Returns a Type object describing the runtime type.

A tiny demonstration:

Output (your hash code will differ):

ToString() and GetType() both produce the type name (Product) because Product didn't override ToString and the default just returns the class name. GetHashCode() returns a number derived from the object's identity in memory, which is why it changes between runs.

The full breakdown of these methods, plus the rules for overriding them correctly, lives in this section's lesson 7 ("Object Class") and lesson 9 ("Equals & GetHashCode"). The point to take away here is that you never have to write : object yourself. The compiler does it for any class that doesn't declare a different base.

One small consequence is that the chain in the previous diagram is more precise once we add the implicit root:

Whether you write the : object or leave it off, the runtime treats them the same. Most C# code leaves it off.

A Worked Hierarchy: E-Commerce Products

To pull everything together, here's a slightly bigger E-Commerce example that uses inheritance the way real C# code does. The base class Product knows the basics of any item in the store. Book and Electronics derive from it and add their own details. A small Main builds a cart-like list and uses members from both levels.

The List<Product> holds both a Book and an Electronics, which is the upcasting in action. Iterating that list calls PrintSummary on each item, and each derived class uses the inherited implementation from Product without writing anything itself. The protected field stock is updated through the public Restock method, while readers can see the value through the Stock property (a getter-only property). Each derived class adds exactly what's unique to it.

If you needed to add a Toy class next, you'd write:

Four lines. Every Toy instance now has a name, price, stock, the ability to restock, and the ability to print a summary. Nothing else has to change. That payoff is what makes inheritance worth the rules around it.

Inheritance vs Composition

Inheritance is one way to build complex things from simpler ones. The other is composition, where a class holds an instance of another class as a field rather than inheriting from it. They solve overlapping problems with very different trade-offs, and picking the right one for a given relationship matters more than any specific syntax.

The phrase to repeat in your head is "is-a" vs "has-a". A Book is a Product, so inheritance fits. A Book has an Author, but a Book is not an Author, so the relationship should be a field, not inheritance:

The same Book uses both. It inherits from Product because every book really is a product on the store's shelves. It holds an Author because every book really has an author, but a book is not an author. Mixing the two relationships up (making Book inherit from Author to "reuse" the name fields, for example) produces a model that's wrong, and the wrongness shows up in the code that consumes it.

A small rule of thumb most experienced C# developers follow: prefer composition unless inheritance is clearly the right model. Composition is more flexible, because you can swap or rearrange components without changing the type hierarchy. Inheritance commits you to the relationship at compile time, and changing it later means changing every consumer of the type. When in doubt, hold an instance rather than inherit from it. You can always introduce inheritance later if the "is-a" relationship really does emerge, but ripping it out once it's in is painful.

A short table to keep them straight:

QuestionInheritanceComposition
Relationship"is-a""has-a"
CouplingTight (derived knows the base)Loose (holder uses the held object's public API)
FlexibilityLow (single base class, fixed at compile time)High (swap components freely)
PolymorphismYes (built-in)Yes (through interfaces on the field's type)
Reuse patternInherit existing fields and methodsDelegate to fields

We'll come back to this trade-off in the design patterns section, where several patterns (Decorator, Strategy) deliberately favor composition over inheritance. For now, the takeaway is: inheritance is a tool, not a default. Use it when one class genuinely is a kind of another, and stick with composition the rest of the time.

Putting It Together

The full set of ideas from this chapter, condensed:

  • A class can inherit from one other class by writing : BaseClass after its name.
  • The derived class inherits all members from the base class except constructors, finalizers, and static constructors.
  • Whether the derived class can use each inherited member depends on the access modifier: public and protected are visible, internal is visible within the same assembly, private is not.
  • protected is the access modifier designed for inheritance: visible to the declaring class and its descendants, hidden from everything else.
  • C# allows exactly one base class per class (single inheritance), but a class can implement any number of interfaces.
  • Every class that doesn't specify a base implicitly inherits from System.Object (alias object), which is the root of the type system.
  • Inheritance models "is-a" relationships. For "has-a" relationships, use composition (hold an instance as a field).

The pieces this chapter intentionally didn't cover:

  • How to call a base class method or constructor from a derived class with the base keyword. Covered in _base Keyword_.
  • Overriding base class methods using virtual and override. Covered in _Method Overriding (virtual/override)_.
  • Hiding (rather than overriding) base members with the new keyword. Covered in _new Keyword (Method Hiding)_.
  • Constructor chaining and how derived constructors invoke base constructors. Covered in _Constructor Chaining_.
  • Preventing further inheritance with sealed. Covered in _Sealed Classes & Methods_.
  • The System.Object methods (ToString, Equals, GetHashCode, GetType) in detail. Covered in _Object Class_.
  • Runtime type checks with is and as. Covered in _is & as Operators_.
  • Implementing Equals and GetHashCode correctly. Covered in _Equals() & GetHashCode()_.

Inheritance covered the "what" and "why" here. We've inherited members, but what if we want to call or extend a base class method from the derived class? That's what the base keyword is for.