在switch语句中使用'@unknown default'的实例

279 阅读4分钟

当在一个枚举上进行切换时,我们需要明确地处理它的所有情况,或者提供一个default ,作为之前任何case 语句没有匹配的情况的回退。例如,像这样的例子:

struct Article {
    enum State {
        case draft
        case published
        case removed
    }

    var state: State
    ...
}

// Explicitly handling all possible cases:

func articleIconColor(forState state: Article.State) -> Color {
    switch state {
    case .draft:
        return .yellow
    case .published:
        return .green
    case .removed:
        return .red
    }
}

// Using a default case:

extension Article {
    var isDraft: Bool {
        switch state {
        case .draft:
            return true
        default:
            return false
        }
    }
}

有趣的是:我们也可以用default ,而不是用case _ ,来匹配一个switch语句中所有可能的模式。这两者在功能上是相同的,因为_ 在 Swift 的模式匹配系统中充当了 "通配符"。

在可能的情况下,我总是建议编写详尽的switch 语句,明确地处理每一种可能的情况,即使这涉及到多一点的代码。原因是,如果我们将来增加一个新的案例,这样做会给我们带来编译器错误,这反过来又 "迫使我们 "做出正确的决定,如何在我们的代码库中处理这个新案例。默认情况可能很方便,但是当一段旧的代码最终要处理一个枚举的情况时,它们很快就会成为一个常见的错误来源,而它并不是被设计来处理的,仅仅是因为它是在一个default 语句中被调用的。

所以这里是我个人实现上述isDraft 属性的方法:

extension Article {
    var isDraft: Bool {
        switch state {
        case .draft:
            return true
        case .published, .removed:
            return false
        }
    }
}

然而,在处理某些系统提供的枚举时,我们有时需要使用default ,因为这些枚举可能在任何时候被更新为新的情况。例如,如果我们试图详尽地切换到像UIKit的UIUserInterfaceStyle 枚举上,那么编译器会给我们一个警告:

extension UITraitEnvironment {
    var isUsingDarkMode: Bool {
        // ⚠️ Warning: Switch covers known cases, but
        // 'UIUserInterfaceStyle' may have additional unknown
        // values, possibly added in future versions.
        switch traitCollection.userInterfaceStyle {
        case .dark:
            return true
        case .light, .unspecified:
            return false
        }
    }
}

解决上述警告的一个方法当然是使用标准的default 语句(因为这将捕捉到未来可能添加的任何额外情况),但这样我们又回到了那种情况,我们的代码在处理这些新情况时可能最终做错了事。

值得庆幸的是,Swift对这个问题有一个内置的解决方案,那就是@unknown 属性,它可以附加到defaultcase _ 语句中,以便处理任何在我们编写代码时未知的情况,同时如果我们忘记处理一个现有的情况,仍然会产生警告。下面是我们如何将该属性应用于上述isUsingDarkMode 的实现:

extension UITraitEnvironment {
    var isUsingDarkMode: Bool {
        switch traitCollection.userInterfaceStyle {
        case .dark:
            return true
        case .light, .unspecified:
            return false
        @unknown default:
            return false
        }
    }
}

请注意,我们需要把我们的@unknown default 案例写成一个单独的语句,而不是把它和另一个导致相同结果的案例结合起来。绕过这个限制的一个方法是使用fallthrough 关键字,这将使 Swift 自动转移到我们的switch 语句中的下一个案例 - 像这样:

extension UITraitEnvironment {
    var isUsingDarkMode: Bool {
        switch traitCollection.userInterfaceStyle {
        case .dark:
            return true
        case .light, .unspecified:
            fallthrough
        @unknown default:
            return false
        }
    }
}

以上并不是我个人真正使用的模式,因为我更喜欢将我的@unknown default 回退代码与处理已知案例的代码明确分开,但我仍然认为值得一提的是这一技术。

那么,为什么只有在某些特定的枚举上进行切换时才需要(或者至少是推荐@unknown default 语句呢?这就是冻结枚举概念的来历。当一个枚举被标记为冻结时,它告诉编译器它永远不会(或至少不应该)获得任何新的情况,这意味着我们可以安全地详尽地切换它的情况而不需要处理任何未知的情况。

例如,如果我们看一下Foundation的ComparisonResult 枚举的Swift接口是什么样子的,那么我们可以看到它被标记为@frozen 属性。

@frozen public enum ComparisonResult: Int {
    case orderedAscending = -1
    case orderedSame = 0
    case orderedDescending = 1
}

这意味着我们可以自由地切换到ComparisonResult 值(以及其他类似的值),而不会被警告说我们应该添加一个@unknown default 的情况。

那么,这是否意味着我们也应该为我们自己的枚举添加同样的@frozen 属性呢?并非如此,因为编译器会自动将所有用户定义的Swift枚举默认为冻结。然而,如果我们使用的是我们在Objective-C中定义的枚举,那么如果我们想让它们在导入Swift时变成冻结的,我们就必须明确地将它们标记为 "封闭的可扩展性"。

typedef NS_ENUM(NSInteger, SXSArticleState) {
    SXSArticleStateDraft,
    SXSArticleStatePublished,
    SXSArticleStateRemoved
} __attribute__((enum_extensibility(closed))) NS_SWIFT_NAME(ArticleState);

另外,我们可以用NS_CLOSED_ENUM 替换NS_ENUM ,让上述可扩展性属性自动应用。

有了上述做法,我们的SXSArticleState 类型现在将作为一个冻结的枚举被导入 Swift,称为ArticleState ,它的工作方式与我们早期的、Swift 本地的Article.State 枚举完全一样。

谢谢你的阅读!