SE-0213 changed the way that initialization from literals works.
Quoting from the source compatibility section:
This is a source breaking change because it’s possible to declare a conformance to
a literal protocol and also have a failable initializer with the same parameter type:struct Q: ExpressibleByStringLiteral { typealias StringLiteralType = String var question: String init?(_ possibleQuestion: StringLiteralType) { return nil } init(stringLiteral str: StringLiteralType) { self.question = str } } _ = Q("ultimate question") // 'nil' _ = "ultimate question" as Q // Q(question: 'ultimate question')
Although such situations are possible, we consider them to be quite rare
in practice. FWIW, none were found in the compatibility test suite.
It turns out that I use this pattern a lot when implementing literal protocols where not all literal values are valid. Something like:
struct Hour: ExpressibleByStringLiteral {
let hour: Int, minute: Int
init?(_ string: String) {
// Check whether `string` is a valid "HH:MM". If not, return `nil`
}
init(stringLiteral value: String) {
self.init(value)!
}
}
Confusingly, the end result is:
// Swift 4
_ = TimeOfDay("X") // calls `init(_:)`, returns `nil`
_ = TimeOfDay("X")?.hour // calls `init(_:)`, returns `nil`
// Swift 5
_ = TimeOfDay("X") // calls `init(stringLiteral:)`, **crashes**
_ = TimeOfDay("X")?.hour // calls `init(_:)`, returns `nil`
It looks like the optional chaining in the second statement causes the compiler to use the failable initializer instead of the literal one.
I find it pretty surprising/unintuitive that TimeOfDay("X")
and TimeOfDay("X")?.hour
call different initializers. Is that the desired behavior?
cc @xedin (proposal author) and @John_McCall (review manager)