Ilea Cristian
Ilea Cristian

Reputation: 5831

if let acting weird when explicitly specifying type

Let's say we have:

let a:Int? = nil

// block not executed - unwapping done, type is inferred
if let unwrapped_a = a {
    println(unwrapped_a)
}

// block not executed - unwrapping done, type is specified
if let unwrapped_a:Int = a {
    println(unwrapped_a)
}

// block gets executed - unwrapping not done, new local constant + assignment is done instead? 
if let not_unwrapped_a:Int? = a {
    println(not_unwrapped_a)
}

So should I assume that Swift does an unwrapping in the first case, but an assignment in the second case?

Isn't this syntax a bit too close to create confusion? I mean, yes, the compiler warns you that you're using an optional type when working with not_unwrapped_a, but still.

Update:

So after Airspeed Velocity's answer I found another (but actually the same) weird case:

if let not_unwrapped_a:Int???? = a {
    println(not_unwrapped_a)
}

a will be silently wrapped in an Int????. So it will be a type of Int????? (five) - because a was already an optional. And then it will get unwrapped once.

Upvotes: 6

Views: 353

Answers (2)

Airspeed Velocity
Airspeed Velocity

Reputation: 40965

Case 1 and case 2 are identical – they are both assignments of the contents of a to a new variable. The only difference is you are leaving Swift to infer the type of unwrapped_a in option 1, whereas you’re manually giving the type in option 2. The main reason you’d ever need to do option 2 is if the source value were ambiguous – for example if it were an overloaded function that could return multiple types.

Case 3 is quite interesting.

Whenever you have a value, Swift will always be willing to silently upgrade it to an optional wrapping the value, if it helps make the types match and the code compile. Swift auto-upgrading of types is fairly rare (it won’t implicitly upgrade an Int16 to an Int32 for example) but values to optionals is an exception.

This means you can pass values wherever an optional is needed without having to bother to wrap it:

func f(maybe: Int?) { ... }

let i = 1

// you can just pass a straight value:
f(i)

// Swift will turn this into this:
f(Optional(i))

So in your final example, you’ve told Swift you want not_unwrapped_a to be an Int?. But it’s part of a let that requires a to be unwrapped before it’s assigned to it.

Presented with this, the only way Swift can make it work is to implicitly wrap a in another optional, so that’s what it does. Now it is an optional containing an optional containing nil. That is not a nil-valued optional - that is an optional containing a value (of an optional containing nil). Unwrapping that gives you an optional containing nil. It seems like nothing happened. But it did - it got wrapped a second time, then unwrapped once.

You can see this in action if you compile your sample code with swiftc -dump-ast source.swift. You’ll see the phrase inject_into_optional implicit type='Int??’. The Int?? is an optional containing an optional.

Optionals containing optionals aren’t obscure edge cases - they can happen easily. For example if you ever for…in over an array containing optionals, or used a subscript to get a value out of a dictionary that contained optionals, then optionals of optionals have been involved in that process.

Another way of thinking about this is if you think of if let x = y { } as kind of* like a function, if_let, defined as follows:

func if_let<T>(optVal: T?, block: T->()) {
    if optVal != nil {
        block(optVal!)
    }
}

Now imagine if you supplied a block that took an Int? – that is, T would be an Int?. So T? would be a Int??. When you passed a regular Int? into if_let along with that block, Swift would then implicitly upgrade it to an Int?? to make it compile. That’s essentially what’s happening with the if let not_unwrapped_a:Int?.

I agree, the implicit optional upgrade can sometimes be surprising (even more surprising is that Swift will upgrade functions that return optionals, i.e. if a function takes an (Int)->Int?, it will upgrade an (Int)->Int to return an optional instead). But presumably the feeling is the potential confusion is worth it for the convenience in this case.

* only kind of

Upvotes: 5

Antonio
Antonio

Reputation: 72760

The purpose of the optional binding is to check an optional for not nil, unwrap and assign to a non optional of the type enclosed in the optional. So in my opinion the 1st case is the correct way of using it - the only variation I would use is adding an optional cast (useful for example when the optional contains AnyObject).

I wouldn't use the 2nd case, preferring an optional cast:

if let unwrapped_a = a as? Int { ... }

unless, as noted by @drewag in comments, the type is explicitly specified to avoid ambiguities when it's not clear.

In my opinion the 3rd case should generate a compilation error. I don't see any use of optional binding to assign an optional to another optional. Optional binding is not a generic inline assignment (such as if let x = 5 {...}), so conceptually it should not work if the left side is an optional - it should be handled the same way as if the right side is not an optional (for which compilation fails instead).

Upvotes: 4

Related Questions