(ok I think I’ve pushed the puns too far with that headline)
David Owens rightly called me out for pulling a bit of a fast one at the start of my last article on how ?:
and ??
differ in behaviour. Not that they don’t, but I was acting as if what the ternary operator version was doing was something perfectly sensible, and that ??
was the funny one. But I glossed over what the ternary operator itself was doing.
Look at this code again:
let i: Int? = nil let j: Int? = 5 let k: Int? = 6 // this returns {Some 5} let x = i != nil ? i! : j // this returns {Some 6} let y = k != nil ? k! : j
What are the types of x
and y
? Int?
, right? And how is that decided? From the type of the ternary expression. But which part? If you look at the second and third part of the expression, they’re different types. i!
and k!
are of type Int
, whereas j
is of type Int?
.
That’s no good! Both possible values from the ternary expression have to be able to become the same type, because the type of x
is determined at compile time, before you know what i
or k
contains.
For example, this won’t compile:
let s = "hello" let i = 123 // x can't be either a String or an Int let x = i > 0 ? s : i
But note I said become the same type, not be the same type. As we saw, the compiler is willing to put your values into optionals to make them fit. It can convert i!
, an Int
, to be of type Int?
, and that would match what j
is. Since that’s the only possible way this would work, it does it. I’m guessing it rewrites the expression as:
let x = i != nil ? Optional(i!) : j
This looks similar to what was happening with ??
– the left-hand side is getting upconverted from an Int
to an Int?
. But in this case, it’s happening after the comparison to nil
, whereas in the ??
function, it was upconverted beforehand. Which explains why, with the ternary statement version, j
is picked over i
.
Of course, none of this is likely the behaviour you wanted. You probably just fat-fingered an optional onto the right-hand side, meaning for it to be a regular Int
. If so, you might find being explicit about the type would help avoid this confusion:
// if j is an Int?, this will not compile: let x: Int = i != nil ? i! : j // and neither will this: let y: Int = i ?? j
In the case of the ??
version, the compiler’s error message is pretty helpful – it asks you if you meant to have type Int?
for j
, and offers to stick a !
after it to unwrap it for you. This makes it clear that, in both cases, passing an optional on the right-hand side is user error (which is why I don’t think the way ??
behaves is necessarily a bug).
Finally, here’s a silly thing to try. Up until now, we’ve been fixing the type of the result by converting one of the two types to another. What if you had two types, both of which could be implicitly converted to a third type? Would that work too?
Yes it would. We can create a class that can be constructed from either a string literal or an integer literal, and then use a ternary statement that returned one or the other:
struct Both: IntegerLiteralConvertible, StringLiteralConvertible { typealias ExtendedGraphemeClusterLiteralType = String let str: String static func convertFromIntegerLiteral(value: IntegerLiteralType) -> Both { return Both(str: String(value)) } static func convertFromStringLiteral(value: StringLiteralType) -> Both { return Both(str: value) } static func convertFromExtendedGraphemeClusterLiteral(value: ExtendedGraphemeClusterLiteralType) -> Both { return Both(str: value) } } let b = true // x will be of type String let x = b ? "one" : "two" // y will be of type Int let y = b ? 1 : 2 // z1 and z2 will be of type Both let z1 = b ? "one" : 2 // {str "one"} let z2 = !b ? "one" : 2 // {str "2"}
Well, it amused me anyway.
[…] final post on upconversion of non-optional values to optional. As we saw previously, if you pass a non-optional value to a function argument that expects an optional, it will get […]