Processing optional best practice?

Which of the two following would be best practice for processing an array of data that may not exist, and why ?

func processArray1(withData arrayData: [String]?) {
    
    if arrayData?.isEmpty == false {
        for text in arrayData! {
            print("TEXT : \(text)")
        }
    }
}

func processArray2(withData arrayData: [String] = []) {
    
    for text in arrayData {
        print("TEXT : \(text)")
    }
}

I favour the cleaner second option, is there a more elegant way of processing the first ?

There are essentially three different contingency plans for handling nil:

  1. Panic. You didn't expect nil, thought it would never happen, yet here we are. For example, a Storyboard is resource file is missing from your app bundle. Some things just simply aren't gracefully recoverable. It's reasonable to crash than to allow the app to live on in a zombie state that might further damage things (e.g. corrupt user documents).
  2. Substitute a default value. E.g. if a user doesn't provide you their name, you could say "Hello," instead of "Hello Joe,".
  3. Skip an operation. E.g. If you're going to refresh all of the entries in a list, but there are no entries, then there's nothing to refresh!

Both of the approach you show implement technique 3. But they differ. processArray1(withData: nil) is valid, but processArray2(withData: nil) is not. The default argument means you can omit the argument, and [] will be used instead (you can call processArray2(), but it doesn't mean you can pass nil.

Typically, you want to handle optionals as early as possible. It's usually the caller's responsibility to handle optionals, according to one of the three strategies above, before passing on to your code. If your code doesn't need an optional to work, then it shouldn't ask for one.

The way I would right this is simply:

func processArray2(withData arrayData: [String]) {
    for text in arrayData {
        print("TEXT : \(text)")
    }
}

With the expectation that callers will unwrap before proceeding to call this code.

4 Likes

Using optionals with arrays adds one more empty value for your type. If you care about .none vs .some([]) - use optionals. If both are the same for you problem - keeps things simple, and don't use optionals.

Note that processArray1() will crash on nil input. Inside the if condition, arrayData?.isEmpty will evaluate to Bool?.none, false is implicitly casted to Bool?.some(false) and the two are not equal. It is better to use if let / guard let instead of if + force unwrapping to avoid confusion.

Note that processArray1() requires argument value to be specified, while processArray2() can be called without arguments. I'm not sure that is intentional.

2 Likes

I agree if/guard let are easier to reason about than this roundabout way, but if nil != false, and thus the body of the if is not reached when arrayData is nil, how will it crash?

Ah, sorry, my bad. You are correct.

I did not know that.

Thanks for the info !

The important question to ask here is, are “the array exists but contains no strings” (empty array) and “the array does not exist” (nil array) distinct conditions that your logic needs to handle? That is, do they mean two different things in your code, and are both of those things relevant here?

For instance, in this function, a missing array means “use strings loaded from the disk”; an empty array means “use this empty list of strings”:

func processArray(withData arrayData: [String]?) {
     let loadedData = arrayData ?? loadArrayFromDisk()
     for text in loadedData {
         print(text)
     }
}

In your case, it seems like you want to treat empty arrays and nil arrays the same, which suggests that the parameter should not be optional. But if there are other parts of the code where the distinction is important and it just happens to not be in this function, maybe you’d write it like this.

6 Likes

Personally, I would write the first function as follows:

func processArray1(withData arrayData: [String]?) { 
    if let data = arrayData {
        for text in data {
            print("TEXT : \(text)")
        }
    }
}

There is no need to check for the array to be empty - the for loop will do nothing anyhow with empty array. If this would be needed for some reason, could write:

func processArray1(withData arrayData: [String]?) { 
    if let data = arrayData, !data.isEmpty {
        for text in data {
            print("TEXT : \(text)")
        }
    }
}

If you want avoid nesting if statements too deep, can also write with guard:

func processArray1(withData arrayData: [String]?) { 
    guard let data = arrayData, !data.isEmpty else {
        return
    }

    for text in data {
        print("TEXT : \(text)")
    }
}
2 Likes

You just helped me see a way to make my code cleaner. Thanks, pal!

Terms of Service

Privacy Policy

Cookie Policy