可选功能可以说是Swift最重要的功能之一,也是它区别于Objective-C等语言的关键。通过被迫处理可能为空的情况,我们倾向于编写更可预测、更不容易出错的代码。

然而,有时可选变量可能会让您陷入困境,作为程序员,您知道(或者至少是假定)某个变量在使用时总是非nil,即使它是可选类型。就像在视图控制器中处理视图一样:

class TableViewController: UIViewController {
    var tableView: UITableView?

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView = UITableView(frame: view.bounds)
        view.addSubview(tableView!)
    }

    func viewModelDidUpdate(_ viewModel: ViewModel) {
        tableView?.reloadData()
    }
}

这就是Swift程序员几乎不同意制表符和空格的地方。有人说:

因为它是可选的,你应该总是正确地打开它,使用if let或guard let。

而另一些人则会朝着完全不同的方向说:

"因为你知道变量不会是nil,强制打开它(使用!)。崩溃总比以一种未知的状态结束要好。”

基本上,我们在这里讨论的是,是否要进行防御性编程。我们是试图从一个未定义的状态中恢复过来,还是简单地放弃并崩溃?

如果我必须给这个问题一个二选一的答案,我肯定会选择后者。未定义的状态会导致很难追踪bug,可能会导致不必要的代码执行,而使用防御性编程只会导致难以推理的代码。

但是,我宁愿不给出一个二元的答案,而是研究一些我们可以用更微妙的方式来解决这个问题的技术。就让我们一探究竟吧!

Is it really optional?

变量和属性是可选的,但实际上是程序逻辑所需要的,它们实际上是架构缺陷的症状。如果某些东西是需要的,那么如果没有它,你就会处于一个未定义的状态——它不应该是可选的。

虽然在某些情况下(如与某些系统api交互时),可选选项是很难避免的——但我们可以使用一些技术在许多情况下消除可选选项。

Being lazy is better than being non-optionally optional

避免那些值需要在父对象创建后创建的可选属性(例如视图控制器中的视图-应该在loadView()或viewDidLoad()中创建)的一种方法是通过使用惰性属性。lazy属性可以是非可选的,但仍然不需要在其父的初始化式中创建。它将在第一次访问时被创建。

让我们更新之前的TableViewController,为它的tableView使用一个惰性属性:

class TableViewController: UIViewController {
    lazy var tableView = UITableView()
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.frame = view.bounds
        view.addSubview(tableView)
    }
    func viewModelDidUpdate(_ viewModel: ViewModel) {
        tableView.reloadData()
    }
}

没有可选选项,没有未定义的状态!🎉

Proper dependency management is better than non-optional optionals

可选项的另一个常见用途是打破循环依赖关系。有时你会遇到这样的情况:A依赖于B,但B也依赖于A。

class UserManager {
    private weak var commentManager: CommentManager?

    func userDidPostComment(_ comment: Comment) {
        user.totalNumberOfComments += 1
    }

    func logOutCurrentUser() {
        user.logOut()
        commentManager?.clearCache()
    }
}

class CommentManager {
    private weak var userManager: UserManager?

    func composer(_ composer: CommentComposer
                  didPostComment comment: Comment) {
        userManager?.userDidPostComment(comment)
        handle(comment)
    }

    func clearCache() {
        cache.clear()
    }
}

正如我们在上面看到的,我们在UserManager和CommentManager之间有一个循环依赖关系,其中它们都不假定对对方拥有所有权,但它们仍然在其逻辑的一部分上依赖对方。 那只是等待发生的bug !😅

为了解决上述问题,我们将让CommentComposer扮演一个中间人的角色,并承担通知UserManager和CommentManager评论已经被做出的责任:

class CommentComposer {
    private let commentManager: CommentManager
    private let userManager: UserManager
    private lazy var textView = UITextView()

    init(commentManager: CommentManager,
         userManager: UserManager) {
        self.commentManager = commentManager
        self.userManager = userManager
    }

    func postComment() {
        let comment = Comment(text: textView.text)
        commentManager.handle(comment)
        userManager.userDidPostComment(comment)
    }
}

这样,UserManager就可以拥有一个对CommentManager的强引用,而不需要任何保留(或依赖)周期:

class UserManager {
    private let commentManager: CommentManager

    init(commentManager: CommentManager) {
        self.commentManager = commentManager
    }

    func userDidPostComment(_ comment: Comment) {
        user.totalNumberOfComments += 1
    }
}

我们再次删除了所有可选的代码,并拥有了可预测的代码!🎉

Crashing gracefully

上面我们看到了几个例子,在这些例子中,我们可以调整代码,通过删除可选选项来消除不确定性。然而,有时这是不可能的。 假设你正在加载一个包含应用配置的本地JSON文件。
这本质上是一个可能失败的操作,因此我们需要添加一些错误处理。

如果配置加载失败,继续执行程序将使应用程序处于未定义状态,因此在这种情况下,最好崩溃。这样我们就会得到一个崩溃报告,希望我们的测试和QA过程能够在它到达我们的用户之前发现这个问题。

那么,我们怎么崩溃呢?最简单的解决方案就是使用!操作符,强制展开可选对象,如果它包含nil会导致崩溃:

let configuration = loadConfiguration()!

虽然这种方法很简单,但它有一个很大的缺点。如果这段代码开始崩溃,我们得到的错误信息是:

fatal error: unexpectedly found nil while unwrapping an Optional value

错误消息没有告诉我们错误发生的原因和位置,也没有告诉我们如何修复它。 相反,让我们使用guard语句,并结合preconditionFailure()函数,以自定义消息退出。

guard let configuration = loadConfiguration() else {
    preconditionFailure("Configuration couldn't be loaded. " +
                        "Verify that Config.JSON is valid.")
}

使用上面的崩溃时,我们会得到一个更有用的错误信息:

fatal error: Configuration couldn’t be loaded. Verify that Config.JSON is valid.: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17

我们现在有了一个明确的行动,我们可以采取解决问题,我们确切地知道它发生在我们的代码库的什么地方!🚀

Introducing Require

执行上面的guard-let-preconditionFailure可能有点乏味,而且它确实使代码更难遵循。我们真的不想在代码中给这样的特殊情况留出太多空间——我们想专注于我们的逻辑。

My solution to that is Require.它在Optional上添加了一个简单的require()方法,该方法执行上述操作,但使调用站点更加整洁。当使用Require时,上面的配置加载代码是这样的

let configuration = loadConfiguration().require(hint: "Verify that Config.JSON is valid")

如果失败,它将给我们以下错误信息:

fatal error: Required value was nil. Debugging hint: Verify that Config.JSON is valid: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17

Require的另一个优点是它还会抛出一个NSException以及调用preconditionFailure,这将使崩溃报告工具(如Crashlytics)能够获取崩溃的所有元数据。

Summary

总结一下,以下是我在Swift中处理非可选选项的建议:

Being lazy is better than being non-optionally optional
Proper dependency management is better than non-optional optionals
Crash gracefully when you need to use non-optional optionals

原文链接