Checking if required init? failed from a convenience init

Hopefully this gets the gist of it - I'm adding a convenience initialiser to an AppKit class.

extension Example {
    // Example comes [from AppKit] with an initialiser, e.g.:
    //
    //   required init?()

    convenience init?(arg: Bool) {
        self.init()
        // How do I tell if the init failed or not?
    }
}

The compiler won't let me check self against nil (because it claims self can never be nil), it won't let me assign to self (it claims it's immutable), and I can't return anything (cannot return anything but nil from an initialiser).

2 Likes

Failable init delegation always immediately propagates the failure, so you'll have to contrive something like this:

convenience init?(arg: Bool) {
    var success = false
    defer { if !success { print("guess we failed") } }
    self.init()
    success = true
}

Not the most convenient thing—maybe guard self.init(…) should be a special allowed form in this case—but it's what's available today.

3 Likes

Ah, defer's not a terrible workaround for my purposes, and I hadn't thought of that - thanks!

It's also good just to know I'm not crazy… I spent quite a while trying to figure this out, and thought for sure it must be something dumb I was doing wrong.

It is pretty weird to me that the call to init just silently exits the caller. Swift normally requires an explicit indication of non-sequential control flow.

3 Likes

Yeah, this predates try/throws, as a port of the ObjC idiom:

self = [self init];
if (!self) {
  return nil
}

We knew that we didn't want people to forget assigning to self, and that we didn't want the nil to go untested and later be a problem if someone tried to access a variable. I'm not sure we even had guard yet, which meant we didn't have a construct that forced you to exit if you wanted to do any cleanup, but even then there usually wasn't any cleanup, in the fast majority of ObjC code that followed this idiom:

guard self.init() else {
  return nil
}

So it's just implicit. Now that you point it out it does seem weird though. (And if we supported guard self.init(), it would also allow delegating from a throwing convenience init to an optional-failable init, as proposed way back in 2015.)

2 Likes

I believe you could hack this up with a purpose-built protocol which enables your initializer to sneak around the self-rebinding limitations:

class B {
    required init?() {}
}

class C: B, P {}

protocol P {
    init?()
}

extension P {
    init?(_: Bool) {
        let test = Self()
        if let test = test {
            print("got self")
            self = test
        } else {
            return nil
        }
    }
}

let c = C(true) // got self

I’m not certain there wouldn’t be issues with conforming the AppKit class to a protocol like this but I think it should work if it’s a required initializer that you’re trying to call through to.

This is odd indeed. "If we did it today from scratch would we do it this way?" No!