Swift中的可选项(Optionals)

Swift中的可选项(Optionals)

原文地址 hudson 译

Swift整体设计的一个关键部分是它如何要求我们明确处理可能缺失或可选的值。虽然这一要求通常迫使我们更彻底地思考如何构建对象和管理状态------也可以减少未处理的运行时错误。

在Swift中,通过在其类型后添加问号将值标记为可选值,这反过来又要求我们首先打开它。然后才能以任何具体方式使用它。例如,在这里,使用if let语句来解包可选的User值,以确定用户是否已登录 :

swift 复制代码
func setupApp(forUser user: User?) {
    if let user = user {
        showHomeScreen(for: user)
    } else {
        showLoginScreen()
    }
}

虽然有多种方法可以解包和处理可选值,但另一种非常常见的模式是使用guard语句尽早返回,以防可选值缺少。以下是重构上述函数后的 guard 版本:

swift 复制代码
func setupApp(forUser user: User?) {
    guard let user = user else {
        // Guard statements require us to "exit" out of the
        // current scope, for example by returning:
        return showLoginScreen()
    }

    showHomeScreen(for: user)
}

从本质上讲,可选项提供了一种表示"值缺失"的内部方式------如果需要模拟某种形式的默认或缺失状态,可选项是的理想选择。

例如,在这里,我们创建了一个Relationship 枚举,表达应用程序的两个用户之间的关系。其中使用none表示缺乏任何关系, 也是默认值:

swift 复制代码
enum Relationship {
    case none
    case friend
    case family
    case coworker
}

struct User {
    var name: String
    var relationship: Relationship = .none
    ...
}

上述代码在技术上是有效的,但最好的方案是将relationship属性作为可选项来实现。这样,就可以使用Swift的内置方式来表示缺乏任何关系,既能使用if letguard等机制,又能删除枚举中的none,使枚举得到简化 :

swift 复制代码
enum Relationship {
    case friend
    case family
    case coworker
}

struct User {
    var name: String
    var relationship: Relationship? = nil
    ...
}

然而,如果不小心,可选项也可能成为模棱两可的根源。 在实现给定值之前,考虑其是否实际上是可选的,这始终很重要------因为如果代码需要一个值才能运行,理想情况下,应该能够保证它始终存在。

例如,看看一个视图控制器实现,它目前使用可选项来存储其子视图------headerViewlogOutButton------以便在系统调用viewDidLoad()后延迟创建它们,这是构建基于视图控制器的UI的推荐方法:

swift 复制代码
class ProfileViewController: UIViewController {
    private var headerView: HeaderView?
    private var logOutButton: UIButton?

    override func viewDidLoad() {
        super.viewDidLoad()

        let headerView = HeaderView()
        view.addSubview(headerView)
        self.headerView = headerView

        let logOutButton = UIButton()
        view.addSubview(logOutButton)
        self.logOutButton = logOutButton
        
        // More view configuration
        ...
    }
}

以上是一个非常常见的模式,但它确实存在一个相当实质性的问题,因为总是必须不断将可选的子视图解包------即使它们是视图控制器逻辑的必要部分。例如,在这里,必须解包headerView属性,以便能够为其属性分配各种模型值:

swift 复制代码
extension ProfileViewController {
    func userDidUpdate(_ user: User) {
        guard let headerView = headerView else {
            // This should never happen, but we still have
            // to maintain this code path.
            return
        }

        headerView.imageView.image = user.image
        headerView.label.text = user.name
    }
}

上面代码本质上处理的是非可选的可选值 ------这些值在技术上可能是可选的,但当涉及到如何实现代码逻辑时,这些值实际上不是可选的。

虽然删除非可选的可选项有时相当困难,但在上述情况下,有一个非常直接的方法。使用Swift的lazy关键字,可以推迟视图控制器子视图的初始化,直到首次访问这些属性------ 与以前行为完全相同,但没有任何选项------导致更简单的代码:

swift 复制代码
class ProfileViewController: UIViewController {
    private lazy var headerView = HeaderView()
    private lazy var logOutButton = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(headerView)
        view.addSubview(logOutButton)
        
        // More view configuration
        ...
    }

    func userDidUpdate(_ user: User) {
        headerView.imageView.image = user.image
        headerView.label.text = user.name
    }
}

有关在Swift中使用lazy属性的更多信息,请查看本文

处理非可选可选项的另一种方法是使用强制拆包(使用),将可选值转换为具体值,无需任何检查。然而,虽然偶尔可能需要强制拆包,但它总是有导致崩溃的风险,如果其值最终丢失的话------因此,如果能找到其他解决方案,这通常是更可取的。

接下来,让我们快速看看可选项幕后是如何实际实现的。Swift版本的可选项的很酷之处在于,它们实际上是使用标准枚举建模的,它看起来像这样:

swift 复制代码
enum Optional<Wrapped>: ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
}

以上只是Swift如何使用自己的类型系统实现其许多核心语言功能的众多示例之一------这不仅是一个非常有趣的设计,而且在这种情况下,还使我们能够像对待任何其他枚举一样对待可选值。例如,可以使用switch语句:

swift 复制代码
func icon(forRelationship relationship: Relationship?) -> Icon? {
    // Here we switch on the optional itself, rather than on
    // its underlying Relationship value:
    switch relationship {
    case .some(let relationship):
        // Then, we switch on the wrapped value itself:
        switch relationship {
        case .friend:
            return .user
        case .family:
            return .familyMember
        case .coworker:
            return .work
        }
    case .none:
        return nil
    }
}

就Swift的各种功能如何协同工作而言,上述内容可能很有趣------但由于许多嵌套语句,生成的代码有点难以阅读。谢天谢地,Swift还能够直接打开任何可选项------就像打开其包装值一样。所要做的就是包含一个nil 枚举值来处理缺乏值的问题:

swift 复制代码
func icon(forRelationship relationship: Relationship?) -> Icon? {
    switch relationship {
    case .friend:
        return .user
    case .family:
        return .familyMember
    case .coworker:
        return .work
    case nil:
        return nil
    }
}

上述语法在Swift 5.1及更高版本中有效。使用早期版本时,必须为每个非空案例附加一个问号------像这样:case .friend?:.

可选项是使用Swift自己的独立类型实现的,这一事实也意味着它们可以拥有自己的方法和属性。例如,在这里,我们将表示URLString转换为URLRequest实例------这要求我们首先选择性地将该字符串转换为URL值,然后将其传递到新的URLRequest中:

swift 复制代码
func makeRequest(forURLString string: String) -> URLRequest? {
    guard let url = URL(string: string) else {
        return nil
    }

    return URLRequest(url: url)
}

上述代码有效,但可以使其更加紧凑------直接在可选的URL上调用map。与使用map变换集合类似,在可选项的上调用map可以使用闭包来转换其包装的任何值------像这样:

swift 复制代码
func makeRequest(forURLString string: String) -> URLRequest? {
    URL(string: string).map { URLRequest(url: $0) }
}

真正酷的是,我们不仅可以使用内置在可选类型中的方法和属性,还可以定义自己的方法和属性。例如,以下定义两个属性,能够检查任何集合是nil还是空:

swift 复制代码
extension Optional where Wrapped: Collection {
    var isNilOrEmpty: Bool {
        // If the left-hand expression is nil, the right one
        // will be used, meaning that 'true' is our default:
        self?.isEmpty ?? true
    }

    var nonEmpty: Wrapped? {
        // Either return this collection, or nil if it's empty:
        isNilOrEmpty ? nil : self
    }
}

有了上述内容,现在可以轻松处理任何缺失的值和空值------所有这些都使用单个guard语句:

swift 复制代码
extension EditorViewController: UITextFieldDelegate {
    func textFieldDidEndEditing(_ textField: UITextField) {
        guard let text = textField.text.nonEmpty else {
            return
        }

        // Handle non-empty text
        ...
    }
}

要了解有关上述技术的更多信息,请查看"在Swift中扩展可选项" . 。

如何处理可选的、可能缺失的值可以说与如何处理具体值一样重要------通过充分利用Swift的Optional类型及其各种功能,最终的代码不仅有更高的正确率,而且也非常简洁和优雅。

感谢您的阅读!🚀

相关推荐
大熊猫侯佩20 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(五)
swiftui·swift·apple watch
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(三)
数据库·swiftui·swift
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(二)
数据库·swiftui·swift
大熊猫侯佩2 天前
用异步序列优雅的监听 SwiftData 2.0 中历史追踪记录(History Trace)的变化
数据库·swiftui·swift
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(一)
数据库·swiftui·swift
season_zhu3 天前
iOS开发:关于日志框架
ios·架构·swift
大熊猫侯佩3 天前
SwiftUI 中如何花样玩转 SF Symbols 符号动画和过渡特效
swiftui·swift·apple
大熊猫侯佩3 天前
SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的原因和解决
swiftui·swift·apple
大熊猫侯佩3 天前
使用令牌(Token)进一步优化 SwiftData 2.0 中历史记录追踪(History Trace)的使用
数据库·swift·apple
大熊猫侯佩3 天前
SwiftUI 在 iOS 18 中的 ForEach 点击手势逻辑发生改变的解决
swiftui·swift·apple