相关资料:
optional
允许我们表示某些数据的缺失,不是指空的字符串那么简单,而且实际上就不存在这个值。
Bool
、Array
和 Struct
等…// 常见定义方式,在类型后加问号
var icon: String?
// 加了问号,说明是Int optional,那它未必有值,可能是nil,因此不能直接运算
var luckyNumber:Int? = nil
luckyNumber + 7 //程序报错
var icon: String? = getIcon()
// 返回可能有值,也可能没值
print(icon)
// 打印出 Optional("heart")
当把参数定义成可选类型 optional
时,即使它有值,但是打印出来的时候是 Optional("heart")
,而不是 "heart"
。
因为当把类型定义为可选类型时,相当于给 ”值“ 包了一个盒子,打印的时候是把 ”盒子 + 值“ 一起打印出来了。
那如何将盒子里的值单独拿出来呢?
就是在调用值时在后面加感叹号 (!) 。使用感叹号 (!) 来强制解包一个 Optional。这表示你确定该 Optional 不为 nil
,如果实际为 nil
则会引发运行时错误。使用强制解包时要小心,确保有充分的理由相信 Optional 不为 nil
,否则会引发崩溃。
let optionalValue: Int? = 42
let unwrappedValue = optionalValue!
// 例子1
if icon != nil{
print(icon!)
// 因为icon是可选类型,所以它返回的时候是 “盒子 + 值”,为了去掉盒子,下面就用了!强制拆包
// 但这里的问题是,一旦实际上盒子里没有值,还强制拿出来,会造成程序崩溃。
}else{
print("...")
}
// 例子2:这里后面的x也要加惊叹号解开,让 x 从 Int optional 变成 Int 类型才可以相加
if x != nil{
x! + 1
}
// 例子3: 如果前面的表达式成立,就取冒号前面的值;如果不成立,就取后面的值。
topScore = loadedScore != nil ? loadedScore! : 0
什么时候应该在 Swift 中强制解包?
// 情况1:当遇到一些自己手写输入的静态数据,确保不会出错的情况,就可以强制解包
let url = URL(string: "<https://www.apple.com>")!
// 情况2:创建一个从 1 到 10 的封闭范围,使用 randomElement() 方法从该范围中选取一个随机数,这种情况也不会出错
let randomNumber = (1...10).randomElement()!
// 情况3:如果枚举符合 CaseIterable 协议,Swift 将生成一个 allCases 数组属性,其中包含按定义顺序排列的所有情况
// 因此,我们可以创建一个 Direction 枚举并选择一个随机值
// 因为是从已有的 case 中选取一个,我们知道它总是会成功,所以可以强制解包。这种情况如果不强制解包,很难用某个默认值代替!
enum Direction: CaseIterable {
case north, south, east, west
}
let randomDirection = Direction.allCases.randomElement()!
// 更好的想法是创建功能和扩展,将强制解包隔离在一个地方。这意味着强制解包可以存储在澄清其行为的位置附近,并且绝大多数代码不需要直接强制展开。
enum Direction: CaseIterable {
case north, south, east, west
static func random() -> Direction {
Direction.allCases.randomElement()!
}
}
// 有了这个,需要获得随机 Direction 的地方就不再需要强制解包了
let randomDirection = Direction.random()
由于强制解析不安全,所以一般不会用强制解析,而应该改用下面几种拆包方式:
使用 if let
语句来条件性地解包,并将解包后的值赋给一个新的变量。它的意思是如果可选类型的值不为空,就把它拿出来,存到前面的变量中。这种做法不会导致代码崩溃。使用了 if let
后,如果可选项内部有值,它将被解开——这意味着内部的值被传入到常量/变量中。
if let 常量 = Optional的值
if var 变量 = Optional的值
// 例子1: 解开常规可选值
let optionalValue: Int? = 42
if let unwrappedValue = optionalValue {
print(unwrappedValue)
// 使用unwrappedValue
} else {
print("...")
// Optional 为 nil的情况
}
// 例子2: 解开函数返回值
func getUsername() -> String? { "Taylor" }
if let username = getUsername() {
print("Username is \\(username)")
} else {
print("No username")
}
// 技巧1:由于这个常量/变量只是为了拆包使用,所以没有必要起特别的名字,一般可以用同名
if let grade = grade { print(grade) }
// 技巧2:可以一次取出多个 optional
if let firstInput = firstInput , let secondInput = secondInput{
if let firstNum = Double(firstInput), let secondNum = Double(secondInput){
print("第一个数字 \\(firstNum) 加上第二个数字:\\(secondNum) 等于 \\(firstNum + secondNum)")
}
}else{
print("遇到了EOF,end of file")
}
// 技巧3:可以简写
// 这种简写方式代表:如果参数值不是 nil,则为 true;如果参数值是 nil 则为 false
// 常规写法
if let number = number { print(square(number: number)) }
// 简写方法
if let number {
print(square(number: number))
}else{
...
}
使用 guard
语句可以先检查可选值是否有值。如果检查不通过,则执行 guard
内部的代码;如果检查通过(即可选值有值),才执行 guard
代码块后面的代码。 else
的作用是: “检查是否可以解开可选内容,但如果不能,那么就……”。
所以 guard let
有时称为早期返回,在函数启动时检查函数的所有输入是否有效,如果有无效,就运行一些代码,然后退出。
// 利用 Guard 来解包
func printSquare(of number: Int?) {
guard let number = number else {
print("Missing input")
return
}
// number 在 guard 外面依旧有效可以使用
print("\\(number) x \\(number) is \\(number * number)")
}
// 解包的缩写形式
func processData(data: Data?) {
guard let data else {
print("No data found")
return
}
// process the data
}
<aside>
💡 使用 guard
来检查函数的输入是否有效,如果检查失败,Swift 要求必须使用 return
退出当前作用域。这种情况下意味着如果函数失败,必须从该函数返回,这不是可选的,是必须做的。如果没有 return
,Swift 将不会编译我们的代码。
</aside>
// 应用:有值和没值分别返回不同的数据
func double(number: Int?) -> Int? {
guard let number = number else {
return nil
}
return number * 2
}
let input = 5
if let doubled = double(number: input) {
print("\\(input) doubled is \\(doubled).")
}
// 应用:对多个可选值进行检查(如果第一个可选值不存在,则不会执行第二个 guard 语句)
func processData(data: Data?, metadata: Metadata?) {
guard let data else {
print("No data found")
return
}
guard let metadata else {
print("No metadata found")
return
}
// process the data and metadata
}
除了 optional 解包,其他情况下也可以使用 guard
语句。参见:guard … else 语句
// 下面这段代码是错的,guard 后面必须跟 else
func verify(age: Int?) -> Bool {
guard age >= 18 {
return true
} else {
return false
}
}
if let 和 guard let … else 的差异:
if let
一样, guard let
检查可选值中是否有值,如果有,它将检索该值并将其放入选择的常量中if let
会运行大括号内的代码。而 guard let
是,如果可选值没有值,才运行大括号内的代码guard let
声明的常量,在大括号外面也可以使用;而 if let
声明的常量,只能在大括号内的作用域使用if let
,但如果您在继续之前专门检查条件是否正确,则首选 guard let
func getMeaningOfLife() -> Int? { 42 }
//用 if let 写,getMeaningOfLife() 的结果只有在返回整数而不是 nil 时才会被打印
func printMeaningOfLife() {
if let name = getMeaningOfLife() {
print(name)
}
}
//用 guard let 写,
//guard 要求在使用时退出当前它的作用域,这意味着如果函数失败,必须从该函数返回。所以必须写return,如果没写,Swift将报错
func printMeaningOfLife() {
guard let name = getMeaningOfLife() else {
return
}
print(name)
}
nil 合并运算符在您有可选值,并且想要使用其中的值或在缺少时提供默认值的任何地方都很有用。使用 ??
运算符来提供一个默认值,如果 Optional 为nil
,那就使用后面的默认值。
// 例子1:
let optionalValue: String? = "allen"
print("OK" + (optionalValue ?? "N/A"))
//optionalValue 如果有值,就取前面的值;
//optionalValue 如果为nil,就取后面的默认值“N/A”。这对于给Optional一个默认值非常有用。
//这意味着无论 Optional 包含什么值或 nil,最终打印结果都是真正的字符串,而不是可选字符串。
// 例子2: 从字符串创建整数,由于可能提供了无效的整数(例如“Hello”),所以可能会返回一个可选的 Int?
//这时可以使用 nil 合并来提供默认值:
let input = ""
let number = Int(input) ?? 0
print(number)
// 例子3: 读取字典键将始终返回一个可选值,因此您可能需要在此处使用 nil 合并来确保返回一个非可选值:
let scores = ["Picard": 800, "Data": 7000, "Troi": 900]
let crusherScore = scores["Crusher"] ?? 0
// 另外字典还提供了一种不同的方法,让我们指定未找到键时的默认值
let crusherScore = scores["Crusher", default: 0]
当有需要还可以连着使用 nil 双问号运算符:
//这代码的意思是先尝试运行 first() ,如果返回 nil 则尝试运行 second() ,如果再返回 nil ,那么它将使用默认值——空字符串
let savedData = first() ?? second() ?? ""
在这个示例中,map
方法接受一个闭包,该闭包用于对 Optional 中的值进行转换。如果 Optional 有值,闭包将被应用,然后方法将返回一个包含转换结果的新 Optional。如果 Optional 为 nil,则方法将返回 nil,而不会引发错误。
map
方法对于将Optional中的值转换为另一种类型或执行某些操作时非常有用。它通常用于函数式编程风格,可以使代码更具表现力和清晰度。请注意,map
方法返回的是一个新的Optional,原始Optional保持不变,这有助于保持不可变性。
// 例子1:使用 map 方法将 Optional 值加倍;如果 optionalValue 有值,doubledValue 将包含新的值(84),否则为nil
let optionalValue: Int? = 42
let doubledValue = optionalValue.map { $0 * 2 }
print(doubledValue)
// 例子2:使用 map 方法处理 optional 的对象
selectedRestaurant.map {
// $0为取map的第一个参数
RestaurantDetailView(restaurant: $0)
.transition(.move(edge: .bottom))
}
flatMap
函数可用于解开嵌套的可选值。以下代码演示了使用带有嵌套可选整数的 flatMap
函数的示例:
let nestedOptional: Int?? = Optional(Optional(5))
let flatMappedOptional = nestedOptional.flatMap { $0 }
print(flatMappedOptional) // Output: Optional(5)
在此示例中, flatMap
函数接受一个返回嵌套可选整数的闭包,并返回一个值为 5 的新可选整数。需要注意的是,如果外部可选值里面的值为 nil
,则 flatMap 也将返回 nil
。
使用 ?
来调用 Optional 对象的属性、方法或下标,如果 Optional 为 nil
,整个表达式会返回 nil
,而不会引发错误。
链式调用的神奇之处在于,如果 Optional 为 nil
,它会默默地不执行任何操作,它只会发回与之前相同的可选值,但仍然为 nil
。这意味着 Optional 链的返回值始终是可选的,这就是为什么最后仍然需要通过 nil 合并来提供默认值。
// 例子1:
let optionalUser: User? = getUserFromDatabase()
let username = optionalUser?.username
// 例子2: Optional 的链式调用代表“如果 Optional 内部有值,则将其解开......” 并且可以添加更多代码。
// 在下面例子中,我们说“如果设法从数组中获取一个随机元素,则将其大写。”(因为 randomElement() 返回的是 Optional,因为数组可能为空)
let names = ["Arya", "Bran", "Robb", "Sansa"]
let chosen = names.randomElement()?.uppercased() ?? "No one"
print("Next in line: \\(chosen)")
// 如果 names.randomElement() 返回的是 nil ,则 names.randomElement()?.uppercased() 返回的也是 nil
可选链可以任意长,只要任何部分发送回 nil
,该代码行的其余部分就会被忽略并发送回 nil
// 例子:我们有一个 Book 结构的可选实例 - 我们可能有一本书要排序,也可能没有。
// 1. 这本书可能有作者,也可能是匿名的。
// 2. 如果确实存在作者字符串,则它可能是空字符串或有文本,因此我们不能总是依赖第一个字母。
// 3. 如果第一个字母存在,请确保它是大写的,以便正确排序具有小写名字的作者(例如bell hooks)
struct Book {
let title: String
let author: String?
}
var book: Book? = nil
let author = book?.author?.first?.uppercased() ?? "A"
print(author)
// 最后 author 的类型是字符串,和最后的返回值保持一致
// 因此,它的内容是“如果我们有一本书,并且该书有作者,并且作者有第一个字母,则将其大写并将其发回,否则发回 A”。
<aside> 💡 optional 的链式调用与 nil 合并是非常好的伴侣,因为它允许您深入挖掘可选层,同时如果任何可选值为 nil ,还可以提供明智的回退。
</aside>