Swift 5中switch cases中"@unknown default"和"default"的区别

39

从 Swift 5 开始,新的 case 属性 @unknown 被引入。

在使用和不使用 @unknown 的情况下,确切的区别是什么?我们什么时候需要使用 @unknown 关键字?


1
请阅读SE-0192,并观看此视频,了解Swift 5中最重要的新闻。 - vadian
5个回答

23

来源于SE-0192:处理未来的枚举情况(重点是我的):

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()
}

8
作为开发者,我更希望在枚举类型中添加新值时使得编译失败,而不是只看到一个警告。我真的不理解 "@unknown" 的实用性。 - dor506
1
@dor506,我认为这个想法是允许苹果在未来的iOS更新中更新Swift标准库,而不会破坏针对早期版本编译的应用程序。我认为以前标准库包含在每个应用程序的二进制文件中。请查看SE-0192获取更多信息。 - Worth
@dor506,我也同意! - Ashok

18

如果只使用default,那么它将在我们的switch不匹配任何选项时使用。让我们来看一个最全面的案例:

enum Option {
  case A
  case B
}

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

这个示例是详尽的,我们不会遇到任何错误。但是如果我们需要在我们的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!")
  }
}
在这个第二个例子中,我们会收到一个错误 Switch must be exhaustive。为了避免这个错误,我们可以实现一个默认情况:
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!")
  }
}

如果用户选择了选项C,他将落入默认情况。但是当我们将选项D、E等添加到枚举中时会发生什么?如果我们不改变switch它们都将落入default。根据你想要实现的内容,这可能不是一个问题。

现在,使用@unknown,我们继续捕获所有其他选项,但这里的区别在于编译器会发出警告Switch must be exhaustive(不是错误!),如果枚举的所有已知元素没有匹配(即switch 不完整)。

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!")
  }
}

如果我们添加选项D、E等,我们只会看到一个警告,然后决定是否要实现其他情况(例如,我们想要为选项D和E设置自定义消息),或者我们是否要保留默认消息“您选择了另一个选项”。将其视为友好提醒而不是重大错误 :)

其他示例:https://www.raywenderlich.com/55728-what-s-new-in-swift-5


3
从理论上讲,这个新案例听起来很有用。但问题在于,现在你将拥有一个永久性的警告,指出默认值永远不会被使用,而我希望在我的项目中没有任何警告。 - AdamM
@AdamM 是的,但是你只需要通过在switch语句中添加另一个case来修复警告。 - matt

14

那些暗示你永远不会因为你的枚举而收到警告的回答是错误的。这是关于Swift如何处理外部库/框架中的C(和Objective-C)枚举的问题。一些Swift标准库枚举受到影响。

好的,那么让我们考虑一个实际的例子。我们针对Cocoa枚举编写一个详尽的switch语句:

    var err : [URLError.NetworkUnavailableReason] = ...
    switch err {
    case URLError.NetworkUnavailableReason.cellular: break
    case URLError.NetworkUnavailableReason.expensive: break
    case URLError.NetworkUnavailableReason.constrained: break
    }
此时我们收到一个警告。为什么?
因为我们的switch语句现在是穷尽的,但它可能并不总是穷尽的。如果框架以后添加了新的case怎么办?我们编译过的代码不会改变,所以当新的case进入switch时它会崩溃(陷阱)。
因此,我们需要一种方法来使我们的代码即使在框架更改后仍然继续工作。编译器因此告诉我们:“即使switch已经穷尽,也要添加一个默认case。”
当然,现在可以添加一个普通的默认case:
    switch err {
    case URLError.NetworkUnavailableReason.cellular: break
    case URLError.NetworkUnavailableReason.expensive: break
    case URLError.NetworkUnavailableReason.constrained: break
    default: break
    }

问题在于,如果该框架确实发生了变化,我们永远不会听到有关它的消息。因此有一个更好的方式:@unknown default

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

这意味着:"嘿,编译器,我不希望再出现其他的情况,但如果我将此项目针对框架进行编译并且你发现还有另外一种情况,请警告我,以便我可以明确地将其添加到switch语句中。"

这就是@unknown的特殊之处。如果背地里添加了另一个情况,编译器将会给我们另一个警告,告诉我们情况的变化,然后我们可以修复代码来包含它。换句话说,你现在服从警告现在,以便在未来可能获得有用的警告

这种语法的另一个好处是,如果我们将@unknown default添加到现在不完整的switch语句中,编译器将会警告我们关于那个


10

默认情况

每个switch语句都必须是全面的。也就是说,考虑到的类型的每个可能值都必须与switch case之一匹配。如果不适合为每个可能的值提供一个case,则可以定义一个默认情况来覆盖未明确处理的任何值。此默认情况由default关键字表示,并且必须始终出现在最后。

例如:

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")
}

开关语句的第一个case匹配英文字母表的第一个字母"a",第二个case匹配最后一个字母"z"。由于开关必须对每个可能的字符都有一个case,而不仅仅是每个字母字符,因此此开关语句使用一个默认case来匹配除"a"和"z"以外的所有字符。这项规定确保了开关语句是穷尽的。 @unknown 默认情况 来自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!

更多参考:Hacking with Swift


1
在@unknown默认之前的“case”会导致错误.. 不需要包含它。 - Mikrasya

1

编译器会在扩展案例的同时警告您。 如果您不使用此关键字并稍后扩展案例,则可能会忘记在各处更新代码。 此关键字有助于以后使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接