| :orphan: |
| |
| Initializer Inheritance |
| ======================= |
| |
| :Authors: Doug Gregor, John McCall |
| |
| .. contents:: |
| |
| Introduction |
| ------------ |
| This proposal introduces the notion of initializer inheritance into |
| the Swift initialization model. The intent is to more closely model |
| Objective-C's initializer inheritance model while maintaining memory |
| safety. |
| |
| Background |
| ---------- |
| An initializer is a definition, and it belongs to the class that |
| defines it. However, it also has a signature, and it makes sense to |
| talk about other initializers in related classes that share that |
| signature. |
| |
| Initializers come in two basic kinds: complete object and subobject. |
| That is, an initializer either takes responsibility for initializing |
| the complete object, potentially including derived class subobjects, |
| or it takes responsibility for initializing only the subobjects of its |
| class and its superclasses. |
| |
| There are three kinds of delegation: |
| |
| * **super**: runs an initializer belonging to a superclass (in ObjC, |
| ``[super init...]``; in Swift, ``super.init(...)``). |
| |
| * **peer**: runs an initializer belonging to the current class (ObjC |
| does not have syntax for this, although it is supported by the |
| runtime; in Swift, the current meaning of ``self.init(...)``) |
| |
| * **dispatched**: given a signature, runs the initializer with that |
| signature that is either defined or inherited by the most-derived |
| class (in ObjC, ``[self init...]``; not currently supported by Swift) |
| |
| We can also distinguish two ways to originally invoke an initializer: |
| |
| * **direct**: the most-derived class is statically known |
| |
| * **indirect**: the most-derived class is not statically known and |
| an initialization must be dispatched |
| |
| Either kind of dispatched initialization poses a soundness problem |
| because there may not be a sound initializer with any given signature |
| in the most-derived class. In ObjC, initializers are normal instance |
| methods and are therefore inherited like normal, but this isn't really |
| quite right; initialization is different from a normal method in that |
| it's not inherently sensible to require subclasses to provide |
| initializers at all the signatures that their superclasses provide. |
| |
| Subobject initializers |
| ~~~~~~~~~~~~~~~~~~~~~~ |
| The defining class of a subobject initializer is central to its |
| behavior. It can be soundly inherited by a class C only if is trivial |
| to initialize the ivars of C, but it's convenient to ignore that and |
| assume that subobjects will always trivially wrap and delegate to |
| superclass subobject initializers. |
| |
| A subobject initializer must either (1) delegate to a peer subobject |
| initializer or (2) take responsibility for initializing all ivars of |
| its defining class and delegate to a subobject initializer of its |
| superclass. It cannot initialize any ivars of its defining class if |
| it then delegates to a peer subobject initializer, although it can |
| re-assign ivars after initialization completes. |
| |
| A subobject initializer soundly acts like a complete object |
| initializer of a class C if and only if it is defined by C. |
| |
| Complete object initializers |
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| The defining class of a complete object initializer doesn't really |
| matter. In principle, complete object initializers could just as well |
| be freestanding functions to which a metatype is passed. It can make |
| sense to inherit a complete object initializer. |
| |
| A complete object initializer must either (1) delegate (in any way) to |
| another complete object initializer or (2) delegate (dispatched) to a |
| subobject initializer of the most-derived class. It may not |
| initialize any ivars, although it can re-assign them after |
| initialization completes. |
| |
| A complete object initializer soundly acts like a complete object |
| initializer of a class C if and only if it delegates to an initializer |
| which soundly acts like a complete object initializer of C. |
| |
| These rules are not obvious to check statically because they're |
| dependent on the dynamic value of the most-derived class C. Therefore |
| any ability to check them depends on restricting C somehow relative to |
| the defining class of the initializer. Since, statically, we only |
| know the defining class of the initializer, we can't establish |
| soundness solely at the definition site; instead we have to prevent |
| unsound initializers from being called. |
| |
| Guaranteed initializers |
| ~~~~~~~~~~~~~~~~~~~~~~~ |
| The chief soundness question comes back to dispatched initialization: |
| when can we reasonably assume that the most-derived class provides an |
| initializer with a given signature? |
| |
| Only a complete object initializer can perform dispatched delegation. |
| A dispatched delegation which invokes another complete object |
| initializer poses no direct soundness issues. The dynamic requirement |
| for soundness is that, eventually, the chain of dispatches leads to a |
| subobject initializer that soundly acts like a complete object |
| initializer of the most-derived class. |
| |
| Virtual initializers |
| ~~~~~~~~~~~~~~~~~~~~ |
| The above condition is not sufficient to make indirect initialization |
| sound, because it relies on the ability to simply not use an |
| initializer in cases where its delegation behavior isn't known to be |
| sound, and we can't do that to arbitrary code. For that, we would |
| need true virtual initializers. |
| |
| A virtual initializer is a contract much more like that of a standard |
| virtual method: it guarantees that every subclass will either define |
| or inherit a constructor with a given signature, which is easy to |
| check at the time of definition of the subclass. |
| |
| Proposal |
| -------- |
| Currently, all Swift initializers are subobject initializers, and |
| there is no way to express the notion of a complete subobject |
| initializer. We propose to introduce complete subobject initializers |
| into Swift and to make them inheritable when we can guarantee that |
| doing so is safe. |
| |
| Complete object initializers |
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| Introduce the notion of a complete object initializer, which is |
| written as an initializer with ``Self`` as its return type [#]_, e.g.:: |
| |
| init() -> Self { |
| // ... |
| } |
| |
| The use of ``Self`` here fits well with dynamic ``Self``, because a |
| complete object initializer returns an instance of the dynamic type |
| being initialized (rather than the type that defines the initializer). |
| |
| A complete object initializer must delegate to another initializer via |
| ``self.init``, which may itself be either a subobject initializer or a |
| complete object initializer. The delegation itself is dispatched. For |
| example:: |
| |
| class A { |
| var title: String |
| |
| init() -> Self { // complete object initializer |
| self.init(withTitle:"The Next Great American Novel") |
| } |
| |
| init withTitle(title: String) { // subobject initializer |
| self.title = title |
| } |
| } |
| |
| Subobject initializers become more restricted. They must initialize |
| A's instance variables and then perform super delegation to a |
| subobject initializer of the superclass (if any). |
| |
| Initializer inheritance |
| ~~~~~~~~~~~~~~~~~~~~~~~ |
| |
| A class inherits the complete object initializers of its direct |
| superclass when it overrides all of the subobject initializers of its |
| direct superclass. Subobject initializers are never inherited. Some |
| examples:: |
| |
| class B1 : A { |
| var counter: Int |
| |
| init withTitle(title: String) { // subobject initializer |
| counter = 0 |
| super.init(withTitle:title) |
| } |
| |
| // inherits A's init() |
| } |
| |
| class B2 : A { |
| var counter: Int |
| |
| init withTitle(title: String) -> Self { // complete object initializer |
| self.init(withTitle: title, initialCount: 0) |
| } |
| |
| init withTitle(title: String) initialCount(Int) { // subobject initializer |
| counter = initialCount |
| super.init(withTitle:title) |
| } |
| |
| // inherits A's init() |
| } |
| |
| class B3 : A { |
| var counter: Int |
| |
| init withInitialCount(initialCount: Int) { // subobject initializer |
| counter = initialCount |
| super.init(withTitle: "Unnamed") |
| } |
| |
| init withStringCount(str: String) -> Self { // complete object initializer |
| var initialCount = 0 |
| if let count = str.toInt() { initialCount = count } |
| self.init(withInitialCount: initialCount) |
| } |
| |
| // does not inherit A's init(), because init withTitle(String) is not |
| // overridden. |
| } |
| |
| ``B3`` does not override ``A``'s subobject initializer, so it does not |
| inherit ``init()``. Classes ``B1`` and ``B2``, however, both inherit |
| the initializer ``init()`` from ``A``, because both override its only |
| subobject initializer, ``init withTitle(String)``. This means that one |
| can construct either a ``B1`` or a ``B2`` with no arguments:: |
| |
| B1() // okay |
| B2() // okay |
| B3() // error |
| |
| That ``B1`` uses a subobject initializer to override it's superclass's |
| subobject initializer while ``B2`` uses a complete object initializer |
| has an effect on future subclasses. A few more examples:: |
| |
| class C1 : B1 { |
| init withTitle(title: String) { // subobject initializer |
| super.init(withTitle:title) |
| } |
| |
| init withTitle(title: String) initialCount(Int) { // subobject initializer |
| counter = initialCount |
| super.init(withTitle:title) |
| } |
| } |
| |
| class C2 : B2 { |
| init withTitle(title: String) initialCount(Int) { // subobject initializer |
| super.init(withTitle: title, initialCount:initialCount) |
| } |
| |
| // inherits A's init(), B2's init withTitle(String) |
| } |
| |
| class C3 : B3 { |
| init withInitialCount(initialCount: Int) { // subobject initializer |
| super.init(withInitialCount: initialCount) |
| } |
| |
| // inherits B3's init withStringCount(str: String) |
| // does not inherit A's init() |
| } |
| |
| Virtual initializers |
| ~~~~~~~~~~~~~~~~~~~~ |
| With the initializer inheritance rules described above, there is no |
| guarantee that one can dynamically dispatch to an initializer via a |
| metatype of the class. For example:: |
| |
| class D { |
| init() { } |
| } |
| |
| func f(_ meta: D.Type) { |
| meta() // error: no guarantee that an arbitrary of subclass D has an init() |
| } |
| |
| Virtual initializers, which are initializers that have the ``virtual`` |
| attribute, are guaranteed to be available in every subclass of |
| ``D``. For example, if ``D`` was written as:: |
| |
| class D { |
| @virtual init() { } |
| } |
| |
| func f(_ meta: D.Type) { |
| meta() // okay: every subclass of D guaranteed to have an init() |
| } |
| |
| Note that ``@virtual`` places a requirement on all subclasses to |
| ensure that an initializer with the same signature is available in |
| every subclass. For example:: |
| |
| class E1 : D { |
| var title: String |
| |
| // error: E1 must provide init() |
| } |
| |
| class E2 : D { |
| var title: String |
| |
| @virtual init() { |
| title = "Unnamed" |
| super.init() |
| } |
| |
| // okay, init() is available here |
| } |
| |
| class E3 : D { |
| var title: String |
| |
| @virtual init() -> Self { |
| self.init(withTitle: "Unnamed") |
| } |
| |
| init withTitle(title: String) { |
| self.title = title |
| super.init() |
| } |
| } |
| |
| Whether an initializer is virtual is orthogonal to whether it is a |
| complete object or subobject initializer. However, an inherited |
| complete object initializer can be used to satisfy the requirement for |
| a virtual requirement. For example, ``E3``'s subclasses need not |
| provide an ``init()`` if they override ``init withTitle(String)``:: |
| |
| class F3A : E3 { |
| init withTitle(title: String) { |
| super.init(withTitle: title) |
| } |
| |
| // okay: inherited ``init()`` from E3 satisfies requirement for virtual init() |
| } |
| |
| class F3B : E3 { |
| // error: requirement for virtual init() not satisfied, because it is neither defined nor inherited |
| } |
| |
| class F3C : E3 { |
| @virtual init() { |
| super.init(withTitle: "TSPL") |
| } |
| |
| // okay: satisfies requirement for virtual init(). |
| } |
| |
| Objective-C interoperability |
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| When an Objective-C class that contains at least one |
| designated-initializer annotation (i.e., via |
| ``NS_DESIGNATED_INITIALIZER``) is imported into Swift, it's designated |
| initializers are considered subobject initializers. Any non-designed |
| initializers (i.e., secondary or convenience initializers) are |
| considered to be complete object initializers. No other special-case |
| behavior is warranted here. |
| |
| When an Objective-C class with no designated-initializer annotations |
| is imported into Swift, all initializers in the same module as the |
| class definition are subobject initializers, while initializers in a |
| different module are complete object initializers. This effectively |
| means that subclassing Objective-C classes without designated-initializer |
| annotations will provide little or no initializer inheritance, because |
| one would have to override nearly *all* of its initializers before |
| getting the others inherited. This seems acceptable so long as we get |
| designated-initializer annotations into enough of the SDK. |
| |
| In Objective-C, initializers are always inherited, so an error of |
| omission on the Swift side (failing to override a subobject |
| initializer from a superclass) can result in runtime errors if an |
| Objective-C framework messages that initializer. For example, consider |
| a trivial ``NSDocument``:: |
| |
| class MyDocument : NSDocument { |
| var title: String |
| } |
| |
| In Swift, there would be no way to create an object of type |
| ``MyDocument``. However, the frameworks will allocate an instance of |
| ``MyDocument`` and then send a message such as |
| ``initWithContentsOfURL:ofType:error:`` to the object. This will find |
| ``-[NSDocument initWithContentsOfURL:ofType:error:]``, which delegates |
| to ``-[NSDocument init]``, leaving ``MyDocument``'s stored properties |
| uninitialized. |
| |
| We can improve the experience slightly by producing a diagnostic when |
| there are no initializers for a given class. However, a more |
| comprehensive approach is to emit Objective-C entry points for each of |
| the subobject initializers of the direct superclass that have not been |
| implemented. These entry points would immediately abort with some |
| diagnostic indicating that the initializer needs to be |
| implemented. |
| |
| .. [#] Syntax suggestion from Joe Groff. |
| |