Introduction
Inheritance is the mechanism by which one class (the Derived class) acquires the properties and behaviors of another (the Base class). This creates a hierarchy that mirrors the real world.
Deep Dive: Inheritance (Beyond the Definition)
The problem: The “copy-paste” trap
Imagine you are building a large game or a business system. You create a Rectangle class. Then you need a Square. Then a Triangle.
Without inheritance, you would find yourself copy-pasting the same code for Color, Position, and BorderWidth into every single class.
- The Danger If you decide to change how
colorworks, you have to find and fix it is 50 different files This is how bugs are born - The Solution : Inheritance allows us to define the “Shared DNA” in one place ( The Base Class) and let every other class “inherit” that DNA.
The “Genetic” Connection Think of inheritance as a Family Tree. In biology, you inherit traits from your parents (eye color, height), but you also have your own unique traits that your parents don’t have
In C#, this works exactly the same way:
-
The Base Class (The Parent): Contains the general logic that is common to everyone. It is the “Blueprint of a Blueprint.”
-
The Derived Class (The Child): It automatically “owns” everything the parent has, but it is allowed to specialize. It can add new features or even “re-define” how a parent’s trait works.
The “Is-A” Logic Gate To be a professional developer, you must use the “Is-A” Test. Inheritance creates a strict logical relationship. If you cannot say the phrase “[Child] is a [Parent]” and have it make sense, your inheritance is probably wrong.
-
Valid: A
PhysicsProfessoris-aTeacher. (Inheritance works!) -
Valid: A
SaveButtonis-aUIElement. (Inheritance works!) -
Invalid: A
Caris-aEngine. (No! A car has an engine, but it isn’t one. This is a different concept called Composition).
Why it matters for “The Big Picture”
Inheritance isn’t just about saving your fingers from typing. It is about Polymorphism (a fancy word for “Many Shapes”).
Because a Square is-a Polygon, the computer can treat them as the same thing when it needs to. You can tell a list of 1,000 different shapes to Draw(), and because they all inherited from the same Parent, they will all obey—even though a Square draws differently than a Circle.
“When you see class Square : Polygon, imagine that C# invisibly copies every line of code from Polygon and pastes it inside Square’s curly braces. You can’t see the code, but it is there, working and ready to use.”
Base and Derived Classes
In C#, inheritance isn’t just a concept; it’s a physical structure in your code. To understand it, you must understand the two roles a class can play.
The Base Class
A Base Class (also called a Parent Class or Superclass) is a standard class that serves as the “genetic donor.” It contains the properties and methods that are common to many different objects.
Example: The Polygon Every polygon has a number of sides. Instead of defining “sides” for every shape, we put it here:
class Polygon
{
// We use 'public' so the Child class can see and use these variables
public int numberOfSides;
// Default Constructor
public Polygon()
{
numberOfSides = 0;
Console.WriteLine("1. Polygon constructor finished!");
}
// Constructor with parameters
public Polygon(int sides)
{
numberOfSides = sides;
Console.WriteLine("1. Polygon constructor finished!");
}
}
The Derived Class
A Derived Class (also called a Subclass) is the class that “builds on top” of the base. It takes everything from the parent and then adds its own unique flavor.
The Syntax: The Colon (:)
In C#, the colon is the “Inheritance Operator.” It tells the compiler: “Take everything inside Polygon and put it inside Square too.”
// Square inherits from Polygon
class Square : Polygon
{
public float length;
public Square(float s) : base(side) // alias/pointer that refers immediate parent class.
{
length = s;
// Look! 'numberOfSides' is NOT defined in this class.
// But we can use it because we inherited it from Polygon.
numberOfSides = 4;
Console.WriteLine("2. Square constructor finished!");
}
public float GetArea()
{
return length * length;
}
public float GetPerimeter()
{
return length * numberOfSides;
}
}
When you write class Square : Polygon, C# takes everything from the Polygon class and invisibly pastes it inside the Square class.
In real life, you have two parents. In C# programming, a class can only have ONE base class.
class Square : PolygonPerfect.class Square : Polygon, ShapeError! (C# does not allow “Multiple Inheritance”).
Multilevel Inheritance While you can only have one parent, you can have a Grandparent. Inheritance can go many layers deep:
- Grandparent:
class Shape - Parent:
class Polygon : Shape - Child:
class Square : Polygon
In this case, the Square gets everything from Polygon AND everything from Shape. It is a cumulative process.
How it looks in the Main Program
When your students create a Square object, they can access variables from both classes as if they were one single unit.
Definition: Specific vs General
Specific Declaration
Square mySquare = new Square(5.5f); // using the child is better
This uses the Specific Remote Control. It gives you access to everything: the general properties of the parent (numberOfSides) AND the unique properties of the child (length, GetArea, GetPerimeter). Use this when you need to perform square-specific math.
General Declaration
Polygon mySquare = new Square(5.5f); // we dont' get prop and fun of square
This uses the General Remote Control. It treats the Square as a simple Polygon. You can see the numberOfSides, but you cannot see the length or call GetArea. Use this when you want to store different shapes (like Triangles and Squares) together in one list.
using System;
class Polygon
{
public int numberOfSides;
// Default Constructor
public Polygon()
{
numberOfSides = 0;
Console.WriteLine("1. Polygon Default Constructor finished!");
}
// Constructor with parameters
public Polygon(int sides)
{
numberOfSides = sides;
Console.WriteLine("1. Polygon Parameterized Constructor finished!");
}
}
class Square : Polygon
{
public float length;
// We pass '4' to the parent because every Square has 4 sides
public Square(float s) : base(4)
{
length = s;
// numberOfSides is inherited, so we can use it here too
Console.WriteLine("2. Square Constructor finished!");
}
public float GetArea()
{
return length * length;
}
public float GetPerimeter()
{
// Using the inherited numberOfSides property!
return length * numberOfSides;
}
}
class Program
{
static void Main()
{
Console.WriteLine("--- Creating a Specific Square ---");
// Using the Child type gives us access to Area and Perimeter
Square mySquare = new Square(5.5f);
Console.WriteLine($"Length: {mySquare.length}");
Console.WriteLine($"Sides: {mySquare.numberOfSides}");
Console.WriteLine($"Area: {mySquare.GetArea()}");
Console.WriteLine($"Perimeter: {mySquare.GetPerimeter()}");
Console.WriteLine("\n--- Creating a General Polygon ---");
// This works, but we can't call GetArea() here!
Polygon poly = new Square(10.0f);
Console.WriteLine($"Sides: {poly.numberOfSides}");
// Console.WriteLine(poly.GetArea()); // <-- This would cause an ERROR!
}
}
class Program
{
static void Main()
{
// 1. Create an array that can hold 3 Polygons
Polygon[] squareCollection = new Polygon[3];
// 2. Put different sized Squares into the Polygon array
squareCollection[0] = new Square(2.0f);
squareCollection[1] = new Square(5.5f);
squareCollection[2] = new Square(10.0f);
Console.WriteLine("--- Processing the Square Collection ---");
// 3. Loop through the array
for (int i = 0; i < squareCollection.Length; i++)
{
// We can access numberOfSides because it belongs to Polygon
Console.WriteLine($"Item {i}: This is a shape with {squareCollection[i].numberOfSides} sides.");
// NOTE FOR STUDENTS:
// Console.WriteLine(squareCollection[i].GetArea());
// The line above would fail! The 'Polygon' remote has no 'Area' button.
}
}
}
Deep Dive: Constructors & The Inheritance Bridge
Constructors are Not Inherited
While a Square inherits every variable and method from Polygon, it does not inherit its constructors.
The Reason: A constructor’s only job is to put an object into a “Valid Starting State.”
The Problem: A Polygon constructor only knows how to set up polygon things (like numberOfSides). It has no idea that a Square exists, let alone that it needs a Size or Length property initialized.
The “Invisible” Handshake (Implicit calls)
If you do not write a constructor in your Square class, or if you write one without the base keyword, C# does not just skip the parent. It automatically looks for the parent’s parameterless constructor: Polygon().
-
If
Polygon()exists: Everything works silently. -
If
Polygon()DOES NOT exist: (because you only made a constructor with parameters), the code will crash/fail to compile.
The base keyword Targeted Construction
To make our code cleaner, we use the base keyword. This allows the Square to “delegate” part of the work back to the Polygon.
The Syntax: The call to the base constructor happens after the parentheses but before the curly braces { }.
public Square(float size) : base(4) // This calls Polygon(4)
{
Size = size; // This sets Square's specific data
}
full Implementation Example
class Polygon
{
public int numberOfSides;
// The Parent takes care of the sides
public Polygon(int sides)
{
this.numberOfSides = sides;
Console.WriteLine($"Step 1: Polygon initialized with {sides} sides.");
}
}
class Square : Polygon
{
public float size;
// The Child takes care of the size, but TELLS the parent the sides
public Square(float s) : base(4)
{
this.size = s;
Console.WriteLine($"Step 2: Square initialized with size {s}.");
}
public float GetPerimeter() => size * numberOfSides;
}
No Redundancy: We don’t write numberOfSides = 4 inside the Square. We let the Polygon handle its own variables.
Strict Order: C# guarantees the Polygon foundation is built before the Square decorations are added.
Safety: By using : base(4), we ensure that a Square can never accidentally be created with 3 or 5 sides.
| Scenario | Code Written | What C# Does |
|---|---|---|
| No Constructor | Nothing | Calls base() (the empty one) automatically. |
| Explicit Call | : base(4) | Jumps directly to Polygon(int sides). |
| The Result | Execution | Parent code runs first $\rightarrow$ Child code runs second. |
The Protected Access Modifier
The three levels of security
-
public(The Front Yard): Anyone passing by on the street (any other class) can see and touch things here. -
private(The Safe): Only the person who owns the safe (the specific class) knows the code. Not even their children can open it. -
protected(The Living Room): Strangers on the street can’t come in, but family members (Derived Classes) are welcome to use everything inside.
Why use protected?
If we make numberOfSides private, our Square cannot use it to calculate perimeter. If we make it public, a “Stranger” class (like a CateringCompany class) could accidentally change a Square’s sides to 100, breaking our logic.
protected is the perfect middle ground. It allows the Square to use the variable while keeping it hidden from the rest of the program.
using System;
class Polygon
{
// PROTECTED: Square can see this, but the 'Program' class cannot.
protected int numberOfSides;
public Polygon(int sides)
{
numberOfSides = sides;
}
}
class Square : Polygon
{
public float size;
public Square(float s) : base(4)
{
size = s;
}
public float GetPerimeter()
{
// This works because Square is a "family member" (child) of Polygon
return size * numberOfSides;
}
}
class Program
{
static void Main()
{
Square mySquare = new Square(5.0f);
// SUCCESS: This works because GetPerimeter is public
Console.WriteLine($"Perimeter: {mySquare.GetPerimeter()}");
// ERROR: This line would fail to compile!
// Console.WriteLine(mySquare.numberOfSides);
// Why? Because 'Program' is not a child of Polygon. It's a "stranger."
}
}
class ClassA // The Parent
{
public int X = 10; // The "Universal" Variable
protected int Y = 20; // The "Family" Variable
private int Z = 30; // The "Secret" Variable
public void ShowZ() {
Console.WriteLine(X); // Only ClassA can touch X
Console.WriteLine(Y); // Only ClassA can touch Y
Console.WriteLine(Z); // Only ClassA can touch Z
}
}
class ClassB : ClassA // The Child
{
public void TestAccess()
{
Console.WriteLine(X); // ALLOWED (Public)
Console.WriteLine(Y); // ALLOWED (Protected - Inherited)
// Console.WriteLine(Z); // ERROR (Private - Parent only)
}
}
class Program // The Stranger (The Object User)
{
static void Main()
{
ClassB obj = new ClassB();
// --- Outside Perspective ---
Console.WriteLine(obj.X); // ALLOWED (Public)
// Console.WriteLine(obj.Y); // ERROR (Protected is hidden from strangers)
// Console.WriteLine(obj.Z); // ERROR (Private is hidden from strangers)
}
}
| Modifier | Inside the Same Class? | Inside a Derived Class? | Outside Classes? |
|---|---|---|---|
public | Yes | Yes | Yes |
protected | Yes | Yes | No |
private | Yes | No | No |
The Base Class of Everything: object
The Universal Parent
in C#, every class is a child of the object class. Even if you don’t write :Polygon , C# secretly write : Object for you
- The keyword:
objectWhich is just a nickname forSystem.Object - The Rule: a variable of type
objectcan hold anything - a Square, a Number, a string or a custom class.
What is “Inside” the Object Class? When you create a class, you get 4 main methods for free. Think of these as the 4 Default Buttons on every “Remote Control”:
| Method | What it does |
|---|---|
.ToString() | Converts the object to text. (Usually shows the class name unless you override it). |
.Equals(obj) | Checks if two objects are the same. |
.GetType() | Tells you exactly what the object is (e.g., “This is a Square”). |
.GetHashCode() | Gives a unique ID number to the object (used for sorting/searching). |
The “Box” Analogy (Boxing)
When you put a specific item into an object variable, you are “Boxing” it.
-
Specific Type (
Square s): You can see thesizeandGetArea(). -
Object Type (
object o): You can only see the 4 buttons above. Thesizeis hidden inside the box.
using System;
class Square
{
public float size = 5.0f;
// We override the built-in ToString to make it useful
public override string ToString() => $"Square with size {size}";
}
class Program
{
static void Main()
{
// One array to hold completely different types
object[] things = new object[3];
things[0] = new Square(); // A Class
things[1] = "Hello!"; // A String
things[2] = 100; // An Integer
foreach (object item in things)
{
// We can call ToString() because EVERY object has it!
Console.WriteLine(item.ToString());
}
}
}
object a = 10; // int
object b = "Hello"; // string
object c = 4.5f; // float
object d = new Square(5); // Your custom class
The Sealed Keyword: Closing the inheritance Line
What does sealed do?
When you mark a class as sealed, you are preventing any other class from inheriting from it. It becomes the Final Version of that class.
-
Inheritance stops here: If you try to create a class that derives from a sealed class, C# will give you a compiler error.
-
The Syntax: Put the word
sealedright before the wordclass.
sealed class Square : Polygon
{
// This class is now "Sealed."
// You can use it, but you cannot inherit from it.
}
// ERROR: This will not work!
// class ColoredSquare : Square { }
Why use sealed?
There are two main reasons to seal a class:
-
Security & Design: You want to make sure nobody “breaks” your logic by overriding methods or adding weird behavior to your specific class. It protects your code from being misused by other developers.
-
Performance Boost: Because the compiler knows no one will ever inherit from this class, it can make the code run slightly faster. It doesn’t have to look for “potential” child classes when calling methods.
Structs: The Naturally Sealed Types
In C#, Structs (like int, bool, or custom structs) are sealed by default.
-
You cannot inherit from a Struct.
-
A Struct cannot inherit from a Class.
-
This is why Structs are generally faster and simpler than Classes.
| Feature | Class (Standard) | Sealed Class | Struct |
|---|---|---|---|
| Can be Inherited? | Yes | No | No |
| Can Inherit others? | Yes | Yes | No |
| Performance | Standard | Faster | Fastest |
// add note for when declare the object should we use part or child as the type?