Swift has a unique class inheritance system that is both powerful and elegant. Normally with other languages, class inheritance is easy to pick up and just a matter of syntax. However, Swift provides an elaborate set of inheritance concepts that doesn’t exist in many other languages. It is almost impossible to just “pick up” without reading the details in the documentation. So in this post, I’d like to shed light on all the inheritance goodies in Swift.
Getting Started
For due diligence, let’s look at a simple class inheritance example in Swift:
class Vehicle { var passengerCount = 0 var creationDate: NSDate init() { creationDate = NSDate() } } class Ship: Vehicle { var hasSails: Bool override init() { hasSails = false super.init() } }
Nothing earth-shattering here… typical class inheritance as any other language with different syntax. It has a constructor called “init”, base class implementations can have an “override”, and the base class can be called using the “super” keyword. However, notice that all properties have to have a value, whether it be in the property declaration or in the initializers (I’ll talk more about this in a bit).
If this is all there was to class inheritance in Swift, it would be sufficient. Swift doesn’t stop there though. There are several more inheritance concepts that Swift provides at your disposal.
Inheritance Lifecycle
First, let’s understand that the inheritance process in Swift is actually split up into two phases:
- In the first phase, all class properties in the inheritance chain are assigned an initial value. This can be done in the property declaration or class initializers.
- Once all properties have an initial value, the second phase begins. This phase gives each class an opportunity to update its properties or instance further.
The two-phase initialization prevents property values from being accessed before they are initialized, and prevents property values from being unintentionally overwritten.
Compiler Safety-Checks
Trying to keep track over which properties have been set to fulfill the phase at the right time would be daunting for large classes. So Swift’s compiler performs four helpful safety-checks to make sure that two-phase initialization is completed without error.
- A designated initializer (or constructor that calls a super constructor) must ensure that all of the properties introduced by its class are initialized before it delegates up to a superclass initializer.
- A designated initializer must delegate up to a superclass initializer before assigning a value to an inherited property. If it doesn’t, the new value the designated initializer assigns will be overwritten by the superclass as part of its own initialization.
- A convenience initializer (or constructor that calls one of its own constructors) must delegate to another initializer before assigning a value to any property. If it doesn’t, the new value can be overwritten by initializer it delegates to.
- An initializer cannot call any instance methods, read the values of any instance properties, or refer to self as a value until after the first phase of initialization is complete.
Designated and Convenience Initializers?
The terms “designated and convenience initializers” were briefly mentioned in the last section, but I’d like to go more in depth about what these kinds of constructors actually mean.
- Designated initializer are constructors that fully initializes all properties introduced by that class and calls a superclass initializer to continue the initialization process. Every class must have at least one designated initializer. If none are implemented, then “init()” is implicitly created.
- Convenience initializer call a designated initializer from the same class. This allows you to leverage other initializers in your class so you don’t have to set values for all the class properties again, but would like to extend or modify the properties or instance.
Here’s an example of the code in action:
class Ship: Vehicle { var hasSails: Bool override init() { hasSails = false super.init() } convenience init(passengerCount: Int) { self.init() self.passengerCount = passengerCount } }
In the code above, I’ve added a new initializer that accepts a parameter to update the “passengerCount” property during initialization. It immediately delegates across to another initializer (using self.init not super.init). It’s “convenient” because it effectively passes on the burden to another initializer for setting properties as required by one of the compiler safety-checks. The “convenience initializer” is not required to do anything, but allows you to extend existing initializers.
Automatic Initializers
A major difference between Swift inheritance and other languages is that initializers are NOT inherited by default. You have to explicitly override initializers in the subclass using the “override” keyword in front of the initializer. This may seem tedious, but there are certain situation where initializers are inherited automatically:
- If a subclass doesn’t define any designated initializers, it automatically inherits all of its superclass designated initializers.
- If a subclass provides an implementation of all of its superclass designated initializers—either by inheriting them as per the rule above, or by providing a custom implementation as part of its definition—then it automatically inherits all of the superclass convenience initializers as well.
Let’s look at an example. The subclass in the following example inherits all of its superclass initializers:
class Vehicle { var hasEngine = true var creationDate: NSDate init() { creationDate = NSDate() } init(hasEngine: Bool) { creationDate = NSDate() self.hasEngine = hasEngine } } class Ship: Vehicle { var hasSails = false } var myShip = Ship(hasEngine: false)
Notice there are no initializers in “Ship”. This allows all initializers from the superclass to be inherited automatically without having to explicitly override them.
In this next example, the initializers are not automatically inherited anymore:
class Vehicle { var hasEngine = true var creationDate: NSDate init() { creationDate = NSDate() } init(hasEngine: Bool) { creationDate = NSDate() self.hasEngine = hasEngine } } class Ship: Vehicle { var hasSails = false override init() { super.init() } } var myShip = Ship() // OK var myShip2 = Ship(hasEngine: false) // DOES NOT WORK!!
I’ve added an “init” initializer in the subclass. Now all other initializers are no longer inherited, so “Ship(hasEngine: false)” cannot be called and it will give a compile error.
However, adding only convenient initializers will not break the rule and will still allow all initializers from the superclass to be automatically inherited. The rules say that you must either have no or all designated initializers defined in your subclass for automatic initializer inheritance to kick in. Here’s an example where initializers from the subclass would still be inherited:
class Vehicle { var hasEngine = true var creationDate: NSDate init() { creationDate = NSDate() } init(hasEngine: Bool) { creationDate = NSDate() self.hasEngine = hasEngine } } class Ship: Vehicle { var hasSails = false convenience init(creationDate: NSDate) { self.init() self.creationDate = creationDate } } Ship(hasEngine: false)
I just added a “convenience initializer” to the “Ship” class. This still satisfies the automatic initializer inheritance rules and allows me to call the superclass initializer with the “hasEngine” parameter for the subclass initialization.
Failable Initializers
This is a very handy type of initializer. Sometimes one would like to return a “nil” instance if some conditions are not met. In other languages, I’d be forced to create a static instance factory, such as Vehicle.createInstance(), that would either return the instance or a null. This works, but Swift has a native way of handling this called failable initializers.
The syntax would be to add a question mark (?) after the initializer name to indicate that this particular initializer can return a “nil”:
class Ship: Vehicle { var hasSails = false init?(serialNumber: String?) { super.init() // Conditions must be met or fail instance guard let s = serialNumber where !s.isEmpty else { return nil } self.serialNumber = s } } var myShip = Ship(serialNumber: "") // Returns nil
In the above example, the initializer “init?(serialNumber: String?)” has a guard statement that checks whether serialNumber is nil or empty. If it is, it will return nil for the entire instance. Note that a failable initializer creates an optional value of the type it initializes.
Required Initializers
Required initializers indicate that every subclass of the class must implement that initializer. For example:
class Vehicle { var manufacturer = "" var creationDate: NSDate init() { creationDate = NSDate() } required init(manufacturer: String) { self.manufacturer = manufacturer creationDate = NSDate() } } class Ship: Vehicle { var hasSails: Bool override init() { hasSails = false super.init() } required init(manufacturer: String) { hasSails = false super.init(manufacturer: manufacturer) } }
The subclass is required to implement an initializer of “required init(manufacturer: String)”. Note that automatic initializer inheritance rules still apply, so if those rules pass, required initializers are also inherited as well and do not have to be explicitly implemented in those scenarios.
Deinitializers
Swift automatically deallocates your instances when they are no longer needed to free up resources. Usually you don’t need to do any manual clean-up when instances are deallocated. It would be useful if you could though, such as closing a file or caching a user’s state. Swift has you covered:
class Vehicle { deinit { print("Vehicle: Deinit complete.") } }
A deinitializer is called immediately before a class instance is deallocated. You cannot explicitly call a deinitializer yourself since the system calls it automatically. Deinitializers are also inherited by subclasses, which could also implement their own deinitializers (they would be called in sequence until it reaches the root superclass).
The Big Kahuna
There are so many rules and conventions in Swift inheritance, enough to make your head spin. So for the grand finale to this post, I’d like to include a massive code sample that illustrates just about all of the inheritance rules in one snippet with comments and a console output, so you can study it, plug it into the Swift playground, and see it play out:
import Foundation /* * Cache some reusable variables */ let formatter = NSDateFormatter() formatter.dateFormat = "yyyy/MM/dd" func someFunc(value: AnyObject) {} /* * Begin class inheritance */ class Vehicle { var name = "" var manufacturer = "" var model = "" var serialNumber = "" var photoURL = "" var maxSpeed = 0 var hasEngine = true var isAutonomous = false var passengerCount = 0 var active = true var comments = "" var creationDate: NSDate // Implicit if no other designated initializers init() { print("Vehicle: Init start.") // Safety check 1: Ensure all new properties initialized creationDate = NSDate() print("Vehicle.creationDate (\(creationDate)) assigned from init.") print("**All Vehicle properties initialized**") print("***PHASE 1 COMPLETE***") // Safety check 4: Cannot call any instance methods, // read the values of any instance properties, or // refer to self as a value until after the first // phase of initialization is complete print("Vehicle.getLocation() (\(getLocation())) called.") print("Vehicle: Init complete.") } required init(manufacturer: String) { self.manufacturer = manufacturer creationDate = NSDate() } deinit { print("Vehicle: Deinit complete.") } func getLocation() -> (latitude: Double, longitude: Double, altitude: Double) { return (0, 0, 0) } func getSpeed() -> Int { return 0 } } class Ship: Vehicle { var hasSails: Bool override init() { print("Ship: Init start.") // Safety check 1: Ensure all new properties initialized // before delegating up to superclass hasSails = false print("Ship.hasSails (\(hasSails)) assigned from init.") print("**All Ship properties initialized**") super.init() // Safety check 2: Delegate up to superclass // before assigning value to inherited properties creationDate = formatter.dateFromString("2000/01/01")! print("Ship.creationDate (\(creationDate)) re-assigned from init.") // Safety check 4: Cannot call any instance methods, // read the values of any instance properties, or // refer to self as a value until after the first // phase of initialization is complete print("Ship.getNearestDock() (\(getNearestDock())) called from init.") print("Ship.hasEngine (\(hasEngine)) called from init.") someFunc(self) print("Ship: Init complete.") } required init(manufacturer: String) { // Safety check 1: Ensure all new properties initialized // before delegating up to superclass hasSails = false super.init(manufacturer: manufacturer) } deinit { print("Ship: Deinit complete.") } init(hasEngine: Bool) { // Safety check 1: Ensure all new properties initialized // before delegating up to superclass hasSails = false super.init() // Safety check 2: Delegate up to superclass // before assigning value to inherited properties self.hasEngine = hasEngine } // Failable initializer init?(serialNumber: String?) { // Safety check 1: Ensure all new properties initialized // before delegating up to superclass hasSails = false super.init() // Conditions must be met or fail instance guard let s = serialNumber where !s.isEmpty else { // A failable initializer can trigger an initialization failure // only after all stored properties introduced have been set // and any initializer delegation has taken place return nil } // Safety check 2: Delegate up to superclass // before assigning values to inherited properties self.serialNumber = s } convenience init(passengerCount: Int) { print("Ship: Convenience init start.") self.init() // Safety check 3: Convenience initializer must delegate to // another initializer before assigning values to any properties self.passengerCount = passengerCount print("Ship.passengerCount re-assigned from convenience init.") print("Ship: Convenience init complete.") } func getNearestDock() -> (latitude: Double, longitude: Double) { return (0, 0) } } class Sailboat: Ship { // Automatic Initializer Inheritance // Rule 2: If subclass provides an implementation of all // of its superclass designated initializers—either by // inheriting them without defining any designated // initializers (as per rule 1), or by providing a custom // implementation as part of its definition—then it automatically // inherits all of the superclass convenience initializers override init() { super.init() hasSails = true } required init(manufacturer: String) { super.init(manufacturer: manufacturer) hasSails = false } // Uncomment so all designated initializers overriden // which will inherit convenience initializers /* override init?(serialNumber: String?) { super.init(serialNumber: serialNumber) } // Notice overriden designated initializer can be // implemented as a convenience initializer convenience override init(hasEngine: Bool) { self.init() // Safety check 3: Convenience initializer must delegate to // another initializer before assigning values to any properties self.hasEngine = hasEngine } */ } class Submarine: Ship { // Automatic Initializer Inheritance // Rule 1: If subclass doesn’t define any designated initializers, // it automatically inherits all superclass designated initializers convenience init(active: Bool) { self.init() // Safety check 3: Convenience initializer must delegate to // another initializer before assigning values to any properties self.active = active } } /* * Sample instances */ var myBoat = Ship(passengerCount: 50) var mySailBoat = Sailboat() // Only available if all designated initializers overriden //var mySailBoat2 = Sailboat(hasSails: true) var mySubmarine = Submarine() var mySubmarine2 = Submarine(passengerCount: 100) var mySubmarine3 = Submarine(hasEngine: true) var myNilShip = Ship(serialNumber: "") // Should return nil var myBoat2 = Ship(serialNumber: "12345678") myBoat2 = nil // Deallocate will call deinit
Here is the console output to the first initialization, `Ship(passengerCount: 50)`. This way, you can trace the sequence of events:
Ship: Convenience init start. Ship: Init start. Ship.hasSails (false) assigned from init. **All Ship properties initialized** Vehicle: Init start. Vehicle.creationDate (2016-02-04 15:54:03 +0000) assigned from init. **All Vehicle properties initialized** ***PHASE 1 COMPLETE*** Vehicle.getLocation() ((0.0, 0.0, 0.0)) called. Vehicle: Init complete. Ship.creationDate (2000-01-01 05:00:00 +0000) re-assigned from init. Ship.getNearestDock() ((0.0, 0.0)) called from init. Ship.hasEngine (true) called from init. Ship: Init complete. Ship.passengerCount re-assigned from convenience init. Ship: Convenience init complete.
Conclusion
Object-oriented programming has been the cornerstone for software architecture for decades. Not much has changed in those years, but Swift proved there is still room to introduce unique concepts to class inheritance. Not only are these Swift inheritance tools powerful, but they also enforce safe and intentful code. As a bonus, if you do not follow the inheritance rules, then most of the time the compiler will give you an error or warning right away.
HAPPY CODING!!
[…] Swift has a very well thought-out initializer system in place. With options such as designated and convenience initializers, one must ensure all properties have values since the compiler will make sure of it. Take a look at my other post for more details. […]