When I was working on reimplementing the way we deal with implicitly unwrapped optionals earlier this year, I came across some odd and inconsistent behavior, but had not yet had a chance to follow up on it. Someone else recently noticed the same behavior and opened a bug: [SR-8326] Surprising interaction between casting to Any and optional promotion · Issue #50854 · apple/swift · GitHub
The TL;DR version is that we sometimes flatten nils when injecting optionals, and sometimes don't.
class B {}
class D : B {}
let d: D? = nil
let dprime: D?? = d // .some(.none)
let bprime: B?? = d // .none
We do not produce a nil result when we are strictly injecting an optional without any conversion of the underlying type (e.g. from derived-to-base, something-to-Any), but do flatten them when there is also a conversion involved.
This behavior can of course manifest as part of an if let
or guard let
with an explicit type specified, and in fact that the context in which I first noticed this as well as what the report in the bug mentions.
if let b: B? = d {...} // convert from D? to B??, then unwrap one level
This raises the questions:
- Should the condition tested in the
if let
andguard let
be the result after converting to the user-specified type, or should it always be based on the RHS prior to conversion? - Should we always flatten the
nil
when injecting, or never flatten thenil
when injecting? Always flattening of course means we don't need to answer the first question, but it also means we have a type system where we can form multi-level optional types where the extra levels of optionality are redundant.
It seems reasonable to me that the answers here would be:
- We should always use the RHS prior to conversion in
if let
/guard let
- We should never flatten
nil
s in this way.
Thoughts?
P.S.: Here's an example demonstrating the behavior.
class B {}
class D : B {}
func takesD(d: D??) {
if d == nil {
print("nil")
} else {
print("non-nil")
}
}
func takesB(b: B??) {
if b == nil {
print("nil")
} else {
print("non-nil")
}
}
func generic<T>(lhs: T, rhs: T?) {
if rhs == nil {
print("nil")
} else {
print("non-nil")
}
}
func test() {
let d: D? = nil
takesD(d: d) // passes .some(.none)
takesB(b: d) // passes .none
// warning: explicitly specified type 'D?' adds an additional level of optional to the initializer, making the optional check always succeed
if let _: D? = d { // forms .some(.none)
print("non-nil")
} else {
print("nil")
}
// no warning
if let _: B? = d { // results in .none
print("non-nil") // skipped
} else {
print("nil") // printed
}
generic(lhs: d, rhs: d) // rhs is .some(.none)
generic(lhs: d as B?, rhs: d) // rhs is .none
}
test()