Home > Enterprise >  Swift control flow: Unwrapping nil despite if check?
Swift control flow: Unwrapping nil despite if check?

Time:11-22

This sounds like a programming-newbie-question but how is this situation possible?

enter image description here

for typelessResultDict in results {
    let resultDict = typelessResultDict as! Dictionary<String, Any>
    let internalId = resultDict["internalId"] as? String?
    let orderDate = resultDict["orderDate"] as? Date?
    
    if internalId == nil || orderDate == nil {
        #if DEBUG
        fatalError("internalId/orderDate was nil")
        #endif
        continue
    }
    do {
        try removeAllButOne(internalId: internalId!!, orderDate: orderDate!!)
    } catch {
        LOGGER.error("Could not deduplicate.")
    }
}

In a for loop, I am explicitly checking if the variable orderDate is nil and either raise a fatalError in DEBUG or at least continue the loop, effectively canceling all other statements.

However, you can see that my code is executed and raises a fatal error because of force-unwrapping nil a few lines later.

Is this a bug or do I need to revisit first semester computer science?

CodePudding user response:

Note that the types of internalId and orderDate are double optionals - String?? and Date?? respectively. This happens because resultDict["internalId"] produces a Any? (the key could be absent in the dictionary), and the safe cast to String? produces a String??, as there are 3 cases:

  • the key is present in the dictionary, and the cast succeeds (both layers of optional would be non-nil)
  • the key is absent in the dictionary (the outer optional would be non-nil, wrapping a nil as the inner optional)
  • the cast fails (the outer optional is nil)

== nil only checks if the outermost layer of the optional is nil, but you are unwrapping both layers with two exclamation marks. The first exclamation mark is safe as ensured by your if statement, but the second is not.

You can avoid all of this if you just cast to String and Date instead:

let internalId = resultDict["internalId"] as? String
let orderDate = resultDict["orderDate"] as? Date

You can also use a guard statement to eliminate the forced unwrapping:

guard let internalId = resultDict["internalId"] as? String,
      let orderDate = resultDict["orderDate"] as? Date else {
    #if DEBUG
    fatalError("internalId/orderDate was nil")
    #endif
    continue
}

do {
    try removeAllButOne(internalId: internalId, orderDate: orderDate)
} catch {
    LOGGER.error("Could not deduplicate.")
}

CodePudding user response:

The problem is using 'as? String?' and 'as? Date?' that means your internalId and orderDate object types are :

String?? and Date??

They are like a box that eventually can contain an optional, that is another box. So when you check :

if internalId == nil || orderDate == nil

The answer is always false, because they really contains an optional object. Regardless of whether the optional si full or empty.

So instead of 'as? String?' you can use :

  • as? String

Now your code will work.

I suggest you this syntax that is more clear :

let resultDict : [String : Any] = [:]
let internalDict = resultDict["InternalDict"] as? String
let orderDate = resultDict["OrderDate"] as? Date

if let internalDict = internalDict, let orderDate = orderDate
{
    //They are not Nil
    print(internalDict)
    print(orderDate)
}
else
{
    //One of them is Nil
    fatalError("internalDict/orderDate was nil")
}

P.S. Please next time post text and not image.

  • Related