How to avoid force unwrapping a variable?
Asked Answered
R

5

32

How do I avoid using the ! operation doing a force unwrap as using this is usually a bad option.

What is the better option with code like the following where using it makes the code look simpler and because of the if check the variable ! is called on will never be nil and so cannot crash.

My instructor introduced us to the bang(!) operator and then told us to never use it again. Telling us why of course, that it will crash our app if the optional is nil.

However I find myself in situations like these where the bang operator seems to be the most concise and safe option.

func fullName() -> String {
    if middleName == nil {
        return "\(firstName) \(lastName)"
    }else{
        return "\(firstName) \(middleName!) \(lastName)"
    }
}

Is there a better way to do something like this?

Also, here's the full class if anybody is wondering.

class CPerson{
    var firstName: String
    var middleName: String?
    var lastName: String

    init(firstName: String, middleName: String?, lastName: String) {
        self.firstName = firstName
        self.middleName = middleName
        self.lastName = lastName
    }
    convenience init(firstName: String, lastName: String) {
        self.init(firstName: firstName, middleName: nil, lastName: lastName)
    }
    func fullName() -> String {
        if middleName == nil {
            return "\(firstName) \(lastName)"
        }else{
            return "\(firstName) \(middleName!) \(lastName)"
        }
    }
}

My instructor said "If I see you using the bang operator, we're going to fight" O_O

Rotterdam answered 12/9, 2016 at 16:50 Comment(1)
Please edit to include both versions of code -- with and without "bang"Pidgin
E
28

Use the if let or guard constructs:

func fullName() -> String {
    if let middleName = middleName {
        return "\(firstName) \(middleName) \(lastName)"

    } else {
        return "\(firstName) \(lastName)"
    }
}

func fullName() -> String {
    guard let middleName = middleName else {
        return "\(firstName) \(lastName)"
    }
    return "\(firstName) \(middleName) \(lastName)"
}

I've put the guard statement in for completeness but as others have commented this is more commonly used in an error/failure case.

I would also advise against using string interpolation for Strings. They are already strings, there is no need to use the description of each name in a new string.

Consider return firstName + " " + lastName. See Difference between String interpolation and String initializer in Swift for cases when string interpolation could return an unexpected result.

Eba answered 12/9, 2016 at 16:54 Comment(15)
Be very careful with the string + " " + string syntax. This can cause compile times to explode. Chained + operators very often lead to O(n!) time to figure out the correct types. It's probably the most common cause of massive compile times I see in the wild. Interpolation compiles much, much faster.Horton
To me guard implies a failure or error case. And, you could have multiple of them separately. I.e. guard someRequiredValue != nil else { return "error" } guard anotherCheck != nil else { return "error" } return "normal return value"Midian
@MattHorst Did you see my update about guard? I put it in to show how it works with the disclaimer that it is more commonly used in an error/failure case.Eba
@Eba Oops, I hadn't. Thanks for that. It does make sense to include it given it's close relationship to optional binding!Midian
@RobNapier do you mean doing " \(string1) \(string2) is better" ? Can you explain a little bit more in your own answer or here?Dacy
@Honey For concatenating exactly two strings (like in your example), + works fine. It probably ok if there are two + in the expression. By the time you get to three or four + in a single expression, the compile times can explode. That's the nature of O(n!). It grows very, very fast.Horton
@RobNapier I feel like I lack a fundamental understanding of some concept I'm not aware of its existence :|. Can you please share a reference or tutorial? But if I understand you correctly when you say to "figure out correct types" <-- **the concept I think I am clueless of **. Isn't it already a String itself? ie simple String conversion of O(1) for each so if we have 10 Strings it would be 10 actions not 10!? I mean I see the them independent but you seem to be suggesting as if the compiler has to find one type that is universal among them all.Dacy
@Honey The + function is heavily overloaded to handle many different types and protocols. The compiler winds up searching through many combinations of types trying to find the one that matches the types provided. See swiftdoc.org/v2.2/operator/pls In some cases it works out fine, especially in small code snippets, but it is a very, very common cause of combinatorial explosion in the type checker. Not saying it should be this way; it's a problem with the compiler. But just last week I had two different teams complain about crazy build times, and it turned out to be a + chain.Horton
If it doesn't cause you trouble, then it's fine, but if your build times seem really sluggish, it's always the first thing I look for.Horton
@RobNapier Ohhhk! Thank you so much. So I just saw 27 funcs in the link. Does that mean every time it will keep trying them one by one until it finds a function that works. So if we have 3 instances of + it could potentially try it 27*27*27 times?Dacy
@Honey It can actually be a bit worse than that. For example, it can try converting each element to an Optional (since this can be done implicitly), and then it has to figure out if String? conforms to any of the given protocols. This is why many implicit conversions were removed & you're not allowed to create new ones. Every possible implicit conversion combination has to be chased down to see if it could make this thing type-check. I believe this is fixable for the trivial case (just concatenating known static strings), but the least cleverness and it can explode.Horton
There is also a runtime cost to +, since it has to create all the intermediate strings and then immediately them away. + is just a function. I don't believe the compiler currently special-cases string concatenation (in principle it could, but in practice that may be very difficult since it could be overloaded in other ways).Horton
@RobNapier Super thanks. So just to be sure, you're referring to this Abolish ImplicitlyUnwrappedOptional type. Which has 2 benefits: Faster build time and avoidance of unintended implicit unwrappingDacy
@Honey no, that's different. Even in Swift 3, any type can be implicitly promoted to an Optional of that type. This allows you to pass 1 rather than Optional(1) to a function that takes an Int?.Horton
@RobNapier and others interested a wrap of our discussion about huge compiler time for string concatenation discussion with Rob is written in my answer hereDacy
H
17

Your instructor is, broadly speaking, correct. Definitely in this case. There's no reason for making this so special case and forcing duplicated code.

func fullName() -> String {
    return [firstName, middleName, lastName] // The components
        .flatMap{$0}            // Remove any that are nil
        .joined(separator: " ") // Join them up
}

This just joins all the non-nil parts of the name with spaces. The other answers here are also fine, but they don't scale as well to adding more optional pieces (like a "Mr." title or "Jr." suffix).

(This is in Swift3 syntax. Swift 2 is very similar, it's joinWithSeparator(_:) instead.)

Horton answered 12/9, 2016 at 16:56 Comment(5)
Thank you Rob. If I could accept two answers, I would also accept yours.Rotterdam
Obviously this restricts what kind of formatting you have. If you want to output "Smith, John James" or "Smith, John", then this doesn't work. And if a small change in the specification requires a large change in the code, then your code might not be right. With the original code, that change would be trivial.Highboy
@Highboy True. It is difficult to write this code in a way that is robust against many different changes that might happen. One way favors adding random new fields (particularly optional ones), but fails if the format becomes inconsistent. The other favors changes in the format in random ways, but becomes very complex to handle many optional fields. A good example of how it making something easy to extend is often much more difficult than it sounds; there is no one answer that covers all the bases.Horton
so would you never ever want your app to crash? Or you would never force unwrap?? Can you make your answer more generic? Won't you force unwrap IBOutlets?Dacy
@Honey There are many places I would crash the app on purpose, but I use precondition or fatalError for those cases in order to make my intent clear and to provide an explanation (especially since I use custom wrappers that automatically log prior to crash to Loggly and Crashlytics; you can't do that with !). ! will crash, but gives you little context to work with in the crash report. IBOutlet should generally be implicitly unwrapped optionals (UILabel!) so there is no need to add a ! at usage. I'm not saying that you would never ever force unwrap, but very rare.Horton
M
5

What you did will work and indeed, once you know it's not nil, using ! is a correct way to force unwrap it.

However, Swift has feature called Optional Binding (https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/TheBasics.html).

In your case it would be like this:

func fullName() -> String {
        if let middle = middleName {
            return "\(firstName) \(middleName) \(lastName)"
        } else {
            return "\(firstName) \(lastName)"
        }
    }

What the optional binding does is, as Apple says in the above link, "You use optional binding to find out whether an optional contains a value, and if so, to make that value available as a temporary constant or variable". So inside your brackets you have access to middle and can use it as a known non-nil.

You can also chain these so that you have access to multiple temporary constants and don't need to chain if statements. And you can even use a where clause to evaluate an optional condition. Again from Apple's docs above:

if let firstNumber = Int("4"), secondNumber = Int("42") where firstNumber < secondNumber {
    print("\(firstNumber) < \(secondNumber)")
}
Midian answered 12/9, 2016 at 16:54 Comment(1)
I agree with the other posts that you don't need to use a if check like this, for this case of string concatenation. However, I do think how I have it above is better than using guard. To me guard would imply an error/failure case.Midian
C
1

Before unwrapping an optional variable you should must check for nil otherwise your app will crash if variable contain nil. And checks can be performed in several ways like:

  1. if let
  2. guard
  3. if-else
  4. using ternary operator
  5. Nil coalescing operator

And which one to use completely depends on requirement.

You can just replace this code

if middleName == nil {
    return "\(firstName) \(lastName)"
}else{
    return "\(firstName) \(middleName!) \(lastName)"
}

by

return "\(firstName)\(middleName != nil ? " \(middleName!) " : " " )\(lastName)"

OR

you can also use Nil coalescing operator (a ?? b) unwraps an optional a if it contains a value, or returns a default value b if a is nil.

Camshaft answered 12/9, 2016 at 17:47 Comment(0)
H
1

In this case, your instructor is wrong. Your function is absolutely safe. middleName will not change from not nil to nil behind your back. Your function may crash if you make some spelling error, and type the name of a different variable instead of middleName, but that would be a bug anyway and the crash would lead you to the bug.

But usually "if let ... " is the better way to handle this, because it combines the test and the unwrapping.

There are also situations where you don't say "if it's nil, it will crash, that's bad" but "if it's nil then I want it to crash" (usually because you know it can only be nil if there is a bug somewhere in your code). In that case ! does exactly what you want.

Highboy answered 12/9, 2016 at 22:53 Comment(2)
If there's a "better way to handle it" then it would seem his instructor isn't wrong after all..Seemly
He says "usually" there's a "better way" and "in this case" the instructor is wrong.Campagna

© 2022 - 2024 — McMap. All rights reserved.