Swift整体设计的一个关键部分是,它要求我们如何显式地处理可能丢失或可选的值。而这一要求常常迫使我们更彻底地思考如何构建对象和管理我们的状态-它还可以减少由于丢失数据而导致的未处理的运行时错误。
在Swift中,一个值通过在其类型后面添加一个问号被标记为可选的,这反过来要求我们在以任何具体方式使用该值之前打开该值。例如,这里我们使用if let语句来打开一个可选的用户值,以确定用户是否已经登录到我们的应用程序:
func setupApp(forUser user: User?) {
if let user = user {
showHomeScreen(for: user)
} else {
showLoginScreen()
}
}
虽然有多种展开和处理可选值的方法,但另一种非常常见的模式是使用guard语句提前返回,以防缺少可选值。下面是我们如何重构上述函数以使用该模式:
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)
}
本质上,可选选项为我们提供了一种表示“缺少值”的内置方法——这使得它们在需要对某种默认状态或缺失状态建模的情况下成为理想的选择。
例如,这里我们创建了一个关系枚举,它使我们能够表达应用程序的两个用户之间的关系。目前,我们在该enum中使用none来表示没有任何关系,它也作为我们的默认值:
enum Relationship {
case none
case friend
case family
case coworker
}
struct User {
var name: String
var relationship: Relationship = .none
...
}
然而,尽管上面的代码在技术上可以工作,但是可以论证的是,将relationship属性实现为可选属性会更好。这样,我们就可以使用Swift的内置方式来表示缺少任何关系,这样我们就可以使用if let和guard这样的机制,也可以通过删除none来简化枚举:
enum Relationship {
case friend
case family
case coworker
}
struct User {
var name: String
var relationship: Relationship? = nil
...
}
然而,如果我们不小心,可选选项也可能成为歧义的来源。在实现一个给定的值之前,考虑它是否确实是可选的总是很重要的——因为如果一个值是为了让我们的代码运行所必需的,我们应该能够在理想情况下保证它总是存在的。
例如,让我们来看看一个视图控制器实现,它目前使用可选项来存储它的子视图-一个headerView和一个logOutButton -为了在系统调用viewDidLoad()时惰性地创建它们 - 这是构建基于视图控制器的ui的推荐方法:
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属性,以便能够为它的属性分配各种模型值:
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关键字,我们可以延迟视图控制器子视图的初始化,直到这些属性第一次被访问-给我们完全相同的行为,正如我们之前,但没有任何可选的-导致更简单的代码:
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
}
}
处理非可选可选项的另一种方法是使用强制展开(using !)将可选值转换为具体值,而不进行任何检查。 然而,虽然强制展开可能偶尔会得到保证,但它总是伴随着导致崩溃的风险,以防它的值丢失——所以如果我们能找到另一个解决方案,那通常是更可取的。
接下来,让我们快速了解一下可选选项实际上是如何实现的。 Swift版本的可选项有一个很酷的地方,那就是它们实际上是用一个标准的enum来建模的,就像这样:
enum Optional<Wrapped>: ExpressibleByNilLiteral {
case none
case some(Wrapped)
}
上面的例子只是Swift如何使用自己的类型系统来实现其核心语言特性的众多例子之一——这不仅是一个真正有趣的设计,但在这种情况下,我们也可以像对待其他enum一样对待可选值。例如,我们可以打开它们:
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来处理没有值的情况:
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?:.
可选项是使用它们自己的独立类型来实现的,这也意味着它们可以拥有自己的方法和属性。例如,这里我们将一个表示URL的字符串转换为一个URLRequest实例——这要求我们首先将该字符串转换为URL值,然后将该值传递给新的URLRequest:
func makeRequest(forURLString string: String) -> URLRequest? {
guard let url = URL(string: string) else {
return nil
}
return URLRequest(url: url)
}
上面的代码可以工作,但是如果我们想的话,我们可以让它变得更紧凑——通过直接在我们的可选URL上调用map。类似于我们如何使用map来转换集合,在可选对象上调用map让我们可以使用闭包来转换它所包装的任何值,就像这样:
func makeRequest(forURLString string: String) -> URLRequest? {
URL(string: string).map { URLRequest(url: $0) }
}
真正酷的是,我们不仅可以使用内置在可选类型中的方法和属性,还可以定义自己的方法和属性。 例如,下面是我们如何定义两个属性,使我们能够检查任何集合是nil还是空:
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语句:
extension EditorViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
guard let text = textField.text.nonEmpty else {
return
}
// Handle non-empty text
...
}
}
如何处理可选的、可能缺失的值与如何处理具体的值一样重要-通过充分利用Swift的可选类型和它的各种特性,我们通常可以最终得到不仅有更高的概率是正确的代码,而且也非常简洁和优雅。