Mixing Initializers
init? init! init throws
Today is a really simple post but something people struggle with, often in the context of extensions or interacting with RawRepresentable
enums.
Can't delegate to failable initializer
Let's say you have a type with a failable initializer. Now you want to extend that type to support deserializing from JSON. You throw nice detailed errors if JSON parsing fails and you'd like to throw if anything goes wrong, either in JSON parsing or with the failable initializer, it would be nice if we could just throw.
struct FizzFail {
init?(string: String) { /* magic */ }
init(json: JsonValue) throws {
//do something to get value from json
guard let string = json.string else { throw JsonError.invalidJson }
//error: a non-failable initializer cannot delegate to failable initializer
if self.init(string: string) == nil {
throw JsonError.missingValue
}
}
}
There are two problems here. One is that we can't delegate to a failable initializer. The compiler will helpfully offer to turn this initializer into a failable one or insert an unsafe unwrap that will abort if someone sends us a mangled JSON value (and that's not common right?).
The second is there is no syntax to checking the result of calling the failable initializer even if we could; self
is only optional in the context of a failable initializer.
There are three solutions:
1. If you own the type, move actual initialization into a private throwing initializer then delegate from both public initializers. In the failable case use try?
to ignore the error.
struct Fizz {
private init(value: String) throws { /* magic */ }
init(json: JsonValue) throws {
guard let string = json.string else {
throw JsonError.invalidJson
}
try self.init(value: string)
}
init?(string: String) {
try? self.init(value: string)
}
}
2. If you don't own the type but it is a value type you can reassign self. This causes an extra allocation but otherwise results in much cleaner code.
extension FizzFail {
init(json: JsonValue) throws {
//do something to get value from json
guard let string = json.string,
let value = type(of: self).init(string: string) else {
throw JsonError.invalidJson
}
self = value
}
}
3. If you don't own the type and it is a reference type then you'll just have to live with a static or class function because you don't have any good options.
RawValue
The same trick shown above works for things like enums with raw values. Even though you own the type you don't own the automatically-generated constructor so just reassign self:
enum Buzz: String {
case wat = "wat"
case other = "other"
private enum ConstructionError: Error {
case invalidConstruction
}
init(string: String) {
if let value = type(of: self).init(rawValue: string) {
self = value
} else {
self = .other
}
}
}
Conclusion
Brought to you by can I whip up a post in 30 minutes so people don't think my blog is dead? and the letter Q.
This blog represents my own personal opinion and is not endorsed by my employer.