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

Classes

A class can do everything that a structure can; 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 straight to some code.

Inheriting from another class

First, 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 here. With a structure, the initializer we created earlier would not be necessary because it would be created for us. With classes, initializers are not automatically created unless all 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 created a new class called House that inherits from our Building class. This is signified by the colon (:) followed by Building in the class declaration. Formally, we would say that House is a subclass of Building and Building is the superclass of House.

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

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

This is the beginning of what makes classes powerful. If we need to define 10 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 and so on. It is often helpful to think of a class hierarchy as an upside down tree, as shown in the following figure:

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.

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 is why we are able to use self before calling the initializer. You cannot use self for any other reason than to initialize its properties.

You may have noticed the use of the keyword super in our House initializer. The super keyword 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 as we explore inheritance further.

Inheritance also creates four types of initializers, which are:

  • Overriding initializers
  • Required initializers
  • Designated initializers
  • Convenience initializers
Overriding initializers

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

class House: Building {
    //...

    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 maintain the ability to initialize subclasses using their superclass' initializer. By default, if you don't specify any new initializer in a subclass, it will inherit all the initializers from its superclass. However, as soon as you declare an initializer in a subclass, it hides all the superclass' initializers. Using an overriding initializer, you can expose the superclass's version of the initializer again.

Required initializers

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

class Building {
    //...

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

Then, if we only implemented our own initializer in House, we would get an error, as follows:

class House: Building {
    //...

    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, while declaring this initializer, we would 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 for circumstances where 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 required initializers to make sure that all the subclasses take both paths into account. A subclass should still have the ability to be initialized from both a file and from within code. Marking both of the superclass' initializers as required makes sure that is the case.

Designated and convenience initializers

When talking about designated initializers, we have to talk about convenience initializers at the same time. The normal initializer that we started with is really called a designated initializer. This means that there 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 can. For example, we can define a convenience initializer on our Building class that takes another building and makes a copy of it:

class Building {
    // ...

    convenience init(otherBuilding: Building) {
        self.init(squareFootage: otherBuilding.squareFootage)
    }
}
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 this, you will get an error:

class House: Building {

    // ...

    init() {
        self.numberOfBedrooms = 0
        self.numberOfBathrooms = 0
        super.init (otherBuilding: Building (squareFootage: 1000))
        //Must call a designated initializer of the superclass 'Building'
        }
}

This is one of the core 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 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 the initializer must call a designated initializer, which in turn is required to set everything up properly. Basically, you want to funnel all your initialization through as few designated initializers as possible.

Generally, your designated initializer will be the one with the most arguments, possibly with all the possible arguments. This way, you can call the initializer from all your other initializers and mark them as convenience initializers.

Overriding methods and computed properties

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

Methods

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

class Building {
    // ...

    func clean() {
        println("Scrub \(self.squareFootage) sqfeet of floors")
    }
}

class House: Building {
    // ...

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

In our Building superclass, the only thing we have to clean are the floors. However, in our House subclass, we also have to make the beds and clean the bathrooms. As it is implemented in the preceding code, 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 want our Building superclass to do any necessary cleaning, so we must call the superclass version:

override func clean() {
    super.clean()

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

Now, before a house does any cleaning, it will first clean a building. You can control the order in which things happen by moving where you call the super version.

This is a great example of why we would want 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 can also be 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, you are provided with an estimate for energy costs based on $100 per 1000 square ft. This estimate still applies to a house, but there are additional costs related to someone living in the building. To achieve the additional costs, we override the estimatedEnergyCost computed property to return the building's calculation plus $100.

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

Casting

We already discussed that classes are great for sharing functionality between the 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 may 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)
]
println(totalSquareFootageOfBuildings(buildings)) // 3100

Even though this function thinks that we deal with classes of the type Building, the program will execute the House class's implementation of squareFootage. If we also created an office subclass of Building, instances of this could also be included in the array with its own implementation.

On top of that we assign an instance of a subclass to a variable, which 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 we have while using structures. For example, let's consider a hypothetical class hierarchy of images. We can have a base class called Image with subclasses for the different types of encodings such as JPGImage and PNGImage. It's 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 easy-to-understand code, but it also makes maintenance much easier. If we need to add another image encoding, such as GIF, we can create another subclass and all the existing manipulation and display code can gain the GIF support with zero changes in that code.

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

Upcasting

What we have seen so far is considered upcasting because we go up the class tree that we visualized earlier by treating a subclass as its superclass. Previously, we used upcasting by assigning a subclass instance to a variable that was defined as its superclass. We could do the same using the as an operator instead:

var someBuilding2 = House(squareFootage: 1000) as Building

It is really a personal preference as to which way you prefer.

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 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 using the as! operator:

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

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

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

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

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

This code will print 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.