Skip to content

Latest commit

 

History

History
341 lines (275 loc) · 11.4 KB

Initialization.rst

File metadata and controls

341 lines (275 loc) · 11.4 KB
orphan:

The initializer for a class that has a superclass must ensure that its superclass subobject gets initialized. The typical way to do so is through the use of superclass delegation:

class A {
  var x: Int

  init(x: Int) {
    self.x = x
  }
}

class B : A {
  var value: String

  init() {
    value = "Hello"
    super.init(5) // superclass delegation
  }
}

Swift implements two-phase initialization, which requires that all of the instance variables of the subclass be initialized (either within the class or within the initializer) before delegating to the superclass initializer with super.init.

If the superclass is a Swift class, superclass delegation is a direct call to the named initializer in the superclass. If the superclass is an Objective-C class, superclass delegation uses dynamic dispatch via objc_msgSendSuper (and its variants).

An initializer can delegate to one of its peer initializers, which then takes responsibility for initializing this subobject and any superclass subobjects:

extension A {
  init fromString(s: String) {
    self.init(Int(s)) // peer delegation to init(Int)
  }
}

One cannot access any of the instance variables of self nor invoke any instance methods on self before delegating to the peer initializer, because the object has not yet been constructed. Additionally, one cannot initialize the instance variables of self, prior to delegating to the peer, because doing so would lead to double initializations.

Peer delegation is always a direct call to the named initializer, and always calls an initializer defined for the same type as the delegating initializer. Despite the syntactic similarities, this is very different from Objective-C's [self init...], which can call methods in either the superclass or subclass.

Peer delegation is primarily useful when providing convenience initializers without having to duplicate initialization code. However, peer delegation is also the only viable way to implement an initializer for a type within an extension that resides in a different resilience domain than the definition of the type itself. For example, consider the following extension of A:

extension A {
  init(i: Int, j: Int) {
    x = i + j    // initialize x
  }
}

If this extension is in a different resilience domain than the definition of A, there is no way to ensure that this initializer is initializing all of the instance variables of A: new instance variables could be added to A in a future version (these would not be properly initialized) and existing instance variables could become computed properties (these would be initialized when they shouldn't be).

Initializers are not inherited by default. Each subclass takes responsibility for its own initialization by declaring the initializers one can use to create it. To make a superclass's initializer available in a subclass, one can re-declare the initializer and then use superclass delegation to call it:

class C : A {
  var value = "Hello"

  init(x: Int) {
    super.init(x) // superclass delegation
  }
}

Although C's initializer has the same parameters as A's initializer, it does not "override" A's initializer because there is no dynamic dispatch for initializers (but see below).

We could syntax-optimize initializer inheritance if this becomes onerous. DaveA provides a reasonable suggestion:

class C : A {
  var value = "Hello"

  @inherit init(Int)
}

Note: one can only inherit an initializer into a class C if all of the instance variables in that class have in-class initializers.

The initializer model above only safely permits initialization when we statically know the type of the complete object being initialized. For example, this permits the construction A(5) but not the following:

func createAnA(_ aClass: A.metatype) -> A {
  return aClass(5) // error: no complete initializer accepting an ``Int``
}

The issue here is that, while A has an initializer accepting an Int, it's not guaranteed that an arbitrary subclass of A will have such an initializer. Even if we had that guarantee, there wouldn't necessarily be any way to call the initializer, because (as noted above), there is no dynamic dispatch for initializers.

This is an unacceptable limitation for a few reasons. The most obvious reason is that NSCoding depends on dynamic dispatch to -initWithCoder: to deserialize an object of a class type that is dynamically determined, and Swift classes must safely support this paradigm. To address this limitation, we can add the virtual attribute to turn an initializer into a virtual initializer:

class A {
  @virtual init(x: Int) { ... }
}

Virtual initializers can be invoked when constructing an object using an arbitrary value of metatype type (as in the createAnA example above), using dynamic dispatch. Therefore, we need to ensure that a virtual initializer is always a complete object initializer, which requires that every subclass provide a definition for each virtual initializer defined in its superclass. For example, the following class definition would be ill-formed:

class D : A {
  var floating: Double
}

because D does not provide an initializer accepting an Int. To address this issue, one would add:

class D : A {
  var floating: Double

  @virtual init(x: Int) {
    floating = 3.14159
    super.init(x)
  }
}

As a convenience, the compiler could synthesize virtual initializer definitions when all of the instance variables in the subclass have in-class initializers:

class D2 : A {
  var floating = 3.14159

  /* compiler-synthesized */
  @virtual init(x: Int) {
    super.init(x)
  }
}

This looks a lot like inherited initializers, and can eliminate some boilerplate for simple subclasses. The primary downside is that the synthesized implementation might not be the right one, e.g., it will almost surely be wrong for an inherited -initWithCoder:. I don't think this is worth doing.

Note: as a somewhat unfortunate side effect of the terminology, the initializers for structs and enums are considered to be virtual, because they are guaranteed to be complete object initializers. If this bothers us, we could use the term (and attribute) "complete" instead of "virtual". I'd prefer to stick with "virtual" and accept the corner case.

We currently ban initializers in protocols because we didn't have an implementation model for them. Protocols, whether used via generics or via existentials, use dynamic dispatch through the witness table. More importantly, one of the important aspects of protocols is that, when a given class A conforms to a protocol P, all of the subclasses of A also conform to P. This property interacts directly with initializers:

protocol NSCoding {
  init withCoder(coder: NSCoder)
}

class A : NSCoding {
  init withCoder(coder: NSCoder) { /* ... */ }
}

class B : A {
  // conforms to NSCoding?
}

Here, A appears to conform to NSCoding because it provides a matching initializer. B should conform to NSCoding, because it should inherit its conformance from A, but the lack of an initWithCoder: initializer causes problems. The fix here is to require that the witness be a virtual initializer, which guarantees that all of the subclasses will have the same initializer. Thus, the definition of A above will be ill-formed unless initWithCoder: is made virtual:

protocol NSCoding {
  init withCoder(coder: NSCoder)
}

class A : NSCoding {
  @virtual init withCoder(coder: NSCoder) { /* ... */ }
}

class B : A {
  // either error (due to missing initWithCoder) or synthesized initWithCoder:
}

As noted earlier, the initializers of structs and enums are considered virtual.

The initialization model described above guarantees that objects are properly initialized before they are used, covering all of the major use cases for initialization while maintaining soundness. Objective-C has a very different initialization model with which Swift must interoperate.

Each Swift initializer definition produces a corresponding Objective-C init method. The existence of this init method allows object construction from Objective-C (both directly via [[A alloc] init:5] and indirectly via, e.g., [obj initWithCoder:coder]) and initialization of the superclass subobject when an Objective-C class inherits from a Swift class (e.g., [super initWithCoder:coder]).

Note that, while Swift's initializers are not inherited and cannot override, this is only true in Swift code. If a subclass defines an initializer with the same Objective-C selector as an initializer in its superclass, the Objective-C init method produced for the former will override the Objective-C init method produced for the latter.

The emission of Objective-C init methods for Swift initializers open up a few soundness problems, illustrated here:

@interface A
@end

@implementation A
- init {
  return [self initWithInt:5];
}

- initWithInt:(int)x {
  // initialize me
}

- initWithString:(NSString *)s {
  // initialize me
}
@end

class B1 : A {
  var dict: NSDictionary

  init withInt(x: Int) {
    dict = []
    super.init() // loops forever, initializing dict repeatedly
  }
}

class B2 : A {
}

@interface C : B2
@end

@implementation C
@end

void getCFromString(NSString *str) {
  return [C initWithString:str]; // doesn't initialize B's dict ivar
}

The first problem, with B1, comes from A's dispatched delegation to -initWithInt:, which is overridden by B1's initializer with the same selector. We can address this problem by enforcing that superclass delegation to an Objective-C superclass initializer refer to a designated initializer of that superclass when that class has at least one initializer marked as a designated initializer.

The second problem, with C, comes from Objective-C's implicit inheritance of initializers. We can address this problem by specifying that init methods in Objective-C are never visible through Swift classes, making the message send [C initWithString:str] ill-formed. This is a relatively small Clang-side change.

Neither of the above "fixes" are complete. The first depends entirely on the adoption of a not-yet-implemented Clang attribute to mark the designated initializers for Objective-C classes, while the second is (almost trivially) defeated by passing the -initWithString: message to an object of type id or using some other dynamic reflection.

If we want to close these holes tighter, we could stop emitting Objective-C init methods for Swift initializers. Instead, we would fake the init method declarations when importing Swift modules into Clang, and teach Clang's CodeGen to emit calls directly to the Swift initializers. It would still not be perfect (e.g., some variant of the problem with C would persist), but it would be closer. I suspect that this is far more work than it is worth, and that the "fixes" described above are sufficient.