Classes Revisited Again: Initializers - OOP REVISITED - Understanding Swift Programming: Swift 2 (2015)

Understanding Swift Programming: Swift 2 (2015)

PART 4: OOP REVISITED

29. Classes Revisited Again: Initializers

An earlier chapter (Chapter 12 on "Classes, Objects, and Inheritance") described how an instance of a class, that is, an object, is created. When creating a new instance of a class, all variables and constants defined in that class must be assigned an initial value.

(An exception is optionals, which will be set to nil if not otherwise initialized. It's still a good idea, however, to also set initial values to optionals, for good code readability.)

A property that has its initial value set when it is declared has a default property value. Apple recommends doing it this way if the value is always the same for any instantiation. This approach is generally simpler and cleaner.

Thus, if we have a simple class Fruit, the following will define the class, declare the property skinColor, and set an initial value for skinColor:

class Fruit {

var skinColor = "red"

}

If we then create an instance of the class Fruit, the initial value will be set to "red".

let pieceOfFruit = Fruit()

print(pieceOfFruit.skinColor) // Prints: red

Initializers

In the examples shown in the earlier chapter variables and constants were usually initialized this way. In one case, we used a simple version of an initializer, a method-like piece of code in a class that begins with init() and is executed automatically as part of creating a new instance of a class. This looks like the following:

class Fruit {

var skinColor: String

init () {

self.skinColor = "red" // Using self is preferred but not required until there is ambiguity

}

}

A new instance of the class Fruit is created, setting the initial value of the property skinColor to "red". This does the same thing as the previous code that set the value in the declaration of the variable. The Swift compiler will enforce the requirement that an initial value be set, but it is just as happy to have it done by an initializer as in a property declaration.

However, when an initializer is used, the property declaration must have its type set by a type annotation. The compiler cannot infer the type if the value is set in an initializer.

In the above example the property skinColor is referred to by self.skinColor, with self referring to the current class. Just referring to skinColor itself, without the self, will work. Using self, however, is considered a better practice because there are situations in which there is ambiguity and using self is a good habit.

The initializer can appear anywhere in the class definition, but it is convenient to place it between the declarations of properties and the definitions of methods.

In this chapter we will consider initializers in much more detail. They can get quite complicated, especially when a class inherits from one or more superclasses, since inherited properties must also be initialized.

Designated Initializers

The initializer described above is known as a designated initializer. Unless initialization is only done by setting initial values in the same statement that declares a property, each class must have at least one designated initializer.

A designated initializer has the keyword init along with (optional) initial parameters and some code. It is not actually a method, but looks and acts very much like an instance method. There are some differences: The keyword func is not used, and initializers do not return a value. A designated initializer is called automatically when a new instance of a class is created. You cannot call a designated initializer in ordinary code, although you can call one from another designated initializer or from another type of initializer that we will discuss a bit later, known as a convenience initializer.

The initializers described in this chapter work only for classes. A similar process happens for structures, but does not work quite the same way. Enumerations are initialized in still a different way, with each enumeration member having its own initializer, or set of initializers, included in the case clause for that member. See the chapters on structures and enumerations for details.

We can used a designated initializer to allow the statement creating a new object to pass values that are set to properties of the class. This is shown below:

class Fruit {

var skinColor: String

var fleshColor = "white"

init(skinColor: String) {

self.skinColor = skinColor

}

}

If a class is defined as above, a new instance of the class can be created with the following:

let aFruit = Fruit(skinColor: "green")

print(aFruit.skinColor) // Prints: "green"

print(aFruit.fleshColor) // Prints: "white"

This is a little confusing because we are using the same name for the argument that we used for the property. However, this is the usual practice, and the use of self.skinColor allows us to reference the property, while skinColor itself refers to the argument passed in the initializer.

This sets the value of the property skinColor to the value indicated in the statement creating the new instance, by executing the initializer and passing it the skinColor parameter set to the value green.

We can also define a class that gives us the ability to either create a new instance with a default value for skinColor or to set it specifically by the statement creating the instance. The class definition is:

class Fruit {

var skinColor: String

init() {

self.skinColor = "red"

}

init(skinColor:String) {

self.skinColor = skinColor

}

}

There are now two initializers included in the class definition. One has no input parameters, the other has a single input parameter labeled skinColor of type String.

If we want to just create a new instance and execute the designated initializer with no input arguments, we do the following:

var aFruit = Fruit()

This will create an instance of the class Fruit and set its skinColor property to "red".

Alternatively, we can create a new instance and execute the designated initializer that allows us to pass a "green" value for the parameter skinColor:

var aFruit = Fruit(skinColor: "green")

We did this earlier; the only difference is that our class definition now has both kinds of initializers in it, and the system had to determine which initializer to execute. This of course uses what is effectively function overloading, matching the type(s) of input parameters of the initializer to allow calling the correct one. (This might be called "initializer overloading.")

A more realistic Fruit class, as we saw in earlier chapters, has more properties. An example is this:

class Fruit {

var countryGrownIn: String

var skinColor: String

var fleshColor: String

var hasSeeds: Bool?

var weightLbs: Double

init (countryGrownIn: String, skinColor: String, fleshColor: String, hasSeeds: Bool, weightLbs: Double) {

self.countryGrownIn = "United States"

self.skinColor = skinColor

self.fleshColor = fleshColor

self.hasSeeds = hasSeeds

self.weightLbs = weightLbs

}

}

To create an instance of Fruit:

aFruit = Fruit(countryGrownIn: "United States", skinColor: "green", fleshColor: "white", hasSeeds: true, weightLbs: 0.6)

While this gives us a lot of control over the setting of the properties of an instance, we usually don't need such a detailed level of control and would often prefer something easier. That's what convenience initializers are for.

Convenience Initializers

A convenience initializer simplifies code by defining default values for many or most of the properties of the class before calling a designated initializer. A convenience initializer is optional, and it must call another initializer (convenience or designated) in the same class. Eventually, a designated initializer in that class must be executed.

Initializers in classes cannot be called by ordinary code. However, convenience initializers can call designated initializers, and, as we will see later, designated initializers can call designated initializers in superclasses.

Convenience initializers, like designated initializers, are never called directly except by other initializers, and use the keyword init. A convenience initializer has same syntax as a designated initializer, except that it has the keyword convenience just before the init keyword. If there is more than one conveneience initializer defined in the class, a particular convenience initializer is called as a result of matching the parameter names and types in the statement creating an instance of a class.

The class Fruit defined below shows how a convenience initializer is used. This is like the class Fruit we saw before, except that it has five different properties rather than just one:

class Fruit {

var countryGrownIn:String

var skinColor:String

var fleshColor:String

var hasSeeds:Bool

var weightLbs: Double

init(countryGrownIn: String, skinColor: String, fleshColor: String, hasSeeds: Bool, weightLbs: Double) {

self.countryGrownIn = countryGrownIn

self.skinColor = skinColor

self.fleshColor = fleshColor

self.hasSeeds = hasSeeds

self.weightLbs = weightLbs

}

convenience init(skinColor: String, weightLbs: Double) {

self.init(countryGrownIn: "United States", skinColor: skinColor, fleshColor: "white", hasSeeds: true, weightLbs: weightLbs)

}

}

Let's say that for our particular application, most of the time three of the five properties defined in the class are normally the same: "United States" for countryGrownIn, "white" for fleshColor, and true for hasSeeds. The properties that are typically different for different pieces of fruit are skinColor (which might be "red", "green", "yellow", and others) and weightLbs, the weight in pounds. We create a designated initializer that initializes all of the properties in the class. But most of the time, when we create a new instance, we won't define the values of all five properties in our statement that creates the new object. Instead, we'll create a new instance with just two parameters, as shown below:

var aPieceOfFruit = Fruit(skinColor: "green", weightLbs: 0.54)

This creates a new instance but does not call the designated initializer because the parameter names and types do not match. Instead, the initializer that matches the parameter names and types is called, which turns out to be the convenience initializer. That convenience initializer, in turn, calls the designated initializer. In doing so, it passes along the values for skinColor ("green") and weightLbs (0.54), and then specifies default values for the other three properties: countryGrownIn, fleshColor and hasSeeds.

The mixture of using a designated initializer and a convenience initializer allows the programmer full control to in setting the properties of a new instance when creating it when desired, but makes it easy to create a new instance in the more common cases when only one or two parameters need to be set to something other than a default value.

Some General Rules About Initializers

One exception to the rule that variables must be provided with an initial value is in the case of optional types. Optionals may be initialized, but they are not required to be. If they are not initialized they will be assigned a value of nil, that is, no value.

Changes to properties made by initializers do not trigger the execution of property observers.

Constants, which normally can be assigned a value only once, can be changed by initializers, even if they have already been assigned a default value.

Initialization and Inheritance

When a class is part of an inheritance hierarchy, Swift has mechanisms and rules that insure that all properties that are defined in that inheritance hierarchy are set to initial values.

In the case of a class that inherits from one or more superclasses, it may be necessary for that class to provide an initializer that makes a call to the initializer of its immediate superclass, and perhaps to modify the values of properties that it has inherited as well as set the initial values of the properties that it is defined in its own class.

The programmer has a choice here. If the stored properties that are defined in a new subclass are set to initial values as part of their declarations, and if the subclass has no initializers, then the subclass will inherit initializers from its superclass(es) and the programmer does not have to do anything further.

The other alternative, the more common case when the new subclass has one or more initializers, requires that a call be made to an initializer in the subclass’s immediate superclass. If there are further superclasses, this will trigger a chain of calls to initializers that eventually reaches the highest level in the hierarchy, the base class.

This is done in two phases, to insure that a class in the hierarchy does not improperly overwrite properties that were initialized by another class.

The phases are:

Phase One. In this phase, successive calls are made to initializers in every class in the hierarchy that defines one or more stored properties. This begins at the level of the new subclass, which initializes its stored properties and than makes a call to an initializer of its immediate superclass. That initializer will call an initializer of its immediate superclass, continuing until it reaches the topmost level. A designated initializer in every class in the hierarchy must be executed. (Convenience initializers can be called, but they must, directly or indirectly, execute a designated initializer in the same class.) The initializer must initialize all properties that have been introduced by its class (that have not been initialized in their declaration) before calling the superclass.

Phase Two. In this phase, the initializer for the new class can modify the values of any inherited properties, which already have had initial values set by an initializer in a superclass.

What this means for a designated initializer in a new subclass is that it has code that does three things, in the following order:

First, it sets initial values for all properties that do not have initial values set as part of their declaration. (Optionals do not have to be set since they will be set to nil automatically.)

Second, it makes a call to an initializer in its immediate superclass.

Third, if desired, it modifies the values of properties that have been inherited.

If you do not do this in the order indicated, the compiler will complain.

We can see an example of this with a pair of classes, Fruit and Banana:

class Fruit {

var skinColor:String

var countryGrownIn: String

init() {

self.skinColor = "green"

self.countryGrownIn = "United States"

}

}

class Banana: Fruit {

var weightInLbs: Double

init(weightInLbs: Double) {

self.weightInLbs = weightInLbs

super.init()

self.skinColor = "yellow"

print(weightInLbs) // Prints: 0.42

print(self.skinColor) // Prints: yellow

print(self.countryGrownIn) // Prints: United States

}

}

let aBanana = Banana(weightInLbs: 0.42)

We create an instance of the class Banana, providing the weight of our particular banana, 0.42 pounds. This causes execution of the designated initializer for the Banana class and passes it the value of 0.42.

The initializer follows the defined sequence. First, it initializes the one property that was newly defined in the Banana class: weightInLbs. Next, it makes a call to its superclass, Fruit. The superclass executes its initializer, setting the default values to its properties skinColor andcountryGrownIn.

If there was an additional superclass in the hierarchy, its initializer would be called here, but since there is not, the superclass initializer is done.

The instance of the Banana class has inherited two properties from Fruit: skinColor and countryGrownIn. The Banana class initializes skinColor to "yellow" (changing its value from the value set by the superclass). It does not change the value of the other inherited property,countryGrownIn.

We can see the values that get printed by the Banana initializer when it is done. The class has introduced a single new property, weightInLbs, and set it as part of the call creating the new instance. It has inherited a property, skinColor, and changed its value. And it has inherited another property, countryGrownIn, and kept its value.

Overriding Initializers. It is possible to override an inherited initializer. In the case of a designated initializer, the keyword init is preceded by the keyword override. In the case of a convenience initializer, it simply uses the normal syntax, without the override keyword. A convenience initializer that is overriding an inherited initializer must use the same names and types that were used in the initializer it is overriding.

Required Initializers. If an initializer is marked with the keyword required, it will be inherited in any subclasses. It is relatively rare for initializers to be inherited, and this keyword ensures that particular initializers will in fact be inherited.

Deinitialization and Inheritance

Deinitializers are inherited, if they exist, by subclasses. When an object is being destroyed, a deinitializer in its class will be called first if it exists. Then any existing deinitializers from superclasses will be called.

Failable Initializers

It is possible for an initializer to fail in certain circumstances. In cases where an initializer might fail, there must be code that determines whether the attempt to create the new instance has succeeded or failed and that takes appropriate action.

Such “failable initializers” can be used in classes, structures, and enumerations.

If an initializer can fail, it should contain an initializer using the init keyword that contains code to deal with the failure. The keyword init in such a situation should be followed by a question mark.

Why would an attempt to create an instance fail? There are several possibilities.

Some instances require certain resources that, at a given time, might not be available. For example, an instance might be an image (e.g., a Cocoa Touch UIImage type) that must be read from the device’s file system, or downloaded from a remote server. In either case, that image might not be available, for whatever reason.

Other instances might be created using parameters supplied by the statement that calls for the instance to be created. It may be desirable to check these values to see if they are valid. If one or more is not valid, the instance cannot be created, and the initializer must fail.

The following class definition defines a class named Apple that allows instances to be created that sets the values of certain properties when it is created:

class Apple {

var skinColor: String = "red"

var weightInLbs: Double = 0.5

init? (skinColor: String, weightInLbs: Double) {

guard (skinColor == "red" || skinColor == "yellow" || skinColor == "green") else { return nil }

self.skinColor = skinColor

guard weightInLbs <= 3.0 else { return nil }

self.weightInLbs = weightInLbs

}

}

An instance of Apple is created as follows:

let anApple = Apple(skinColor: "red", weightInLbs: 0.8)

This will successfully create an instance of the class Apple that has a skin color of “red” and a weight (in pounds) of 0.8.

However, the initializer has some validity checks when creating an instance. Only apples with a skin color of “red”, “yellow”, or ‘green” are allowed to be created. In addition, the weight of the apple must be no more than 3 pounds.

If we attempt to create a purple apple, it will fail:

let anApple = Apple(skinColor: "purple", weightInLbs: 0.8)

print(anApple) // prints: nil

As part of the validity check in the initializer code, if the validity check fails the statement return nil is executed. This causes an exit from the initializer. Note that initializers do not actually return a value, and so this statement of return nil is not actually an attempt to return a value: it is just a signal to indicate that the initializer has failed. However, the constant anApple will be set to nil in this case. In this very specific situation, the initializer effectively “returns” a value. (If the initializer succeeds, no return statement is executed.)

Hands-On Exercises

Go to the following web address with a Macintosh or Windows PC to do the Hands-On Exercises.

For Chapter 29 exercises, go to

understandingswiftprogramming.com/29