跳到主要内容

Swift 中的可选类型详解:你需要知道的 5 件事

· 阅读需 11 分钟
GoSwiftUI
goswiftui.com

Swift 中的可选类型详解:你需要知道的 5 件事

可选类型是 Swift 的核心,自 Swift 的第一个版本以来就已存在。可选值允许我们编写简洁的代码,同时处理可能的 nil 值。

如果你刚接触 Swift,你应该习惯在属性中添加问号的语法。一旦你习惯了它们,你就可以开始从它们中受益,例如使用扩展。

什么是 Swift 中的可选值?

在我们深入了解你应该知道的知识点列表之前,首先了解基础知识很重要。

属性、方法和下标可以返回一个可选类型,这基本上意味着它要么返回一个值(如果存在),要么返回 nil。可以将多个查询链接在一起,这称为“可选链式”。这是“强制解包”的替代方法,稍后将对此进行更详细的解释。

以下代码示例定义了一个可选的 String,并使用链式打印字符数。

let name: String? = "Antoine van der Lee"
print(name?.count ?? 0)

注意:稍后将解释 ?? 运算符(空合并运算符)。

1. 强制解包 Swift 中的可选类型

强制解包要么返回存在的值,要么在值是 nil 时触发运行时错误。

但在我们深入了解强制解包之前,让我们先了解一下不使用强制解包的解包可能性。

如何解包一个可选类型?

有许多方法可以在 Swift 中解包一个值。你可以使用 guard 语句:

let name: String? = "Antoine van der Lee"
guard let unwrappedName = name else {
return
}
print(unwrappedName.count)

或者你可以使用 if let 语句:

let name: String? = "Antoine van der Lee"
if let unwrappedName = name {
print(unwrappedName.count)
}

SE-0345 以来,我们还可以使用简写语法来解包同名属性:

let name: String? = "Antoine van der Lee"
if let name {
print(name.count)
}

或者你可以使用双问号运算符,也称为空合并运算符。这将返回可选值(如果存在)或默认值,在本例中定义为零:

    let name: String? = "Antoine van der Lee"
print(name?.count ?? 0)

使用感叹号 (!) 强制解包

可以使用值后直接的感叹号 (!) 强制解包一个可选类型:

var name: String? = "Antoine van der Lee"
print(name!.count)

如果上述示例中的 name 变量被设置为 nil,它将导致以下致命运行时错误:

致命错误:意外发现 nil,同时解包一个可选值

因此,意识到你处于控制之中并且通过使用强制解包冒着崩溃的风险非常重要。根据我的经验,如果可能的话,最好不要使用强制解包。

解包可以链接

可选链式可以这样完成:

struct BlogPost {
let title: String?
}

let post: BlogPost? = BlogPost(title: "Learning everything about optionals")
print(post?.title?.count ?? 0)

强制解包也是如此:

let post: BlogPost? = BlogPost(title: "Learning everything about optionals")
print(post!.title!.count)

但请注意,如果你只解包最后一个可选类型,你仍然会得到一个可选类型。在以下示例中,我们只解包 title,而不解包 post。这意味着如果 post 是 nil,我们仍然无法获得 title:

let post: BlogPost? = BlogPost(title: "Learning everything about optionals")
print(post?.title!.count) // 打印:Optional(35)

可选类型作为最佳实践,强制解包以捕获编程错误

最好不要在不需要时使用感叹号。有些人甚至建议启用 强制解包 SwiftLint 规则。这将防止你引入许多意外崩溃。

但是,一些开发人员更喜欢在值是 nil 时使用强制解包,因为这是一个编程错误。因此,你可以通过强制解包和在早期阶段捕获错误来帮助自己进行调试。我的偏好是尽量不使用强制解包。

2. 可选类型是一个包含两个案例的枚举

很高兴知道可选类型基本上是一个包含两个案例的枚举:

enum Optional<Wrapped> {
/// 值的缺失。
case none

/// 值的存在,存储为 `Wrapped`。
case some(Wrapped)
}

但是,你将使用 nil 来表示值的缺失,而不是使用 .none 案例。

考虑到这一点,你可以使用枚举来定义上述 name 变量:

let name = Optional.some("Antoine van der Lee")
print(name!.count)

或者你可以像使用普通枚举一样使用 switch-case:

func printName(_ name: String?) {
switch name {
case .some(let unwrappedValue):
print("Name is \(unwrappedValue)")
case .none:
print("Name is nil")
}
}

printName(nil) // 打印:"Name is nil"
printName("Antoine van der Lee") // 打印:"Name is Antoine van der Lee"

并且查看其 文档,你可以看到可选类型附带了一些非常方便的方法。一个很好的例子是 map 方法:

let sideLength: Int? = Int("20")
let possibleSquare = sideLength.map { $0 * $0 }
print(possibleSquare) // 打印:"Optional(400)"

或者 flatMap 方法,在本例中,它仅在名称通过至少包含 5 个字符的验证后才返回名称:

var name: String? = "Antoine van der Lee"
let validName = name.flatMap { name -> String? in
guard name.count > 5 else { return nil }
return name
}
print(validName) // 打印:"Optional("Antoine van der Lee")"

如果你想了解更多有关 map、flatMap 和 compactMap 之间差异的信息,请查看我的博客文章: CompactMap 与 flatMap:差异详解

扩展可选类型

既然你知道可选类型被定义为一个枚举;你还可以为它编写扩展!

最常见的示例是扩展一个可选的 String 并处理一个空值:

extension Optional where Wrapped == String {
var orEmpty: String {
return self ?? ""
}
}

var name: String? = "Antoine van der Lee"
print(name.orEmpty) // 打印:"Antoine van der Lee"
name = nil
print(name.orEmpty) // 打印:""

虽然我们使用枚举来定义扩展,但我们也可以使用以下语法,使用问号:

extension String? {
var orEmpty: String {
return self ?? ""
}
}

3. 为可选类型编写单元测试

当你编写测试时,有一种很好的方法可以在不强制解包的情况下使用可选类型。如果你使用强制解包,你冒着导致所有测试无法成功的致命错误的风险。

你可以使用 XCTUnwrap,如果值为 nil,它将抛出一个错误:

func testBlogPostTitle() throws {
let blogPost: BlogPost? = fetchSampleBlogPost()
let unwrappedTitle = try XCTUnwrap(blogPost?.title, "Title should be set")
XCTAssertEqual(unwrappedTitle, "Learning everything about optionals")
}

4. 可选协议方法

如果你有 Objective-C 的经验,你可能会错过可选协议方法。尽管在 Swift 中有一种更好的方法来模仿这种行为,使用默认协议实现,但标准库中的常见方式如下所示:

@objc protocol UITableViewDataSource : NSObjectProtocol {

@objc optional func numberOfSections(in tableView: UITableView) -> Int

// ...
}

这允许你使用问号调用该方法:

let tableView = UITableView()
let numberOfSections = tableView.dataSource?.numberOfSections?(in: tableView)

你可以在此处阅读有关协议方法的更多信息:Swift 中的可选协议方法

5. 嵌套可选类型是一件小事

尽管 SE-0230 – 扁平化由“try?”产生的嵌套可选类型 删除了嵌套可选类型最常见的原因之一,但它仍然是一件小事!

var name: String?? = "Antoine van der Lee"
print(name!!.count)

你已经解包了一个仍然返回一个可选类型的可选类型。这曾经是早期 Swift 版本中使用 try? 运算符时的情况。

一个常见的示例是当你使用包含可选值的字典时:

let nameAndAges: [String:Int?] = ["Antoine van der Lee": 28]
let antoinesAge = nameAndAges["Antoine van der Lee"]
print(antoinesAge) // 打印:"Optional(Optional(28))"
print(antoinesAge!) // 打印:"Optional(28)"
print(antoinesAge!!) // 打印:"28"

你可以看到,它基本上只需要使用额外的感叹号或问号。

结论

就是这样!我们介绍了在 Swift 中使用可选类型时需要了解的许多内容。从使用感叹号 (!!) 的基本解包到扩展 Optional 枚举的更高级实现。

如果你想进一步提高你的 Swift 知识,请查看 Swift 类别页面。如果你有任何其他提示或反馈,请随时 联系我 或在 Twitter 上给我发推文。

谢谢!