Learning Swift(Second Edition)
上QQ阅读APP看书,第一时间看更新

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.

Inheriting from another class

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

class Building {
    let squareFootage: Int

    init(squareFootage: Int) {
        self.squareFootage = squareFootage
    }
}
var aBuilding = Building(squareFootage: 1000)

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:

class House: Building {
    let numberOfBedrooms: Int
    let numberOfBathrooms: Double

    init(
        squareFootage: Int,
        numberOfBedrooms: Int,
        numberOfBathrooms: Double
        )
    {
        self.numberOfBedrooms = numberOfBedrooms
        self.numberOfBathrooms = numberOfBathrooms

        super.init(squareFootage: squareFootage)
    }
}

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:

var aHouse = House(
    squareFootage: 800,
    numberOfBedrooms: 2,
    numberOfBathrooms: 1
)
print(aHouse.squareFootage)
print(aHouse.numberOfBedrooms)

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:

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.

Initialization

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

  • All initializers in a subclass must call the initializer of its superclass
  • All properties of a subclass must be initialized before calling the superclass initializer

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:

  • Overriding initializer
  • Required initializer
  • Designated initializer
  • Convenience initializer
Overriding initializer

An overriding initializer is used to replace the initializer in a superclass:

class House: Building {
    let numberOfBedrooms: Int
    let numberOfBathrooms: Double

    override init(squareFootage: Int) {
        self.numberOfBedrooms = 0
        self.numberOfBathrooms = 0
        super.init(squareFootage: squareFootage)
    }
}

An initializer that takes only squareFootage as a parameter already exists in Building. This initializer replaces that initializer so if you try to initialize House using only squareFootage, this initializer will be called. It will then call the Building version of the initializer because we asked it to with the super.init call.

This ability is especially important if you want to initialize subclasses using their superclass initializer. By default, if you don't specify a new initializer in a subclass, it inherits all of the initializers from its superclass. However, as soon as you declare an initializer in a subclass, it hides all of the superclass initializers. By using an overriding initializer, you can expose the superclass version of the initializer again.

Required initializer

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:

class Building {
    let squareFootage: Int

    required init(squareFootage: Int) {
        self.squareFootage = squareFootage
    }
}

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

class House: Building {
    let numberOfBedrooms: Int
    let numberOfBathrooms: Double

    init(
        squareFootage: Int,
        numberOfBedrooms: Int,
        numberOfBathrooms: Double
        )
    {
        self.numberOfBedrooms = numberOfBedrooms
        self.numberOfBathrooms = numberOfBathrooms

        super.init(squareFootage: squareFootage)
    }

    // 'required' initializer 'init(squareFootage:)' must be
    // provided by subclass of 'Building'
}

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

required init(squareFootage: Int) {
    self.numberOfBedrooms = 0
    self.numberOfBathrooms = 0
    super.init(squareFootage: squareFootage)
}

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.

Designated and convenience initializers

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:

class Building {
    // ...

    convenience init(otherBuilding: Building) {
        self.init(squareFootage: otherBuilding.squareFootage)
    }
}
var aBuilding = Building(squareFootage: 1000)
var defaultBuilding = Building(otherBuilding: aBuilding)

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:

class House: Building {

    // ...

    init() {
        self.numberOfBedrooms = 0
        self.numberOfBathrooms = 0
        super.init() // Missing argument for parameter 'squareFootage' in call
    }
}

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.

Overriding methods and computed properties

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.

Methods

Even though it is possible, there is no requirement that an overriding method calls its superclass implementation. For example, let's add clean methods to our Building and House classes:

class Building {
    // ...

    func clean() {
        print(
            "Scrub \(self.squareFootage) square feet of floors"
        )
    }
}

class House: Building {
    // ...

    override func clean() {
        print("Make \(self.numberOfBedrooms) beds")
        print("Clean \(self.numberOfBathrooms) bathrooms")
    }
}

In our Building superclass, the only thing that we have to clean is the floors. However, in our House subclass, we also have to make the beds and clean the bathrooms. As it has been implemented above, when we call clean on House, it will not clean the floors because we overrode that behavior with the clean method on House. In this case, we also need to have our Building superclass do any necessary cleaning, so we must call the superclass version, as shown:

override func clean() {
    super.clean()

    print("Make \(self.numberOfBedrooms) beds")
    print("Clean \(self.numberOfBathrooms) bathrooms")
}

Now, before doing any cleaning based on the house definition, it will first clean based on the building definition. You can control the order in which things happen by changing the place in which you call the super version.

This is a great example of the need to override methods. We can provide common functionality in a superclass that can be extended in each of its subclasses instead of rewriting the same functionality in multiple classes.

Computed properties

It is also useful to override computed properties using the override keyword again:

class Building {
    // ...

    var estimatedEnergyCost: Int {
        return squareFootage / 10
    }
}

class House: Building {
    // ...

    override var estimatedEnergyCost: Int {
        return 100 + super.estimatedEnergyCost
    }
}

In our Building superclass, we have provided an estimate for energy costs based on $100 per 1000 square feet. That estimate still applies to the house but there are additional costs related to someone else living in the building. We must therefore override the estimatedEnergyCost computed property to return the Building calculation plus $100.

Again, using the super version of an overriding computed property is not required. A subclass could have a completely different implementation disregarding what is implemented in its superclass, or it could make use of its superclass implementation.

Casting

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:

func totalSquareFootageOfBuildings(buildings: [Building]) -> Int {
    var sum = 0
    for building in buildings {
        sum += building.squareFootage
    }
    return sum
}

var buildings = [
    House(squareFootage: 1000),
    Building(squareFootage: 1200),
    House(squareFootage: 900)
]
print(totalSquareFootageOfBuildings(buildings)) // 3100

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:

var someBuilding: Building = House(squareFootage: 1000)

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.

Upcasting

What we have seen so far is called upcasting because we are going up the class tree that we visualized earlier by treating a subclass as its superclass. Previously, we upcasted by assigning a subclass instance to a variable that was defined as its superclass. We could do the same thing using the as operator instead, like this:

var someBuilding2 = House(squareFootage: 1000) as Building

It is really personal preference as to which you should use.

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:

var house = someBuilding as! House
print(house.numberOfBathrooms)

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:

var anotherHouse = aBuilding as! House // Execution was interrupted

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:

if let house = someBuilding as? House {
    // someBuilding is of type House
    print(house.numberOfBathrooms)
}
else {
    print("someBuilding is not a house")
}

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.