Swift 中的可选项是一种可以保存值或不保存值的类型。可选项是通过将? 附加到任何类型来编写的:
var name: String? = "Bertie"
可选(连同泛型)是最难理解的 Swift 概念之一。由于它们的编写和使用方式,很容易对它们是什么产生错误的认识。将上面的可选内容与创建普通字符串进行比较:
var name: String = "Bertie" // No "?" after String
从语法上看,可选字符串与普通字符串非常相似。它不是。可选字符串不是打开了某些“可选”设置的字符串。它不是一种特殊的字符串。字符串和可选字符串是完全不同的类型。
这里最重要的是要知道:可选项是一种容器。可选字符串是可能包含字符串的容器。可选的 Int 是可能包含 Int 的容器。将可选项视为一种包裹。在打开它之前(或用可选语言“解包”),您不会知道它是否包含某些内容。
您可以在 Swift 标准库中看到 how optionals are implemented,方法是在任何 Swift 文件中键入“可选”并 ⌘-单击它。这是定义的重要部分:
enum Optional<Wrapped> {
case none
case some(Wrapped)
}
Optional 只是 enum,它可以是以下两种情况之一:.none 或 .some。如果是.some,则有一个关联值,在上面的示例中,它是String“Hello”。可选项使用泛型为关联值提供类型。可选字符串的类型不是String,而是Optional,或者更准确地说是Optional<String>。
Swift 对可选项所做的一切都是让阅读和编写代码更加流畅的魔法。不幸的是,这掩盖了它实际工作的方式。稍后我会介绍一些技巧。
注意:我会经常谈论可选变量,但也可以创建可选常量。我用它们的类型标记所有变量,以便更容易理解正在创建的类型类型,但您不必在自己的代码中。
如何创建选项
要创建一个可选项,请在要包装的类型后附加?。任何类型都可以是可选的,甚至是您自己的自定义类型。类型和? 之间不能有空格。
var name: String? = "Bob" // Create an optional String that contains "Bob"
var peter: Person? = Person() // An optional "Person" (custom type)
// A class with a String and an optional String property
class Car {
var modelName: String // must exist
var internalName: String? // may or may not exist
}
使用选项
您可以将一个可选项与nil 进行比较,看看它是否有值:
var name: String? = "Bob"
name = nil // Set name to nil, the absence of a value
if name != nil {
print("There is a name")
}
if name == nil { // Could also use an "else"
print("Name has no value")
}
这有点令人困惑。这意味着可选是一件事或另一件事。它要么是 nil,要么是“Bob”。这不是真的,可选项不会转换成别的东西。将其与 nil 进行比较是一种使代码更易于阅读的技巧。如果一个可选项等于 nil,这仅表示枚举当前设置为 .none。
只有可选项可以为 nil
如果您尝试将非可选变量设置为 nil,则会收到错误消息。
var red: String = "Red"
red = nil // error: nil cannot be assigned to type 'String'
另一种看待可选项的方式是作为普通 Swift 变量的补充。它们是保证有值的变量的对应物。 Swift 是一种谨慎的语言,它讨厌歧义。大多数变量都被定义为非可选变量,但有时这是不可能的。例如,想象一个视图控制器从缓存或网络加载图像。在创建视图控制器时,它可能有也可能没有该图像。无法保证 image 变量的值。在这种情况下,您必须将其设为可选。它以nil 开头,当检索到图像时,可选值会得到一个值。
使用可选项揭示了程序员的意图。与任何对象都可以为 nil 的 Objective-C 相比,Swift 需要您清楚何时可以缺少值以及何时保证存在。
要使用可选的,你“解包”它
不能使用可选的String 代替实际的String。要在可选项中使用包装的值,您必须将其解包。解开可选项的最简单方法是在可选项名称后添加!。这称为“强制展开”。它返回可选项内的值(作为原始类型),但如果可选项是nil,则会导致运行时崩溃。在打开包装之前,您应该确定有一个值。
var name: String? = "Bob"
let unwrappedName: String = name!
print("Unwrapped name: \(unwrappedName)")
name = nil
let nilName: String = name! // Runtime crash. Unexpected nil.
检查和使用可选的
因为在展开和使用可选项之前,您应该始终检查 nil,这是一种常见的模式:
var mealPreference: String? = "Vegetarian"
if mealPreference != nil {
let unwrappedMealPreference: String = mealPreference!
print("Meal: \(unwrappedMealPreference)") // or do something useful
}
在此模式中,您检查一个值是否存在,然后当您确定它存在时,您强制将其解包为一个临时常量以供使用。因为这是很常见的事情,Swift 提供了一个使用“if let”的快捷方式。这称为“可选绑定”。
var mealPreference: String? = "Vegetarian"
if let unwrappedMealPreference: String = mealPreference {
print("Meal: \(unwrappedMealPreference)")
}
这将创建一个临时常量(或变量,如果您将let 替换为var),其范围仅在if 的大括号内。因为必须使用像“unwrappedMealPreference”或“realMealPreference”这样的名称是一种负担,Swift 允许您重用原始变量名称,在括号范围内创建一个临时名称
var mealPreference: String? = "Vegetarian"
if let mealPreference: String = mealPreference {
print("Meal: \(mealPreference)") // separate from the other mealPreference
}
这里有一些代码来演示使用了不同的变量:
var mealPreference: String? = "Vegetarian"
if var mealPreference: String = mealPreference {
print("Meal: \(mealPreference)") // mealPreference is a String, not a String?
mealPreference = "Beef" // No effect on original
}
// This is the original mealPreference
print("Meal: \(mealPreference)") // Prints "Meal: Optional("Vegetarian")"
可选绑定通过检查可选是否等于 nil 来工作。如果没有,它会将可选项解包到提供的常量中并执行块。在 Xcode 8.3 及更高版本 (Swift 3.1) 中,尝试打印像这样的可选项将导致无用的警告。使用可选的debugDescription 使其静音:
print("\(mealPreference.debugDescription)")
什么是可选的?
选件有两个用例:
- 可能失败的事情(我期待着什么,但我什么也没得到)
- 现在什么都不是但以后可能会出现的事情(反之亦然)
一些具体的例子:
- 可以存在或不存在的属性,例如
Person 类中的middleName 或spouse
- 一种可以返回值或不返回值的方法,例如在数组中搜索匹配项
- 一种可以返回结果或得到错误且不返回任何内容的方法,例如尝试读取文件的内容(通常返回文件的数据)但文件不存在
- 委托属性,不一定要设置,一般在初始化后设置
- 用于类中的
weak 属性。他们指向的东西可以随时设置为nil
- 可能必须释放大量资源以回收内存
- 当您需要一种方法来知道何时设置了一个值(数据尚未加载 > 数据)而不是使用单独的 dataLoaded
Boolean
Objective-C 中不存在可选项,但有一个等效的概念,返回 nil。可以返回对象的方法可以返回 nil。这被认为意味着“没有有效的对象”,并且通常用来表示出现了问题。它仅适用于 Objective-C 对象,不适用于原语或基本 C 类型(枚举、结构)。 Objective-C 通常有专门的类型来表示这些值的缺失(NSNotFound 实际上是NSIntegerMax,kCLLocationCoordinate2DInvalid 表示无效坐标,-1 或一些负值也被使用)。编码人员必须了解这些特殊值,因此必须针对每种情况记录和学习它们。如果方法不能将nil 作为参数,则必须记录在案。在 Objective-C 中,nil 是一个指针,就像所有对象都被定义为指针一样,但 nil 指向一个特定的(零)地址。在 Swift 中,nil 是一个字面量,表示某种类型的缺失。
对比nil
你曾经可以使用任何可选的Boolean:
let leatherTrim: CarExtras? = nil
if leatherTrim {
price = price + 1000
}
在更新的 Swift 版本中,您必须使用 leatherTrim != nil。为什么是这样?问题是 Boolean 可以包装在一个可选项中。如果你有这样的Boolean:
var ambiguous: Boolean? = false
它有两种“假”,一种没有值,一种有值但值为false。 Swift 讨厌模棱两可,所以现在你必须始终检查 nil 的可选项。
您可能想知道可选的Boolean 有什么意义?与其他选项一样,.none 状态可能表明该值是未知的。网络调用的另一端可能有一些东西需要一些时间来轮询。可选布尔值也称为“Three-Value Booleans”
Swift 技巧
Swift 使用了一些技巧来允许可选项工作。考虑这三行普通的可选代码;
var religiousAffiliation: String? = "Rastafarian"
religiousAffiliation = nil
if religiousAffiliation != nil { ... }
这些行都不应该编译。
- 第一行使用字符串文字设置一个可选字符串,两种不同的类型。即使这是
String 类型也不同
- 第二行将一个可选字符串设置为 nil,两种不同的类型
- 第三行将一个可选字符串与 nil 进行比较,两种不同的类型
我将介绍允许这些行工作的选项的一些实现细节。
创建一个可选的
使用? 创建一个可选的语法糖,由 Swift 编译器启用。如果你想长期这样做,你可以创建一个这样的可选:
var name: Optional<String> = Optional("Bob")
这会调用Optional 的第一个初始化程序public init(_ some: Wrapped),它从括号中使用的类型推断出可选的关联类型。
创建和设置可选的更长的方法:
var serialNumber:String? = Optional.none
serialNumber = Optional.some("1234")
print("\(serialNumber.debugDescription)")
将可选设置为nil
你可以创建一个没有初始值的可选,或者创建一个初始值为nil的可选(两者的结果相同)。
var name: String?
var name: String? = nil
允许选项等于nil 由协议ExpressibleByNilLiteral(以前称为NilLiteralConvertible)启用。可选项是使用Optional 的第二个初始化程序public init(nilLiteral: ()) 创建的。文档说您不应该将 ExpressibleByNilLiteral 用于除可选项之外的任何内容,因为这会改变代码中 nil 的含义,但可以这样做:
class Clint: ExpressibleByNilLiteral {
var name: String?
required init(nilLiteral: ()) {
name = "The Man with No Name"
}
}
let clint: Clint = nil // Would normally give an error
print("\(clint.name)")
相同的协议允许您将已创建的可选项设置为nil。虽然不推荐,但可以直接使用 nil 字面量初始化器:
var name: Optional<String> = Optional(nilLiteral: ())
将可选项与nil 进行比较
选项定义了两个特殊的“==”和“!=”运算符,您可以在Optional 定义中看到它们。第一个== 允许您检查是否有任何可选项等于nil。如果关联的类型相同,则设置为 .none 的两个不同的选项将始终相等。当您与 nil 进行比较时,Swift 在幕后创建一个具有相同关联类型的可选项,设置为 .none 然后使用它进行比较。
// How Swift actually compares to nil
var tuxedoRequired: String? = nil
let temp: Optional<String> = Optional.none
if tuxedoRequired == temp { // equivalent to if tuxedoRequired == nil
print("tuxedoRequired is nil")
}
第二个== 运算符允许您比较两个选项。两者必须是相同的类型,并且该类型需要符合Equatable(允许将事物与常规“==”运算符进行比较的协议)。 Swift(大概)解开这两个值并直接比较它们。它还处理其中一个或两个选项为.none 的情况。请注意与 nil 文字比较之间的区别。
此外,它允许您将任何Equatable 类型与该类型的可选包装进行比较:
let numberToFind: Int = 23
let numberFromString: Int? = Int("23") // Optional(23)
if numberToFind == numberFromString {
print("It's a match!") // Prints "It's a match!"
}
在幕后,Swift 在比较之前将非可选项包装为可选项。它也适用于文字 (if 23 == numberFromString {)
我说有两个 == 运算符,但实际上还有第三个可以让您将 nil 放在比较的左侧
if nil == name { ... }
命名选项
对于可选类型与非可选类型的命名不同,没有 Swift 约定。人们避免在名称中添加一些内容以表明它是可选的(如“optionalMiddleName”或“possibleNumberAsString”),并让声明表明它是可选类型。当你想命名一些东西来保存可选的值时,这会变得很困难。 “middleName”这个名字意味着它是一个字符串类型,所以当你从中提取字符串值时,你通常会得到像“actualMiddleName”或“unwrappedMiddleName”或“realMiddleName”这样的名称。使用可选绑定并重用变量名来解决这个问题。
官方定义
来自"The Basics" in the Swift Programming Language:
Swift 还引入了可选类型,用于处理值的缺失。可选项要么说“有一个值,它等于 x”,要么说“根本没有值”。可选项类似于在 Objective-C 中使用带指针的 nil,但它们适用于任何类型,而不仅仅是类。 Optional 比 Objective-C 中的 nil 指针更安全、更有表现力,并且是 Swift 许多最强大功能的核心。
Optional 是 Swift 是一种类型安全语言的一个例子。 Swift 帮助你清楚你的代码可以使用的值的类型。如果您的部分代码需要一个字符串,类型安全会阻止您错误地传递一个 Int。这使您能够在开发过程中尽早发现并修复错误。
最后,这是一首 1899 年关于可选的诗:
昨天在楼梯上
我遇到了一个不在那里的人
他今天又不在了
我希望,我希望他离开
Antigonish
更多资源: