-->

Difference between switch cases “@unknown default”

2020-05-25 11:15发布

问题:

From Swift 5, new case attribute @unknown is introduced.

What is the exact difference when @unknown is being used and not being used? In which case we have to use @unknown keyword?

回答1:

From SE-0192: Handling Future Enum Cases (emphasis mine):

When switching over a non-frozen enum, the switch statement that matches against it must include a catch-all case (usually default or an "ignore" _ pattern).

switch excuse {
case .eatenByPet:
  // …
case .thoughtItWasDueNextWeek:
  // …
}

Failure to do so will produce a warning in Swift 5. A program will trap at run time if an unknown enum case is actually encountered.

All other uses of enums (if case, creation, accessing members, etc) do not change. Only the exhaustiveness checking of switches is affected by the frozen/non-frozen distinction. Non-exhaustive switches over frozen enums (and boolean values) will continue to be invalid in all language modes.

Here's a more complicated example:

switch (excuse, notifiedTeacherBeforeDeadline) {
case (.eatenByPet, true):
  // …
case (.thoughtItWasDueNextWeek, true):
  // …
case (_, false):
  // …
}

This switch handles all known patterns, but still doesn't account for the possibility of a new enum case when the second tuple element is true. This should result in a warning in Swift 5, like the first example.

@unknown

The downside of using a default case is that the compiler can no longer alert a developer that a particular enum has elements that aren't explicitly handled in the switch. To remedy this, switch cases will gain a new attribute, @unknown.

switch excuse {
case .eatenByPet:
  // …
case .thoughtItWasDueNextWeek:
  // …
@unknown default:
  // …
}

Like the regular default, @unknown default matches any value; it is a "catch-all" case. However, the compiler will produce a warning if all known elements of the enum have not already been matched. This is a warning rather than an error so that adding new elements to the enum remains a source-compatible change. (This is also why @unknown default matches any value rather than just those not seen at compile-time.)

@unknown may only be applied to default or a case consisting of the single pattern _. Even in the latter case, @unknown must be used with the last case in a switch. This restriction is discussed further in the "unknown patterns" section under "Future directions".

The compiler will warn if all enums in the pattern being matched by @unknown are explicitly annotated as frozen, or if there are no enums in the pattern at all. This is a warning rather than an error so that annotating an enum as frozen remains a source-compatible change. If the pattern contains any enums that are implicitly frozen (i.e. because it is a user-defined Swift enum), @unknown is permitted, in order to make it easier to adapt to newly-added cases.

@unknown has a downside that it is not testable, since there is no way to create an enum value that does not match any known cases, and there wouldn't be a safe way to use it if there was one. However, combining @unknown with other cases using fallthrough can get the effect of following another case's behavior while still getting compiler warnings for new cases.

switch excuse {
case .eatenByPet:
  showCutePicturesOfPet()

case .thoughtItWasDueNextWeek:
  fallthrough
@unknown default:
  askForDueDateExtension()
}


回答2:

default case

Every switch statement must be exhaustive. That is, every possible value of the type being considered must be matched by one of the switch cases. If it’s not appropriate to provide a case for every possible value, you can define a default case to cover any values that are not addressed explicitly. This default case is indicated by the default keyword, and must always appear last.

For example:

let someCharacter: Character = "z"
switch someCharacter {
case "a":
    print("The first letter of the alphabet")
case "z":
    print("The last letter of the alphabet")
default:
    print("Some other character")
}

The switch statement’s first case matches the first letter of the English alphabet, a, and its second case matches the last letter, z. Because the switch must have a case for every possible character, not just every alphabetic character, this switch statement uses a default case to match all characters other than a and z. This provision ensures that the switch statement is exhaustive

@unknown default case

From Reinder's blog post on "What's New In Swift 5.0":

In Swift 5.0, a new @unknown keyword can be added to the default switch case. This doesn’t change the behavior of default, so this case will still match any cases that aren’t handled in the rest of the switch block.

switch fruit {
case .apple:
    ... 
@unknown default:
    print("We don't sell that kind of fruit here.")
}

The @unknown keyword will trigger a warning in Xcode if you’re dealing with a potentially non-exhaustive switch statement, because of a changed enumeration. You can deliberately consider this new case, thanks to the warning, which wasn’t possible with just default.

And the good thing is that due to how default works, your code won’t break if new cases are added to the enum – but you do get warned. Neat!

more Ref: Hacking with Swift



回答3:

The answers that imply that you will ever get a warning for your enums are wrong. This is about how Swift treats C (and Objective-C) enums in an external library/framework. A few Swift standard library enums are affected.

Okay, so let's consider an actual example. We write an exhaustive switch against a Cocoa enum:

    var err : [URLError.NetworkUnavailableReason] = ...
    switch err {
    case URLError.NetworkUnavailableReason.cellular: break
    case URLError.NetworkUnavailableReason.expensive: break
    case URLError.NetworkUnavailableReason.constrained: break
    }

At this point we get a warning. Why?

Well, our switch is exhaustive now, but it might not always be exhaustive. What if the framework adds cases later? Our compiled code won't change, and so it will crash (trap) when the new case arrives into the switch.

So we need a way to allow our code to keep working even if the framework changes. The compiler therefore is telling us: "Add a default case, even though the switch is exhaustive."

Now, of course it would be possible to add an ordinary default case:

    switch err {
    case URLError.NetworkUnavailableReason.cellular: break
    case URLError.NetworkUnavailableReason.expensive: break
    case URLError.NetworkUnavailableReason.constrained: break
    default: break
    }

The problem with that is if the framework does change, we'll never hear about it. So there's a better way, @unknown default:

    switch err {
    case URLError.NetworkUnavailableReason.cellular: break
    case URLError.NetworkUnavailableReason.expensive: break
    case URLError.NetworkUnavailableReason.constrained: break
    @unknown default: break
    }

This means: "Hey, compiler, I don't expect there to be any more cases, but if I ever try to compile this project against the framework and you discover that there is another case, warn me so that I can add it explicitly to my switch."

So that's what's special about @unknown. If another case is added behind our backs, the compiler will give us another warning telling us about it, and we can fix our code to include it. In other words, you obey the warning now to get rid of the warning now in exchange for a possible useful warning in the future.

Another nice thing about this syntax is that if we add an @unknown default to a switch that is not exhaustive now, the compiler will warn us about that.



回答4:

In the case of only using default, it's used as when our switch doesn't match any of the options. Let's see a first exhaustive case:

enum Option {
  case A
  case B
}

func optionSelected(option: Option) {
  switch(option) {
    case .A:
      print("You chose A!")
    case .B:
      print("You chose B!")
  }
}

This example is exhaustive and we won't get any error. But what if we need to add options in our enum?

enum Option {
  case A
  case B
  case C
}

func optionSelected(option: Option) {
  switch(option) {
    case .A:
      print("You chose A!")
    case .B:
      print("You chose B!")
  }
}

In this second example, we will get an error Switch must be exhaustive. To avoid this error, we might implement a default case:

enum Option {
  case A
  case B
  case C
}

func optionSelected(option: Option) {
  switch(option) {
    case .A:
      print("You chose A!")
    case .B:
      print("You chose B!")
    default:
      print("You chose other option!")
  }
}

If the user chose, Option C, he will fall into the default case. But what happens when we add an Option D, E, etc. into the Enum? If we don't change the switch they will all fall into default. This might not be a problem, depending on what you want to implement.

Now, with the @unknown, we continue catching all the other options, but the difference here is that the compiler we issue a warning Switch must be exhaustive (not an error!) if all known elements of the enum haven't been matched (i.e. the switch wasn't exhaustive).

enum Option2 {
  case A
  case B
  case C
}

func optionSelected2(option: Option2) {
  switch(option) {
    case .A:
      print("You chose A!")
    case .B:
      print("You chose B!")
    case .C:
      print("You chose C!")
    @unknown default:
      print("You chose other option!")
  }
}

If we add an option D, E, etc, we will just see a warning and then decide if we want to implement the other cases (for example, we want a custom message for option D and E) or if we will just leave the default message "you chose another option". Think of it as a friendly remainder instead of a big red error :)

Other examples: https://www.raywenderlich.com/55728-what-s-new-in-swift-5



标签: swift swift5