Swift: Failing with Class (failable initializer, nil coalescing operator)


The basics

A failable initializer can exist in a struct, enum or class, but there are some special considerations when working with a class. So let's begin with the simplest form in which a class with a failable initializer can be written:
class Fail {
    init?() {
    return nil
    }
}
It's important to note here that there's nothing to stop us using a force unwrapped init
init!()
in place of
init?()
But there's not a great deal to report about this fact beyond what is written here.

What does it mean to fail?

So what happens when a failable initializer fails? Adding in a deinit method
class Fail {
    init?() {
    return nil
    }
    deinit {
        "Deinitialized"
    }
}
demonstrates that deinitialization still takes place, which leads us to the conclusion that initialization, albeit incomplete, takes place as well. After all that is the point isn't it? Full instantiation cannot take place because some important piece of the puzzle fails and so nil is returned.

Entanglement with class

Despite the similarities between a class and a struct, use of an init() constructor with a struct is not required when there are properties in need of initialization, but when used it overrides the automated memberwise initializer and properties must then be initialized within the constructor.

Creating a struct with both stored properties and a failable initializer can be written like this:
struct EvenNumberStruct {
    let number: UInt
    init?(number: UInt) {
        if number % 2 != 0 {
            return nil
        }
        self.number = number
    }   
}
But if we were to change the type from a struct to a class, then we would be warned: "All stored properties of a class instance must be initialized before returning nil from an initializer", and here we start to enter the territory alluded to in the opening of this post.

But what's the point of initializing properties when the instance itself is going to be immediately deinitialized? And what's the best approach to doing this? Apple's advice is that we don't initialize the properties and instead use optionals or implicitly unwrapped optionals.

The advantage of implicitly unwrapped optionals being that once set they will behave like ordinary properties. In contrast, straightforward optionals will behave like optional properties even once they are initialized.

Ready for the nil coalescing operator

In a previous blogpost, I discussed the pros and cons of using failable initializers in combination with nil coalescing operators. So let's create a class with a failable initializer that also has a type (or class) method with the ability to create and return an instance.
class EvenNumber {
    let number: UInt!
    init?(number: UInt) {
        if number % 2 != 0 {
            return nil
        }
        self.number = number
    }
    private init(num: UInt) {
        self.number = num
    }
    class func defaultNumber() -> EvenNumber {
        return EvenNumber(num: 2)
    }
    
    deinit {
        number
        "deinitialized"
    }
}
Now a coalescing operator can be used.
let anEvenNumber = EvenNumber(number: 3) ?? EvenNumber.defaultNumber()

Precautions

As noted in the previous blogpost cited above, Apple keeps quiet about the idea that the nil coalescing operator should be combined with failable initializers. So before starting to build classes with failable initializers in the real world, it's worth quoting something from the Apple blog:
Using the failable initializer allows greater use of Swift’s uniform construction syntax, which simplifies the language by eliminating the confusion and duplication between initializers and factory methods. (Apple Blog)
And to include at the same time a similar reference to factory methods and failable intializers that was made elsewhere
... factory methods that have NSError** parameters, such as +[NSString stringWithContentsOfFile:encoding:error:], will now be imported as failable) initializers. (Apple Developer)
in which Apple makes clear that any Objective-C factory method capable of resulting an error should be converted into a failable initializer.

The reason it is worth drawing attention to these is because it might be construed that Apple is placing a call to ditch factory methods (which I'm reading here to mean type methods that return an instance – please correct me if I'm wrong on this) entirely and switch to the use of init. But consider the following:
struct EvenNumberStruct {
    let number: UInt
    init?(number: UInt) {
        if number % 2 != 0 {
            return nil
        }
        self.number = number
    }
    private init(num:UInt) {
        self.number = num
    }
    
    static func defaultNumber() -> EvenNumberStruct {
        return EvenNumberStruct(num:2)
    }
    
}
And ask, why use a class method at all here? Why not open up the private init and make it public? Like so:
struct EvenNumberStruct {
    let number: UInt
    init?(number: UInt) {
        if number % 2 != 0 {
            return nil
        }
        self.number = number
    }
    init() {
        self.number = 2
    }    
}
The reason I would argue is because in this second instance we're not providing an indication of what will be returned. And I'd personally rather call a method like this
EvenNumberStruct.defaultNumber()
where I have clear expectations about what will be the result, rather than do this
EvenNumberStruct()
where the result I would expect is, for example, an object with undefined parameters.

But to be clear, I don't think Apple is calling to ditch factory methods entirely, but is instead only encouraging factory methods capable of returning a guaranteed instance. And this is something that is slightly trickier to convey, while all the same is worth conveying so it does not become lost among the larger aim of utilizing nil to convey a failure of instantiation.

Note: a private init() will only be private when the type is instantiated from a Swift file other than the one in which the class is defined.

Conclusion

I've not worked through the all the variant implementations of failable initializers here and have notably omitted any discussion of enumerations that can fail, as well as discussion of overriding failable initializers in subclasses. I have instead taken the opportunity to follow up on the first roadblock I encountered when approaching failable initializers, in the hope that it will help others who encounter the same thing.




Endorse on Coderwall

Comments