Problem
Currently when class is initialising child objects, which need a callback to the parent object, additional code is needed to pass initialisation checks:
class Inner {
let foo: Int
let callback: () -> Void
init(foo: Int, callback: @escaping () -> Void) {
self.foo = foo
self.callback = callback
}
}
class Parent {}
class Outer: Parent {
let inner: Inner
init(foo: Int) {
inner = Inner(foo: foo) { [weak self] in // error: 'self' used before 'super.init' call
self?.handleCallback()
}
super.init()
}
private func handleCallback() {}
}
Some common workarounds:
#1. Make callback writable:
class Inner {
let foo: Int
var callback: () -> Void
init(foo: Int, callback: @escaping () -> Void) {
self.foo = foo
self.callback = callback
}
}
class Outer: Parent {
let inner: Inner
init(foo: Int) {
inner = Inner(foo: foo, callback: {})
super.init()
inner.callback = { [weak self] in
self?.handleCallback()
}
}
}
#2. Capture mutable variable:
class Outer: Parent {
let inner: Inner
init(foo: Int) {
var weakSelf: Outer? = nil
inner = Inner(foo: foo) { [weak self] in
self?.handleCallback()
}
super.init()
weakSelf = self
}
}
Idea
Workarounds may vary, but some form of mutability is needed to construct cycle like that to break a cycle in initialisation dependency.
Weak references already are a tool for breaking retain cycles. With changes to implementation of the weak references, the same language mechanism could be used to break cycles in initialisation dependency. This would reduce amount of boilerplate code, and smoothen the learning curve of the language.
Currently object with weak references can be in one of the two states - live
or deinitialising
. Allowing weak references to partially constructed objects introduces a third state - initialising
. Strong reference can be formed only for objects in the live
state. Attempting to get a strong reference to an object in the initialising
state returns nil
.
There are two alternatives for when object transitions from initialising
to live
state:
A) On init() of the root class.
B) After init() of the most-derived class.
class Inner {
let check: () -> Bool
init(check: @escaping () -> Bool) {
self.check = check
}
}
class Outer: Parent {
let inner: Inner
init() {
inner = Inner { [weak self] in self != nil }
super.init()
print(inner.check()) // true for A, false for B
}
}
func test() {
let outer = Outer()
print(outer.inner.check()) // true for both A & B
}
In practice, there is little difference between them, since typically callbacks will be called based on events delivered on the next runloop iteration/executor job.
Challenges
Currently weak references don't exist as a type. As a consequence, partially initialised self can be assigned to the weak reference, only if it exists in the scope of the initialiser. There is no way to pass around partially initialised self or a weak reference to the partially initialized self.
struct WeakRef<T: AnyObject> {
weak var ref: T?
}
class Outer: Parent {
init() {
weak var weak1 = self // ok
let callback: () -> Void = { [weak self] in self?.doSomething() } // ok
var weak2 = WeakRef(ref: self) // error
super.init()
}
}
Also, some of the existing code may assume that reading nil
from the weak reference implies that object is dead and associated data can be cleaned up. Introduction of the initialising
state breaks this assumptions. It might be safer to keep behaviour of the built-in weak references as-is, and instead introduce suggested functionality in a new type:
// Protocol that all class types conforms to, but also all existential bound to `AnyObject`
protocol ReferenceType {}
/// Weak reference as a type
struct WeakRef<T: ReferenceType>: Washable {
/// @_maybePartiallyInitialized is a magical attribute that suppresses checks for complete initialisation.
/// Argument type is intentionally non-optional
init(@_maybePartiallyInitialized _ object: T)
var object: T? { get }
var state: ObjectState<T> { get }
}
/// Enum that clearly disambiguates between `initialising` and `reinitialising` states
enum ObjectState<T: ReferenceType> {
/// Object is still initialising - try again later
case initializing
/// Object is alive, and will not deinitize while you are using provided strong reference
case live(T)
/// Object is deinitizing - remove all accosted data
case deinitializing
}
Also such new type could provide stable object identity that persists after object starts deinitialising (see Hashing Weak Variables) or be part of the API that allows listening to changes in object state.
See also Should weak be a type?