Subclassing an open Swift class is not always possible

Subclassing an open Swift class is not always possible

In this blog post, I am elaborating on why defining an open class in Swift with only an internal designated initializer is pointless. I am looking particularly at the use case of subclassing that class outside its module.

Designated Initializers vs. Convenience Initializers

Designated initializers are the primary initializers for a class. A designated initializer fully initializes all properties introduced by that class and calls an appropriate superclass initializer to continue the initialization process up the superclass chain.

Every class must have at least one designated initializer.

init(parameters) {
    statements
}

Convenience initializers are secondary, supporting initializers for a class. You can define a convenience initializer to call a designated initializer from the same class as the convenience initializer with some of the designated initializer’s parameters set to default values. You don’t have to provide convenience initializers if your class doesn’t require them.

convenience init(parameters) {
    statements
}

The Question

// Module "Experiment"

open class Superclass {
    internal init(param1: String, param2: String) {}

    public convenience init(params: [String]) {
        self.init(param1: "", param2: "")
    }
}

Can Superclass be subclassed outside of its module?

The short answer

No

The explanation

At a first glance, it may look like it should be possible because the class has open access level.

Open access applies only to classes and class members, which differs from public access by allowing code outside the module to subclass and override. Marking a class as open explicitly indicates that you’ve considered the impact of code from other modules using that class as a superclass and that you’ve designed your class’s code accordingly.

But the class has only a single designated initializer which has access level internal

internal enables entities to be used within any source file from their defining module, but not in any source file outside of that module. You typically use internal access when defining an app’s or a framework’s internal structure.

Source: Swift Language Guide - Access Control

So calling the designated initializer of the superclass will not work :(

import Experiment

public class Subclass: Superclass {}

func test() {
    // ERROR: Subclass' cannot be constructed because it has no accessible initializers
    let subclass = Subclass(param1: "", param2: "")
}

What about calling the public convenience initializer from the superclass?

import Experiment

public class Subclass: Superclass {}

func test() {
    // ERROR: Subclass' cannot be constructed because it has no accessible initializers
    let subclass = Subclass(params: [])
}

Nope :(

Swift subclasses don’t inherit their superclass initializers by default.

However, superclass initializers are automatically inherited if certain conditions are met.

Rule 1: If your subclass doesn’t define any designated initializers, it automatically inherits all of its superclass designated initializers.

Rule 1 probably means it automatically inherits all the public and open superclass's designated initializers my example has none.

Rule 2: If your subclass provides an implementation of all of its superclass designated initializers—either by inheriting them 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.

Source: Swift Language Guide - Initialization

Rule 2 doesn't matter here as well. We cannot provide an implementation of the superclass's designated initializer because it's internal.

Marina Gornostaeva asks the Swift team if the behavior is a bug or could be explained better through documentation. I am looking forward to their response.

What about adding a designated initializer in the subclass and calling the superclass's convenience initializer?

Not possible here because Swift requires that a designated initializer must call a designated initializer from its immediate superclass.

initializerDelegation01_2x.png

Conclussion

Question: Can you subclass an open Swift class outside of its module if that class has a single, internal designated initializer? Answer: No

You should add a public designated initializer to your open class as a module developer. Otherwise it is not possible to subclass it from outside of its module.

The alternative is to remove the open access level to make the intention clear that the class cannot be subclassed.

Did you find this article valuable?

Support Marco Eidinger by becoming a sponsor. Any amount is appreciated!