10

I'm confused about how Swift checks for nil when I'm using the Any type.
Here's an example:

let testA: Any? = nil
let testB: Any = testA as Any
let testC: Any? = testB

if testA != nil {
    print(testA) // is not called as expected
}
if testB != nil {
    print(testB) // prints "nil"
}
if testC != nil {
    print(testC) // prints "Optional(nil)"
}

testA works as expected. The variable is nil, so the condition is false.

testB works not as expecte. The variable is nil, as shown by the print call. But the condition testB != nil evaluates to true. Why is this the case?

testC confuses me as well, since it is testC = testB = testA. So why should it behave differently than testA?

How would I need to write the if conditions if testB ... and if testC ... to not be true.
I'm looking for a solution that doesn't require me to know the type, like ...

if let testB = testB as String

Edit: I'm testing this with Swift 4 in an Xcode 9.1 Playground file.

Edit2:
Some information about the actual problem I want to solve. I'm getting a dictionary of type [String: Any?] that is created by a JSON parser. I want to check if the value for a given key is nil, but it doesn't work, when the key exists and the value is Optional(nil).

Example:

var dict = [String: Any?]()
var string = "test"
var optionalString: String?
dict["key1"] = string
dict["key2"] = optionalString

if dict["key2"] != nil {
    print(dict["key2"]) // should not be executed, but returns Optional(nil)
}
florieger
  • 1,310
  • 12
  • 22
  • It's weird that `let testB: Any = testA as Any` doesn't give any compiler errors in the first place. `testA` is an optional and `testB` is not, so assigning `testA` to `testB` should at least tell you that it's not safe to do so and that you need to cast with `as?` or `as!`. – halileohalilei Jan 06 '18 at 14:36
  • 4
    `let testB: Any = testA` should have warned you about the eroding of the optional type (which you then silenced by saying `as Any`). Basically the root problem you're dealing with here is that you can't treat something as an optional unless the compiler knows that it's an optional. Saying `let testC: Any? = testB` just makes the compiler implicitly wrap `testB` in another layer of optionality. What's the actual problem you're trying to solve here? – Hamish Jan 06 '18 at 14:43
  • It looks like anything can be cast to Any. Thus Any can be nil as well, as demonstrated above. The strange thing is that the `testB != nil` shows a warning `Comparing non-optional value of type 'Any' to nil always returns true`. – florieger Jan 06 '18 at 14:43
  • @Hamish I added some background information to the question. – florieger Jan 06 '18 at 14:49
  • 3
    @florieger Ah, in that case, you just want to unwrap twice. `if let value = dict["key"]` gives you `value` of type `Any?`, meaning that the dictionary had the value. If you then say `if let nonNilValue = value`, you'll get back an `Any`, meaning that the value itself was non-nil. You can also say `dict["key"] ?? nil` to condense the two layers of optionality into one. Compare https://stackoverflow.com/q/29299727/2976878, https://stackoverflow.com/q/33049246/2976878 – Hamish Jan 06 '18 at 14:56
  • @Hamish this works for my second example, but not for `testB` and `testC` – florieger Jan 06 '18 at 15:01
  • 2
    Related: [Is it possible to cast Any to an Optional?](https://stackoverflow.com/questions/29060818/is-it-possible-to-cast-any-to-an-optional) – Your `testB` is not an optional, and reflection is needed to "extract" the optional again. – Martin R Jan 06 '18 at 15:18
  • 1
    Seconding what @MartinR writes (focusing on the contrived examples); also possibly [the following Q&A](https://stackoverflow.com/questions/40428796/given-a-swift-any-type-can-i-determine-if-its-an-optional) can be of interest, regarding using runtime introspection to query whether an `Any` instance wraps any kind of `Optional<...>` instance or not. – dfrib Jan 06 '18 at 15:27

1 Answers1

3

In Swift, nil is actually a concrete value(of type enum). testB which is of Any type, is holding the enum Optional with value none and hence the condition testB != nil is true.

enter image description here

This solves the mystery of how Any of testB is able to hold the nil value.

Coming to your actual problem, I tried this piece of code in Storyboard(Xcode 9.2) and it worked as expected.

var dict = [String: Any]()
var string = "test"
var optionalString: String?
dict["key1"] = string
dict["key2"] = optionalString

if let value = dict["key2"] {
    print(value) // doesn't get executed
}

For testB and testC, it seems like == check with nil should provide a solution but, because binary operand can not be used with two Any? operands, we can not use ==.

Using switch-case though is able to give correct results:

switch testB {
case Optional<Any>.none:
    print("value is none")
default:
    print("value is not none")
}

switch testC! {
case Optional<Any>.none:
    print("value is none")
default:
    print("value is not none")
}

O/P:
value is none
value is none

Puneet Sharma
  • 9,369
  • 1
  • 27
  • 33
  • Thanks for providing a solution, how could I make it work with `testB` and `testC`. – florieger Jan 06 '18 at 15:44
  • @florieger: Updated the answer – Puneet Sharma Jan 06 '18 at 17:21
  • So the same thing in memory can be interpreted in swift as nil in one case and as non-nil in another case? Absolutely insane language design decision. – algrid Jan 06 '18 at 21:26
  • @algrid: Its more of a Any type thing, but I agree its very confusing. – Puneet Sharma Jan 08 '18 at 04:49
  • 2
    As explained in https://stackoverflow.com/a/61530672/513038 , I think the problem actually occurs in `(Any) != nil` - the expression of compile-time type `Any` is, at compile time, wrapped in another layer of `Optional` - so the first layer of `Optional` is non-`nil`, and it contains an `Optional` which IS `nil`. (Technically `.none`.) At runtime, the `String?` is wrapped to become a `String??`. Maybe go read the linked answer, if my comment doesn't make sense. – Erhannis Apr 30 '20 at 20:05