Classes

A class can do everything that a structure can do except that a class can use something called inheritance. A class can inherit the functionality from another class and then extend or customize its behavior. Let's jump right into some code.

Firstly, let's define a class called Building that we can inherit from later:

Predictably, a class is defined using the class keyword instead of struct. Otherwise, a class looks extremely similar to a structure. However, we can also see one difference. With a structure, the initializer we created before would not be necessary because it would have been created for us. With classes, initializers are not automatically created unless all of the properties have default values.

Now let's look at how to inherit from this building class:

Here, we have created a new class called House that inherits from our Building class. This is denoted by the colon (:) followed by Building in the class declaration. Formally, we would say that House is a subclass of Building and Building is a superclass of House.

If we initialize a variable of the type House, we can then access both the properties of House and those of Building, as shown:

This is the beginning of what makes classes powerful. If we need to define ten different types of buildings, we don't have to add a separate squareFootage property to each one. This is true for properties as well as methods.

Beyond a simple superclass and subclass relationship, we can define an entire hierarchy of classes with subclasses of subclasses of subclasses, and so on. It is often helpful to think of a class hierarchy as an upside down tree:

Inheriting from another class

The trunk of the tree is the topmost superclass and each subclass is a separate branch off of that. The topmost superclass is commonly referred to as the base class as it forms the foundation for all the other classes.

Because of the hierarchical nature of classes, the rules for their initializers are more complex. The following additional rules are applied:

The second rule enables us to use self before calling the initializer. However, you cannot use self for any reason other than to initialize its properties.

You may have noticed the use of the keyword super in our house initializer. super is used to reference the current instance as if it were its superclass. This is how we call the superclass initializer. We will see more uses of super when we explore inheritance further later in the chapter.

Inheritance also creates four types of initializers shown here:

A required initializer is a type of initializer for superclasses. If you mark an initializer as required, it forces all of the subclasses to also define that initializer. For example, we could make the Building initializer required, as shown:

Then, if we implemented our own initializer in House, we would get an error like this:

This time, when declaring this initializer, we repeat the required keyword instead of using override:

This is an important tool when your superclass has multiple initializers that do different things. For example, you could have one initializer that creates an instance of your class from a data file and another one that sets its properties from code. Essentially, you have two paths for initialization and you can use the required initializers to make sure that all subclasses take both paths into account. A subclass should still be able to be initialized from both a file and in code. Marking both of the superclass initializers as required makes sure that this is the case.

To discuss designated initializers, we first have to talk about convenience initializers. The normal initializer that we started with is really called a designated initializer. This means that they are core ways to initialize the class. You can also create convenience initializers which, as the name suggests, are there for convenience and are not a core way to initialize the class.

All convenience initializers must call a designated initializer and they do not have the ability to manually initialize properties like a designated initializer does. For example, we can define a convenience initializer on our Building class that takes another building and makes a copy:

Now, as a convenience, you can create a new building using the properties from an existing building. The other rule about convenience initializers is that they cannot be used by a subclass. If you try to do that, you will get an error like this:

This is one of the main reasons that convenience initializers exist. Ideally, every class should only have one designated initializer. The fewer designated initializers you have, the easier it is to maintain your class hierarchy. This is because you will often add additional properties and other things that need to be initialized. Every time you add something like that, you will have to make sure that every designated initializer sets things up properly and consistently. Using a convenience initializer instead of a designated initializer ensures that everything is consistent because it must call a designated initializer that, in turn, is required to set everything up properly. Basically, you want to funnel all of your initialization through as few designated initializers as possible.

Generally, your designated initializer is the one with the most arguments, possibly with all of the possible arguments. In that way, you can call that from all of your other initializers and mark them as convenience initializers.

Just as with initializers, subclasses can override methods and computed properties. However, you have to be more careful with these. The compiler has fewer protections.

We have already talked about how classes are great for sharing functionality between a hierarchy of types. Another thing that makes classes powerful is that they allow code to interact with multiple types in a more general way. Any subclass can be used in code that treats it as if it were its superclass. For example, we might want to write a function that calculates the total square footage of an array of buildings. For this function, we don't care what specific type of building it is, we just need to have access to the squareFootage property that is defined in the superclass. We can define our function to take an array of buildings and the actual array can contain House instances:

Even though this function thinks we are dealing with classes of the type Building, the program will execute the House implementation of squareFootage. If we had also created an office subclass of Building, instances of that would also be included in the array as well with its own implementation.

We can also assign an instance of a subclass to a variable that is defined to be one of its superclasses:

This provides us with an even more powerful abstraction tool than the one we had when using structures. For example, let's consider a hypothetical class hierarchy of images. We might have a base class called Image with subclasses for the different types of encodings like JPGImage and PNGImage. It is great to have the subclasses so that we can cleanly support multiple types of images but, once the image is loaded, we no longer need to be concerned with the type of encoding the image is saved in. Every other class that wants to manipulate or display the image can do so with a well-defined image superclass; the encoding of the image has been abstracted away from the rest of the code. Not only does this create easier to understand code but it also makes maintenance much easier. If we need to add another image encoding like GIF, we can create another subclass and all the existing manipulation and display code can get GIF support with no changes to that code.

There are actually two different types of casting. So far, we have only seen the type of casting called upcasting. Predictably, the other type of casting is called downcasting.

Downcasting means that we treat a superclass as one of its subclasses.

While upcasting can be done implicitly by using it in a function declared to use its superclass or by assigning it to a variable with its superclass type, downcasting must be done explicitly. This is because upcasting cannot fail based on the nature of its inheritance, but downcasting can. You can always treat a subclass as its superclass but you cannot guarantee that a superclass is, in fact, one of its specific subclasses. You can only downcast an instance that is, in fact, an instance of that class or one of its subclasses.

We can force downcast by using the as! Operator, like this:

The as! operator has an exclamation point added to it because it is an operation that can fail. The exclamation point serves as a warning and ensures that you realize that it can fail. If the forced downcasting fails, for example, if someBuilding were not actually House, the program would crash as so:

A safer way to perform downcasting is using the as? operator in a special if statement called an optional binding. We will discuss this in detail in the next chapter, which concerns optionals but, for now, you can just remember the syntax:

This code prints out numberOfBathrooms in the building only if it is of the type House. The House constant is used as a temporary view of someBuilding with its type explicitly set to House. With this temporary view, you can access someBuilding as if it were House instead of just Building.