相关资料:


Optional 可选值

optional 允许我们表示某些数据的缺失,不是指空的字符串那么简单,而且实际上就不存在这个值。

// 常见定义方式,在类型后加问号
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。这表示你确定该 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 后,如果可选项内部有值,它将被解开——这意味着内部的值被传入到常量/变量中。

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 let … 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 的差异:

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 合并

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 方法解包

在这个示例中,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 函数可用于解开嵌套的可选值。以下代码演示了使用带有嵌套可选整数的 flatMap 函数的示例:

let nestedOptional: Int?? = Optional(Optional(5))
let flatMappedOptional = nestedOptional.flatMap { $0 }
print(flatMappedOptional) // Output: Optional(5)

在此示例中, flatMap 函数接受一个返回嵌套可选整数的闭包,并返回一个值为 5 的新可选整数。需要注意的是,如果外部可选值里面的值为 nil,则 flatMap 也将返回 nil


Optional chaining 链式调用

使用 ? 来调用 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>