orphan: |
---|
Authors: | Doug Gregor, John McCall |
---|
Contents
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.
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.
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.
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.
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.
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.
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.
Introduce the notion of a complete object initializer, which is
written as an initializer with Self
as its return type [1], 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).
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() }
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(). }
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.
[1] | Syntax suggestion from Joe Groff. |