Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2017-03-16

Removing force unwrap from Swift code

Swift is a nice language, and it has introduced new concepts to many people. The optionals is probably the No 1 most visible aspect of that.

I like optionals, but sometimes... they bite... usually at runtime after the software has been released.

Crashes are the result.

I am just guessing, but it would not surprise me if most runtime crashes are caused by force-unwrapped optionals.

This may well be the reason that force-unwrap is frowned upon.

Friends don't let friends force unwrap...

With that in mind, lets purge our code from the force unwrap. In this post a few methods for doing so.

The most obvious method to use is the 'guard' method:

func check(parameter: Int?) -> Int? {
    guard let parameter = parameter else { return nil }
    ...
}

After this invocation of guard, the parameter is nicely unwrapped.

Next up is the ?? operator:

var count: Int?

var unwrappedCount = count ?? 0

By using the ?? operator we can specify a default value to use if the optional is nil

This operator is also useful in many other situation, for example the loop:

var count: Int?
for index in 0 ..< count ?? 0 { ... }

But sometimes we have optional array's,... no problem:

var counters: [Int]?
for count in counters ?? [] { ... }

The next pattern is a matter of taste, some people like it, others don't.

For this pattern first look at the check function that was defined above. It returns an optional. Obviously it is the intention to return nil if the check fails.

When using that function it will be necessary to either use 'if let' or 'guard' or the '??' operator in series with an extra assignment. Each of these ways leads to its own (taste) problems.

The 'if let' means that we will need to put the rest of the code in brackets and have to indent it.

var a: Int?

var pb: Int
if let b = check(parameter: a) {
    pb = b
} else {
    print("error")
    pb = 16 // safe value
}

That looks horrible to me.

The 'guard' in the middle of a code block is also frowned upon, but more problematic is that it is not allowed to continue after the 'else' of the guard is executed:

var a: Int?

guard var pb = check(parameter: a) {
    print("error")
    pb = 16 // safe value
    return
}

Not a real solution either.

The '??' operator has other problems:

var a: Int?

var pb = check(parameter: a) ?? 16

We lose the ability to handle the error. Unless we start using an if statement for that, but then we can do without the ??.

Which lead me to start using a generic type for function results that may generate an error:

enum FunctionResult<T> {
    case error(String)
    case success(T)
}


func check(parameter: Int?) -> FunctionResult<Int> {
    guard let parameter = parameter else { return .error("Parameter is nil") }
    if parameter < 0 { return .error("Parameter to small") }
    else { return .success(parameter) }
}

Of course we then need to use a switch:

var pb: Int
switch check(parameter: a) {
case .error(let m): print(m); pb = 16 // safe value
case .success(let t): pb = t
}

Which imo already beats the previous attempts. But we can do better... with thanks to O.B. on the swift-user mailing list, it is possible to write a generic for that:

func assign<T>(_ closure: @autoclosure() -> FunctionResult<T>, onError: (String) -> T) -> T {
    switch closure() {
    case .error(let message): return onError(message)
    case .success(let t): return t
    }
}

True, that is becoming quite an amount of code, but we only need to write it once to cover a lot of usage, and look what happens to our example:


var a: Int?

var pb = assigncheck(parameter: a), onError: { print($0); return 16 } )

We don't lose any flexibility and it avoids force-unwrapping. But it may obfuscate the assignment a little.

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment