Swift Tutorial: Initialization In Depth, Part 2/2

Launch your Swift skills to the next level as you continue your study of initialization to cover class initialization, subclasses, and convenience initializers. By René Cacheaux.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Un-inheriting Initializers

Now that you’ve returned to a compiling version of Tank, uncomment the fuelTank and liquidOxygenTank instantiations from before:

let fuelTank = Tank(model: "Athena", serialNumber:"003", reusable: true)
let liquidOxygenTank = Tank(identifier: "LOX-012", reusable: true)

fuelTank and liquidOxygenTank no longer instantiate, so this code results in a compiler error. Recall that this compiled and ran fine before adding the designated initializer to Tank. What’s going on?

Both instantiations are attempting to create a Tank using initializers that belong to RocketComponent, which is Tank‘s superclass. The problem is that RocketComponent‘s initializers only know how to initialize the properties introduced by RocketComponent. These initializers have no visibility into properties introduced by subclasses. Therefore RocketComponent‘s initializers are incapable of fully initializing a Tank, even though Tank is a subclass.

This code would work fine in Objective-C because Tank‘s encasingMaterial property would be initialized to nil by the runtime before invoking any initializer. So in Objective-C, there’s no harm in Tank inheriting all of RocketComponent‘s initializers, because they are capable of fully initializing a Tank.

Even though automatic initializer inheritance is allowed in Objective-C, it’s clearly not ideal because Tank might assume that encasingMaterial is always set to a real string value. While this probably won’t cause a crash, it might cause unintended side effects. Swift does not allow this situation because it’s not safe, so as soon as you build a designated initializer for a subclass, the subclass stops inheriting all initializers, both designated and convenience.

In preparation for the next section, comment out the fuelTank and liquidOxygenTank instantiations again:

// let fuelTank = Tank(model: "Athena", serialNumber:"003", reusable: true)
// let liquidOxygenTank = Tank(identifier: "LOX-012", reusable: true)

The Initialization Funnel

To recap: convenience initializers delegate across to other convenience initializers until delegating to a designated initializer.

ClassInitDelegationUp

Designated initializers in subclasses must delegate up to a designated initializer from the immediate superclass.

ClassInitDelegationAcross

Keep in mind that a designated initializer in a subclass cannot delegate up to a superclass convenience initializer.

ConvenienceToConvenienceCannot

Also, a convenience initializer in a subclass cannot delegate up to a superclass convenience initializer. Convenience initializers can only delegate across the class in which they are defined.

DesignatedToConvenienceCannot

Each class in a class hierarchy either has:

  • no initializers
  • one or more designated initializer
  • one or more convenience initializer and one or more designated initializer

Consider the following class diagram:

ExampleClassHierarchy

Say you want to initialize an instance of class D using D’s designated initializer. Initialization will funnel up through C’s designated initializer and up to A’s designated initializer.

Or say you want to initialize an instance of E using E’s second convenience initializer. Initialization will move across to E’s first convenience initializer, then to E’s designated initializer. At that point, initialization will funnel up through D’s and then C’s designated initializers, and finally up to A’s designated initializer.

Class initialization begins moving across convenience initializers, if any, then proceeds to a designated initializer, then finally goes up through each designated initializer for each superclass.

Delegating Up the Funnel

Add the following LiquidTank subclass to the end of your playground:

class LiquidTank: Tank {
  let liquidType: String

  // Init #3a - Designated
  init(model: String, serialNumber: String, reusable: Bool,
      encasingMaterial: String, liquidType: String) {
    self.liquidType = liquidType
    super.init(model: model, serialNumber: serialNumber, reusable: reusable,
      encasingMaterial: encasingMaterial)
  }
}

You will use this class to trace initialization through the funnel. It has your standard subclass initializer: sets its own properties, and then calls the initializer on super.

Add the following two convenience initializers to LiquidTank:

// Init #3b - Convenience
convenience init(model: String, serialNumberInt: Int, reusable: Bool,
    encasingMaterial: String, liquidType: String) {
  let serialNumber = String(serialNumberInt)
  self.init(model: model, serialNumber: serialNumber, reusable: reusable,
    encasingMaterial: encasingMaterial, liquidType: liquidType)
}

// Init #3c - Convenience
convenience init(model: String, serialNumberInt: Int, reusable: Int,
  encasingMaterial: String, liquidType: String) {
    let reusable = reusable > 0
    self.init(model: model, serialNumberInt: serialNumberInt, reusable: reusable,
      encasingMaterial: encasingMaterial, liquidType: liquidType)
}

Now use LiquidTank‘s convenience initializer #3c to instantiate a new rocket propellant fuel tank:

let rp1Tank = LiquidTank(model: "Hermes", serialNumberInt: 5, reusable: 1,
  encasingMaterial: "Aluminum", liquidType: "LOX")

This initialization follows the following flow through the funnel of initializers: 3c > 3b > 3a > 2a > 1a. It first goes across LiquidTank‘s two convenience initializers; then delegation continues to LiquidTank‘s designated initializer. From there, initialization is delegated up to all the superclasses, Tank and RocketComponent. This also takes advantage of the same design decision you learned in Part 1 of this tutorial: two initializers can use the same set of parameter names and the runtime is smart enough to figure out which one to use based on the type of each argument. In this case, the initializer is called with an integer for reusable to indicate how many more times it can be launched, which means the compiler picks initializer #3c.

Re-inheriting Initializers

Remember that subclasses stop inheriting initializers once they define their own designated initializer. What if you want to be able to initialize a subclass using one of the superclass initializers?

Add the following designated initializer to Tank:

// Init #2b - Designated
override init(model: String, serialNumber: String, reusable: Bool) {
  self.encasingMaterial = "Aluminum"
  super.init(model: model, serialNumber: serialNumber, reusable: reusable)
}

This makes one of RocketComponent’s initializers available for instantiating Tank instances. When you want to use a superclass’s un-inherited designated initializer to instantiate a subclass, you override it just like in the example above.

Now add the following designated initializers to LiquidTank:

// Init #3d - Designated
override init(model: String, serialNumber: String, reusable: Bool) {
  self.liquidType = "LOX"
  super.init(model: model, serialNumber: serialNumber,
    reusable: reusable, encasingMaterial: "Aluminum")
}

// Init #3e - Designated
override init(model: String, serialNumber: String, reusable: Bool,
    encasingMaterial: String) {
  self.liquidType = "LOX"
  super.init(model: model, serialNumber: serialNumber, reusable:
    reusable, encasingMaterial: encasingMaterial)
}

This makes both RocketComponent‘s and Tank‘s designated initializers available for instantiating LiquidTank instances.

Now that these designated initializers are back in service, uncomment the fuelTank and liquidOxygenTank instantiations from before:

let fuelTank = Tank(model: "Athena", serialNumber:"003", reusable: true)
let liquidOxygenTank = Tank(identifier: "LOX-012", reusable: true)

The first line instantiates a Tank using the RocketComponent‘s overridden designated initializer. But take a look at the second line: it’s instantiating a Tank using RocketComponent‘s convenience initializer! You didn’t override it. This is how you get subclasses to inherit their superclasses’ convenience initializers.

Override all of a superclass’s set of designated initializers in order to re-inherit the superclass’s set of convenience initializers.

To go one step further, instantiate a LiquidTank using RocketComponent‘s convenience initializer at the bottom of your playground:

let loxTank = LiquidTank(identifier: "LOX-1", reusable: true)

This overrides all of Tank‘s designated initializers. This means that LiquidTank inherits all of RocketComponents‘s convenience initializers.