13

In Swift, you can use if let optional binding to unwrap an optional into a constant or variable with the same name:

func test()
{
  let a: Int? = 1

  if let a = a {
    print("a = \(a)")
  } 
}

For everything inside the if let statement, the optional a is unwrapped into a regular int.

Likewise, I can use a guard statement to achieve a similar effect

func test()
{
  let a: Int? = 1

  guard let requiredA = a else{
    return
  }
  print("a = \(requiredA)")
}

However, I can't use code like this: guard let a = a else:

func test()
{
  let a: Int? = 1

  guard let a = a else{
    return
  }
  print("a = \(a)")
}

Why not?

In a guard statement, if the conditional of the guard statement fails, the else clause is executed and you exit the current scope. If the conditional succeeds, a new variable/constant is created from guard statement's closing brace to the end of the current scope.

Why can't I do the same trick of mapping an optional into a variable/constant with the same name for remainder of the current scope?

P.S.: I realize this question isn't a perfect fit for this site. I'm open to suggestions as to where would be a better place for this question.

Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • 1
    Looks to me like the `if` way creates two variables in different scopes, which is fine, while the `guard` way would be creating two identically-named variables in the same scope, which would be a weird special case and complicate the variable resolution rules to allow it. I'm guessing, though; I don't know Swift. – user2357112 Nov 01 '16 at 03:19

1 Answers1

16

The reason you can't do this:

func test()
{
  let a: Int? = 1

  guard let a = a else{
    return
  }
  print("a = \(a)")
}

is because guard creates the new variable in the same scope, thus you have two variables called a in the same scope. One is an Int and the other is an Int?. That is not allowed.

The error that you get Definition conflicts with previous value is exactly the same as if you had done this:

func test()
{
    let a: Int? = 1

    let a = a!
}

Compare that with:

func test()
{
    let a: Int? = 1

    if let a = a {
        print("a = \(a)")
    }
}

In this case, the new variable a which is an Int exists only in the new scope of the if's then clause, so this works.


From the comments:

But I submit to you that the section of code after the closing brace and to the end of the enclosing scope is actually an inner scope.

I can understand that you would like it to be so, but it isn't. If that were the case, then you could do this, but it too gives an error:

func test()
{
    let a: Int? = 1

    guard let b = a else{
        return
    }
    print("b = \(b)")

    let a = 5  // Definition conflicts with previous value

    print("a = \(a)")
}

The beauty of guard is that it doesn't create new scopes and you avoid creating the pyramid of death that results when you repeatedly use if let to unwrap optionals (and in the process create new scopes).


See the follow-up question When did guard let foo = foo become legal? for more insight on this topic.

Community
  • 1
  • 1
vacawama
  • 150,663
  • 30
  • 266
  • 294
  • But I submit to you that the section of code after the closing brace and to the end of the enclosing scope is actually an inner scope. If you create a new variable/constant, it exists from that point until the end of the current scope. How is that different from `if let`, where inside the body of the statement, the local definition of `a` overrides the outer definition? – Duncan C Nov 01 '16 at 03:29
  • It is different than `if let` because `if let` the `{ }` which follows `if let` creates a new scope. The beauty of `guard` is that you don't have `{ }` and thus avoid the pyramid of death of new scopes. – vacawama Nov 01 '16 at 03:36
  • @DuncanC `if let` is quite different. Try this: `if let b = a { ... } let b = 4`. The 2nd `b` is fine because the `b` from the `if let` is no longer in scope. – rmaddy Nov 01 '16 at 04:31
  • 1
    Yeah, I guess that makes sense. It makes the pattern you use different than `if let`, and leads to some awkward naming, like `reqiredA` in my example. – Duncan C Nov 01 '16 at 12:14
  • @DuncanC Or just use `if let a = a { rest of code }` instead of `guard... return`. – rmaddy Nov 01 '16 at 18:36
  • 1
    Sure, but that ends up with ever-increasing levels of nesting, and doesn't give you the "golden path" advantages that you get with `guard` – Duncan C Nov 01 '16 at 18:52
  • Got the same question here https://stackoverflow.com/questions/58226900/swift-cant-unwrap-an-optional-inside-a-loop but I can't understand why it is not working event with this nice explanation. – Louis Lac Oct 03 '19 at 21:53
  • Actually, I can do that code you posted with the `guard` and it runs without problems in XCode 13.1 – MrAn3 Jan 05 '22 at 23:04
  • @vacawama I think the first example form your question get compiled (now) but the variable `a` is unwrapped in its initial scope – Mike.R Apr 22 '22 at 14:11