How to explain this?

I'm trying to explain why the following doesn't generate an error:

let input = 1
switch input
{
    case 0:
        let message = "Zero"
        print(message)
        
    case 1:
        let message = "One"
        print(message)
        fallthrough
        
    default:
        let message = "Something else"
        print(message)
}

It sure looks to me like this code assigns a value twice to immutable message.

Output is

One
Something else
1 Like

Here’s one possible explanation:

Each case of a switch statement constitutes its own scope, even though there are no curly braces around it. The fallthrough keyword essentially “nests” one of those scopes inside another.

Edit: actually “nests” isn’t quite right, because in a nested scope you can still access variables declared in an outer scope. So it’s more like fallthrough “jumps” from one scope to another (analogous to a function call, but of course it is not one).

9 Likes

It's no different than this. You're not assigning to the same location, you're introducing a new binding with the same name:

let message = "One"
print(message)

do {
  let message = "Something else"
  print(message)
}
7 Likes

You can re-work it so that you actually are assigning to the same binding:

let input = 1
let message: String
switch input {
case 1:
    message = "One"
    print(message)
    fallthrough
        
default:
    message = "Something else"
    print(message)
}

and then you get the expected error:

error: immutable value 'message' may only be initialized once
7 Likes

I was just trying to explain this behavior, not actually trying to do anything with it. Somewhere I had read that the scope of identifiers is limited to the switch statement (inside the curly braces). Apparently, that description is not true; instead, each case has its own scope (right?)

1 Like

It is yes different!

switch 1 {
case 1:
  let message = "One"
  do {
    print(message) // Compiles.
    let message = "Something else"
    print(message)
  }
  fallthrough
default: break
}
switch 1 {
case 1:
  let message = "One"
  fallthrough
default:
  print(message) // Compilesn't: Use of local variable 'message' before its declaration
  let message = "Something else"
  print(message)
}

You can add the braces in—as long as there's a do in front. It may be helpful to imagine them being there.

let input = 1
switch input {
case 0: do {
  let message = "Zero"
  print(message)
}
case 1: do {
  let message = "One"
  print(message)
  fallthrough
}
default: do {
  let message = "Something else"
  print(message)
}
}

do can model this as well, with scoping coming from if statements, if fallthrough only goes to default:

let input = 1
`switch`: do {
  if case 0 = input {
    let message = "Zero"
    print(message)
    break `switch` // `break` must be explicit, like in some other programming languages.
  }
  if case 1 = input {
    let message = "One"
    print(message)
    // Lack of `break` means "fallthrough".
  }
  `default`: do { // This `do` isn't necessary but it's illustrative.
    let message = "Something else"
    print(message)
  }
}
1 Like

While I couldn’t find authoritative documentation outlining what we can clearly see empirically, The Swift Programming Language: Control Flow: Switch does say:

Like the body of an if statement, each case is a separate branch of code execution.

That might not be quite as explicit as “each case is a separate code block with its own local variables”, but it is consistent with that behavior.


Unrelated to the question of local scope of the individual case clauses, I would be inclined to use a switch expression:

let message = switch input {
    case 0: "Zero"
    case 1: fallthrough
    default: "Something else"
}

print(message)

This syntax not only avoids duplicative print statements, but avoids the ambiguity whether the case will return a value or fallthrough. It can only be one or the other.