Inheritance

Inheritance

The Crash Course

  • Inheritance is a way to reuse classes by expanding them into more specific types of classes. For instance, a Square class can be derived (or inherited from) a Polygon class, giving it all of the features that the Polygon class has.
  • Any class can be used as the base class.
  • In another class, you use the colon followed by the class name to specify a base class. For example class Square : Polygon { /* … */ } .
  • The protected access modifier means that anything within the class can use it and anything in any derived class.
  • You can prevent a class from being derived from by using the sealed keyword: sealed class Square { /* … */ } makes the Square class unable to be inherited from.

Introduction

Imagine that you are trying to keep track of geometric polygons, like triangles, rectangles, squares, pentagons, and so on. You can probably easily imagine creating a Polygon class that has, say, the number of sides the polygon has and maybe an array of the positions of the vertices (corners) of the polygon.

Your Polygon class could be used to represent any of these polygons, but let's imagine that for a square, you want to be able to do some extra things with it. For instance, create one using only a size (since the width and height are the same lengths in a square), or possibly you want to be able to create a method that returns the area of the square.

One approach would be to create a Square class, which has everything the Polygon class has, plus the extra stuff you want. The problem with this is that now you have an entirely different class, and if you want to change how people work with polygons, then you'll also want to change how they work with squares. More than that, while you could create an array of polygons (Polygon[] polygons), you wouldn't be able to put squares in that list, even though they are polygons and should be able to be treated as polygons.

After all, a square is a polygon.

And in fact, that is getting right to the heart of what we're going to discuss. A square is a polygon, but more specifically, it is a special type of polygon that has its own special things. But a square can do anything a polygon can do, too, because a square is a polygon.

This is what programmers call an "is-a" relationship. It is so common that programming languages create constructs to facilitate this kind of thing.

In C#, the construct that lets us do this is called inheritance. Inheritance allows us to create a Polygon class and a Square class that is based on the Polygon class and reuse ("inherit") everything from the Polygon class that it is based on. This means any changes we make to the Polygon class will also be automatically changed in the Square class.

Base Classes

A base class (or superclass or parent class—all of these terms are all very commonly used, though we'll stick with "base class" in these tutorials) is any normal class (like any of the ones we've made before) that is used by another for inheritance. In our discussion, the Polygon class would be a base class.

So, for instance, we could have a class that looks like this:

class Polygon
{
    public int NumberOfSides { get; set; }
 
    public Polygon()
    {
        NumberOfSides = 0;
    }
 
    public Polygon(int numberOfSides)
    {
        NumberOfSides = numberOfSides;
    }
}

Derived Classes

A derived class (or subclass) is one that is based on another class. In our example, the Square class would be a derived class that comes from the Polygon class. We would say that the Square class is derived from the Polygon class.

You can create a derived class just like any other class with one small difference, which indicates what class is the base class.

While a "normal" class would be created like this:

class Square
{
    //...
}

To indicate that the Square class is derived from the Polygon class, we use the colon (:) and name the base class:

class Square : Polygon
{
    //...
}

Inside of the class, we simply indicate any new stuff that the Square class has that the Polygon class doesn't have. So our completed Square class might look like this:

class Square : Polygon
{
    public float Size { get; set; }
 
    public Square(float size)
    {
        Size = size;
        NumberOfSides = 4;        
    }
}

So now a square will have a Size property, but in addition, it inherits instance variables, properties, and methods that were in the Polygon class, so the Square class also has a NumberOfSides property that it inherits.

One other thing that I should mention here is that a class can inherit from a different class that inherits from yet another class. You can have as many layers as you want.

Using Derived Classes

There's one very important thing we need to discuss when it comes to using derived classes.

As you've probably guessed, you can create a Polygon object in your program, and you can also create a Square object in your program, exactly like we've seen before:

Polygon polygon = new Polygon(3); // a triangle
Square square = new Square(4.5f); // a square is a polygon with 4 sides of length 4.5.

But since a Square is a Polygon, you can also have a variable that has Polygon as its type but is actually storing a Square. That's fine because a Square is a polygon!

Polygon polygon = new Square(4.5f);

As the program is running, though, when we're using the polygon variable, we only have access to the stuff that a Polygon has. So, for example, we can check to see the NumberOfSides it has, but as the program is running, we can't know if a Polygon typed variable is keeping track of a Polygon or a Square or perhaps yet another type, like a Parallelogram or something. We just know that it is a Polygon, and so we can only work with the things a Polygon has.

Checking an Object's Type and Casting

As I just described, it is possible to be working with a derived type, like the Square, but only know that it is a Polygon. Sometimes, we need to be able to figure out what type an object is, rather than what the variable guarantees it is.

It is possible to check to see if an object is a specific type with the is keyword and casting:

Polygon polygon = new Square(4.5f);
 
if (polygon is Square)
{
    Square square = (Square)polygon;
    // We can now do what we want with the square.
}

If we don't check the type before casting, the cast can fail and cause our program to crash. It is wise to always check before casting.

The pattern above is extremely common. We check if something is a specific derived type and, if so, make a new variable of the derived type, cast the original thing to the derived type, and store it into that variable. This pattern is so common that C# has a shorthand way to do it all at once:

Polygon polygon = new Square(4.5f);
 
if (polygon is Square square)
{
    // We can now do what we want with the `square` variable here.
}

Using Inheritance in Arrays.

In light of what we've just been discussing, it is worth mentioning that you can create an array of the base class (for instance, Polygon[] lotsOfPolygons = new Polygon[5];) and put any derived class inside of it (lotsOfPolygons[2] = new Square(2.1f);).

For all intents and purposes, a derived class like Square can always function in the same capacity as its base class (Polygon).

Constructors and Inheritance

I mentioned earlier that all properties, instance variables, and methods are inherited in a derived class. Constructors, on the other hand, do not.

A constructor cannot be reasonably inherited. Remember that a constructor has the job of putting the object into a valid starting state. With inheritance, that means the whole object—the parts defined in the base class and the parts defined in the derived class. Constructors in the base class know nothing about how to initialize the parts added in a derived class and, therefore, cannot guarantee that the object is in a good starting state once inheritance is involved.

For example, the Polygon constructors could reasonably get a new Polygon instance into a good state, but it doesn't know anything about Square's Size property. So attempting to make a new Square instance by using one of Polygon's constructors is a dead end.

That means Square will simply need to define its own constructors. However, it would be unfortunate if you had to literally copy and paste the code from the Polygon constructors to reuse it in Square, wouldn't it? To address this issue, it is possible and required to call a base class constructor from every derived class constructor.

But we didn't do anything specific for this for Square, and it still worked, right? So what gives?

If you don't specify a constructor to build on (something we'll see how to do ourselves in a moment), then the compiler will assume that you are building on the base class's parameterless constructor (one without parameters—empty parentheses), assuming one exists. If one does not exist, then you must specify one yourself. Keep in mind that if a class has no explicitly written constructor, a parameterless one is automatically included by the compiler, and we can often leverage that here.

That is what has happened in this case. Our Square class has been implicitly configured to build upon Polygon's parameterless constructor.

In this case, however, it would make our life a bit easier if we could tell it to use the other constructor, which has a parameter for the number of sides, and takes care of setting up the NumberOfSides property. We can make our constructor build up on a specific constructor from the base class by using the base keyword:

class Square : Polygon
{
    public float Size { get; set; }
 
    public Square(float size) : base(4)
    {
        Size = size;
    }
}

This is placed after the constructor's parentheses but before its body, which is the only way you can call a constructor outside of the new keyword.

With this change, Square no longer needs to initialize the NumberOfSides property itself, as that is handled in the base class, Polygon.

The protected Access Modifier

In the past, we discussed the private and public access modifiers. To review, remember that public meant anyone could get access to the method or variable, while private means that you only have access to it from inside of the class that it belongs to.

With inheritance, we have another option: protected. If a variable, property, or method is marked with protected instead of private or public, then you'll only have access to it inside of the class or any derived class.

So, for instance, if the Polygon class made the NumberOfSides property protected instead of public, then you could access it from anywhere in the Polygon class, as well as the Square class, and any other derived class, but not from anything outside of those.

The Base Class of Everything: object

Without specifying any class to inherit from, any class you make still is derived from a special base class that is sort of the base class of everything. This is the object class.

Also, there's an object keyword that works a lot like the int or float types that we've seen before. Only the object keyword will always represent an object. (Like any class, it is a reference type rather than a value type.) But because of the properties of inheritance, it can represent any object.

So remember what I said earlier about how you can store a derived class in a base type like the code below?

Polygon polygon = new Square(4.5f);

You can also put it in the object type:

object anyOldObject = new Square(4.5f);

Since object is the base class of all classes, any type of object can be stored in it.

Additionally, the object class defines a few methods for you that every single object that you ever create will have. There is about four total, but one, in particular, that is worth looking at is the ToString() method. All objects have a ToString() method, which means you can call it any time you're working with an object.

Preventing a Class from Being Inherited

There may come a time when you want to prevent a class from being inherited. For instance, you want to ensure that no one derives anything from a specific class.

You can use the sealed keyword to make it so nothing can inherit from a specific class by adding it to your class definition:

sealed class Square : Polygon
{
    // ...
}

Sealing a class can result in a performance boost.

Structs are sealed by default, and cannot be unsealed, so inheritance with structs is impossible.

What's Next?

Inheritance provides a very powerful mechanism for organizing code and making classes more reusable. But it can also be a complicated issue, and in fact, we're more or less going to continue the inheritance discussion in the next tutorial, as we look at it from a different point of view and discuss abstract classes and polymorphism.