Swift Class Initializers & Inheritance: A Metaphor
Perhaps one of the most frustrating aspects of Swift is learning how and when to write class initializers. This is because one of the core mandates of the language— that every property of a class instance contain a value by the end of initialization — requires the compiler to be very strict about the entire step-by-step procedure of initialization. What I found most confusing at the beginning of my Swift studies (and I’m sure many others can empathize) were all of the seemingly arcane rules about when subclasses would or would not inherit initializers.
It’s gotten to the point where many in the community actively encourage learners to avoid creating initializers, and instead to employ crafty workarounds with default values, Optionals, and lazy variables. And while these methods are indeed useful, and many times preferable to the use of initializers, I’m the kind of person who feels that things aren’t quite right so long as I remain confused about a basic element of a larger topic. The fact is, everybody will have to use initializers at some point in their Swift programming lives. It’s better to understand what you’re doing than it is to rely on Xcode to fix the problem, or to forego initializers in situations where they genuinely are the best solution simply because they’re painful to wrap your head around.
When I have trouble grasping a seemingly difficult topic, what I like to do is find a real-world situation or circumstance to help me conceptualize it. To that end, I’ve come up with a metaphor encapsulating how class initializers work, specifically in the case of inheritance.
Before we dive into the metaphor itself, a brief introduction to the most basic case of class initialization is necessary. Here’s an example:
In the BaseHumanBeing class, we have two properties. Because those properties are set to default values, and there are no initializers explicitly declared within the class, we receive a free initializer, init(), which is invoked like so:
If we employ any of a number of techniques to avoid having to write our own initializers, init() is the one we’ll be using most of the time to create instances of our classes. It’s when we start adding custom initializers and writing subclasses that things get hairy. But I think you’ll see that it’s fairly simple to understand how initializers work in more complex scenarios by applying the following concept model to the process.
Class Initialization Is All About Growing Up
To fit in with the metaphor, let’s start by rethinking our BaseHumanBeing class more specifically as an Adult:
We can create an instance of the Adult class like so:
The only differences between this class and the BaseHumanBeing class shown earlier are that Adult has not given its properties default values, and has declared its own initializer. To be precise, Adult had to declare its own initializer because it didn’t employ any other means to provide its properties with default values. Moreover, because Adult declared its own initializer, it no longer receives the init() method that it would otherwise have gotten for free:
The Adult is now on its own in the world, and has to begin doing things for itself. The init(name:age:) method is Adult’s designated initializer, the primary means by which it can follow through on this mandate. But Adult can also have convenience initializers, customized shortcuts that ultimately call on init(name:age:), allowing the process of initialization to be carried out more, well, conveniently:
Adult now has a convenience initializer, which takes only a name parameter, and calls the designated initializer with a default age parameter in order to ensure that all properties of the Adult class are properly filled in. As expected, we now have a total of two ways to create an Adult instance:
The important thing to remember here is that “adult” classes (base or root classes which declare their own initializers) must do for themselves. They can’t rely on any sort of implied defaults like init() to carry the load (though they may redefine init() as a convenience initializer).
The Childhood Phase
For many, learning how initializers work in “adult” Swift classes is hard enough, but the real confusion seems to set in when inheritance enters the picture. Let’s set up an example now:
That was easy! And now let’s create Child instances using every initializer at our disposal:
As you can see, our Child — a completely blank subclass of Adult — not only inherits Adult’s properties, as expected, but every one of its initializers, as well. There were two ways to instantiate an Adult, and there are two ways to instantiate a Child. The important point here is that “child” subclasses (classes that don’t implement any of their own initializers) get everything done for them in the world of initialization.
This is, of course, analogous to how young children in the real-world are, in general terms, solely cared for by their parents. Young kids don’t take care of themselves because they lack the required knowledge, capability, and resources for doing so. But just as children take on more responsibility with age, subclasses take on more responsibility as they begin handling aspects of their own initialization.
The Adolescent Phase
Let’s simulate the “aging-up” of our subclass now:
Here we have a new Adolescent class, which again inherits from the Adult class. The main difference is that this class has updated its abilities and now contains its own convenience initializer. Let’s look at all the ways we can initialize Adolescent objects:
You’ll notice that Adolescent contains a total of three initializers: it’s own convenience initializer init(age:), and the two initializers from the Adult class. Just as most teenagers in the real world can take on new responsibilities without losing access to shelter, food, and other resources provided by their parents, a subclass can implement its own convenience initializers without losing access to initializers from the superclass.
Note: Inside init(age:), we call self.init(name:age:) instead of super.init(name:age:) because we have inherited Adult’s designated initializer. The compiler requires us to delegate to our own inherited version of the method.
Leaving the Nest
If you look closely at the above code, you’ll notice that our inherited initializers have produced strange results in a couple of cases, due to the fact that inappropriate defaults have been applied to our subclasses. Our Child and Adolescent instances should not, by definition, ever be “aged” 22. To spin out the metaphor, our subclasses have outgrown the parental doting of the Adult class. Nobody wants to be spoon-fed or told to clean her room after a certain age. So let’s model a subclass of Adult which has officially left the nest:
You can see that our NestLeaver class contains its own initializer, just like Adolescent did. Now let’s take a look at all the ways we can legally initialize one:
Huh? Adolescent had three ways to initialize, but NestLeaver only has one. What’s going on here?
Well, notice that, while our Adolescent class, which provided its own convenience initializer, retained access to all of the Adult class’s initializers, because NestLeaver provides a designated initializer, it unceremoniously loses access to all of Adult’s initializers. To tie in the metaphor: just as most grown-ups cannot readily expect ongoing assistance from their parents at the drop of a hat, a subclass which declares its own designated initializer cannot rely on access to any of its superclass’s initializers.
This is not so different, in the end, from our earlier case of BaseHumanBeing, which lost access to the default init() method after declaring its own designated initializer. In the world of Swift, when a class takes on more direct responsibility for initializing itself, the compiler holds it to that promise, like the strictest of parents.
Note 1: In init(name:age:job:), we call super.init(name:age:) because we no longer inherit init(name:age:) from Adult, and it therefore no longer exists on self.
Note 2: It’s worth pointing out that, if NestLeaver were to explicitly override Adult’s designated initializer, it would retain access to all of Adult’s convenience initializers. But, by striking off on its own and creating a unique designated initializer, NestLeaver has foregone that possibility, and must handle all initialization by itself.
I hope this article, and the metaphor contained within it, proved helpful in your understanding of class initialization and its intersection with inheritance in Swift. Sometimes, the more finicky aspects of a programming language (i.e. the ones that cause red dots to appear in Xcode) can seem impermeable. But one of the things I love about Swift is how, once I understand what’s really going on, those finicky parts usually start to feel a lot more reasonable and logical. The compiler is incredibly strict, but nevertheless it’s got a good block on its shoulders. You can’t say that about just any programming language.