Moving toward deprecating force unwrap from Swift?

Hi S/E,

A parting thought for for what's left of 2020... It's seemed to me for a long time it's time to move towards deprecating the force unwrap operator from the Swift language. At this stage I'd suggest that it any usage becomes a compiler warning as it is for most Swift code linters. It's generally a sign of laziness and there are almost always better alternatives that make the developer consider how to properly handle an optional being nil. Swift has done a great job of preventing undefined behaviour and yet it leaves this potential foot gun laying around which was at one point was a suggested fix-it. I'd lay odds it has to be 90% of the time a Swift program crashes it was due to a thoughtless force unwrap. It's just too easy to use.

It's not like this is a new idea, I sometimes wonder how force unwrap made it into the language in the first place. I guess the same probably goes for try! and as! IMO.

4 Likes

I don't agree with this point. It's an assertion the programmer is making about their program and is no more a sign of laziness than any other assertion. You could make it more verbose, but it seems hard to justify the code churn and you give up the nice consistency between !, try! and as!. You could remove it and force people to write a custom message explaining the assertion, but that message is generally going to communicate the exact same thing: I thought that this couldn't be nil at this point but it is.

26 Likes

What should I use in situations like these after force unwrapping is deprecated? I don't know any other way to handle a situation when optional is never nil. I could use if let or ?? but that suggests that the result could be nil

let url = URL(string: "https://forums.swift.org/")!
let choice = ["rock", "paper", "scissors"].randomElement()!
26 Likes

Somewhat agree, but it seems to exist in Swift more because of UIKit + storyboards. Unfortunately lifetimes of some of the UIKit objects should be the same theoretically, but for historical reasons they are not and you end up with a lot of implicit and explicit unwrapping.

Moreover, when you are confident something can not be nil but say the language forces you to unwrap nicely, you may not have a clear bail out path - you simply may not know what to do at runtime if it's nil. In these situations I'd rather let my app crash and let me know something is wrong, e.g. the storyboard connection didn't work, or something even more fundamental.

1 Like

That's a fair point but situations like this are outnumbered by situations where the developer has collapsed a whole series of assumptions into a single character. It's not difficult to imagine a typo in a URL causing a crash and an alternative such as this in the standard library would be my preference:
(it could enhanced to require an explanitory message and report the caller #file/#line.)

I figure implicitly unwrapped optionals for @IBOutlets and imported C functions not annotated with _Nullable etc. have to continue as edge cases. It's the operator I have a beef with.

I just thought maybe instead of func unsafeUnwrap() like you suggested, a slightly nicer func get() throws similar to that Result has, but defined for the Optional type. Then try! opt.get() should probably be allowed because it's heavier than a single ! and better reflects the intention (it almost screams "try! try damn it!" :slight_smile: )

There are still cases where force-unwrapping is necessary, like in guards:

doSomething() { error in
    guard error == nil else {
        print(error!) // No real access to error, but we know it exists
        return
    }
}

I think for this specific case there was a pitch to allow "error" to be used inside that section, but there are many cases like this where using ! is valid because the context makes it safe (and the unwrapping would be useless)

In tests nowadays though this was fixed when try XCTUnwrap() was added. I think the try opt.get() suggestion above me fits nicely for this reason.

Why not use?

    if let error = error {

Not keen on trading a force unwrap for a try! myself. Now we have Result<>, the times you'd need a throwing function are getting less and less which I'm happy about as do/catch tended to disrupt the flow of code.

5 Likes

Maybe this?

guard let url = URL(string: "https://forums.swift.org/") else {
    throw someBadURLError
}
guard let choice = ["rock", "paper", "scissors"].randomElement() else {
    throw someEmptyCollectionError
}

I'm on the fence about it.

On one hand, the language should encourage people to replace "crash and burn" with explicit error handling. On the other hand, it might only shift people's "laziness" to using implicitly unwrapped optionals everywhere, which is even worse than force-unwrapping everything in my opinion.

I see force-unwrapping as a quick way to write an assertion: "I know this is not going to be nil so go on!"

Those "thoughtless" force-unwrap lets you concentrate on writing the actual code path. We could make them a bit more verbose, but I doubt it'll change the thought process around them.

10 Likes

My personal opinion on the matter is the opposite. With typed throws, Result and its flow-disruptive case handling would be replaced.

Regarding the title of this pitch, I'm against any future deprecation of the force unwrap operator. That would force users to verbosely add checks, guards and fatalErrors which can be quite distracting if you are writing code conceptually. Correct and safe error/exception handling usually comes in a later phase.

I would probably be fine with having warnings, even though it's something that a linter already does. I don't have a strong preference in having this behavior enforced by default in the language or as an opt-in with external linters.

2 Likes

My thinking has evolved in the more than four years since this was published. The passage of time has solidified a central role for ! in this language, and the cost of migrating code off of it has grown to a point that is absolutely untenable. This thread was written during the Swift 1 era when we had some hope of being able to change this and push people towards total operators like ?? or use a Rust-like spelling of these operations.

I also want to emphasize that the goal of that original thread was not to remove force unwrapping, but rather to deprecate the postfix bang operator in favor of its more explicit form. This is where this discussion departs most from the linked prior discussion.

It's generally a sign of laziness

Partiality is a necessary part of computing in this language. Its most common - and best - usage is preventing a program with a nullability invariant violation from continuing to execute in an undefined or unexpected state. Which actually twists this next bit in its favor

Swift has done a great job of preventing undefined behaviour

Force-unwrapping is not UB as Swift’s semantics are very much defined in the case of the failure of a force unwrap. Unless you are building in -Ounchecked, (which, please don’t unless you really really know what you’re doing) you will deterministically trap. Trapping on failure is always safe.

22 Likes

I agree it's late in the day for Swift to be deprecating things but I think a warning for now it would be an appropriate step to encourage people to move to a slightly more verbose alternative along the lines of what asserts used to be; generally accompanied by a message specific to the context of the failure. I'm thinking along the lines of making the following available:

public func unsafeUnwrap<T>(_ optional: T?, _ reason: String, file: String = #file, line: Int = #line) -> T {
    guard let unwrapped = optional else {
        fatalError("Forced unwrap for \(reason) at \(file):\(line) failed")
    }
    return unwrapped
}

That the behaviour is defined seems mute when the lack of friction in using the ! construct is likely causing apps to fail for users in practice.

1 Like

You mean safeUnwrap or just unwrap here: to emphasize, unwrapping is safe (in fact, it’s the paradigm of what Swift means by safe) and does not cause undefined behavior. There is unsafelyUnwrapped for that if it’s what you want.

To reiterate @codafi, removing or putting a warning on ! is a nonstarter.

I would go so far as to say that I disagree with opinions that users should avoid force unwrapping in favor of non-trapping alternatives in situations where nil isn’t thought possible: that’s the entire point of having an assertion and I would encourage people to use ! in those circumstances.

30 Likes

The operator exists because there isn’t a system in place to offer fast overloads for known state, paired with slower, kid gloves versions for the unknown. (e.g. we still have to maintain our own gloved version of subscripting Arrays.)

I have zero expectation this will change in Swift. It’ll be important in the successor though.

Lots and lots of noise exists in Swift code because of optionals and errors not being cut off precisely where everything afterwards becomes useless.

3 Likes

Thanks for the comments. I’m not sensing a groundswell of support but I felt the question needed to be asked. I’ve made a Swift micro-Package people can use and will move on:

I’ll just re-iterate the advantages of this over the forced unwrap operator.

  • It documents the assertion the programmer is making for future you and production support in the event of a crash.

  • It is easier to audit a codebase for than a single ‘!’ character.

  • It causes the developer to pause in providing their assertion to consider if it really is tenable.

If it were me, at this stage I’d add a compiler opt-in -force-unwrap=[warning,error] to start to lint out this construct. This would be particularly useful for server side use of Swift where taking out a entire server process is not an option (groan).

There is a larger debate about Swift’s, to me, particular notion of “safety" which I’ll not venture into. Determinism is one thing and it saves a lot of time during development but the disorderly exit of a program in production looses data (though at least it doesn’t corrupt it), cancels any asynchronous network requests and the net result will frequently be an uninstall. If a slight increase in the effort involved in unwrapping increased the likelihood exceptional conditions were handled in a less naïve way that would seem a win.

1 Like

I very much disagree with most statements about usability/readability. Here is a chunk of some legit production code (a rewrite of UITableView):

for i in checkedIndices {
                let prefHeight = heightSource(i)
                let delta = prefHeight - verticals[i]!.height
                if delta != 0 {
                    // Update the vertical
                    verticals[i]!.height = prefHeight
                    
                    // Modify all rows below
                    if let nextIndex = i.next(in: dimensions) {
                        for k in IndexRange(from: nextIndex, dimensions: dimensions) {
                            // FIXME: This might accumulate floating point error, adjust for pixel density
                            verticals[k]!.y += delta
                        }
                    }
      // ...

— with the suggested spelling, this will have to be rewritten into

for i in checkedIndices {
                let prefHeight = heightSource(i)
                let delta = prefHeight - unwrap(verticals[i], "Should be there").height
                if delta != 0 {
                    // Update the vertical
                    var vertical = unwrap(verticals[i], "Should be there")
                    vertical.height = prefHeight
                    verticals[i] = vertical
                    
                    // Modify all rows below
                    if let nextIndex = i.next(in: dimensions) {
                        for k in IndexRange(from: nextIndex, dimensions: dimensions) {
                            // FIXME: This might accumulate floating point error, adjust for pixel density
                            var vertical = unwrap(verticals[i], "Should be there")
                            vertical.y += delta
                            verticals[i] = vertical
                        }
                    }
      // ...

— and giving that it's impossible to return inout-like references, there is no way around copying and rewriting the (potentially large) structure that I need to modify in the dictionary.

Frankly, I really dislike the result. It also does not really at all make sense in this code to ever document the unwrapping, since it has already been made sure in my code that when this particular function runs, the assertion has been met — especially that all the unwrapping happens for the same key i and the item never gets deleted — so, while a comment like "Should be there" could be considered "lazy", there's simply nothing else to say. It is even much easier to see that verticals[i] never gets deleted in the original spelling, because all assignments happen to a member, not verticals[i] itself.

I hope that this piece serves as a strong example that there are legitimate cases where excessive spellings make the code quite obtuse and potentially even less performant.

More generally though, I personally don't quite understand why some particular topics (like force-unwrapping or unowned references) receive such strong attention, while a vast majority of other cases, where assertions are exactly as critical (for instance, division by zero), do not have such recognition. If one would ban force-unwrapping and maybe unowned references, then we either would have a very inconsistent language, where optionals are treated with much more piety than other constructs — or the whole dialogue should be held much more holistically, where one considers all possible trapping behaviour and each possible trap receives a personalized spelling.

1 Like

But why does verticals array contain an optional type if it can never be nil? Remove the optionality and a nil value can never be put into to it which would be when you want to trap. It's a dictionary? I'd be inclined to use a guard + continue.

Terms of Service

Privacy Policy

Cookie Policy