原文:Optionals in Swift – AndyBargh.com

Automatically update if let and guard let for Swift 5.7

Swift 编程言语带来了许多新功能,使开发运用程序比曾经更迅捷、更简略、更安全。其中一个新特性便是可选类型( Optionals)。它们在 Swift 中随处可见,但关于刚接触这门言语的人来说或许会感到困惑,而关于不彻底了解它的人来说则会感到懊丧。这篇文章的首要目的是经过深入了解 Optionals,为这个或许令人困惑的国际带来一丝光明。

可选类型处理了什么问题?

首要让咱们看一下可选类型所要处理的问题。

在代码中处理值不存在状况的问题从计算机诞生之初就存在了。在一个理想的国际里,咱们的代码总是与完好且界说良好的数据一同作业,但这并不总是如此。在许多编程言语中,程序员在这种状况下会运用特别值,他们用这些(特别)值来试图标明一个值的缺失。一个常用的比如是 nil,它常常被用来标明没有任何值。可是,这也有问题。

在大多数编程言语中,nil 只能用来标明引用类型,一般不支撑用 nil 来标明值类型。其结果是,关于值类型,开发者依然不得不发明自己的编码来标明一个变量没有值的状况。

除此之外,虽然开发者能够想出这些特别的编码,但大多数言语中都没有内置任何规范来描绘何时能够或不能够运用这些编码。一般来说,在函数或办法的声明中,没有任何东西能够强调是否接受 nil 值,同样也没有任何东西能够标明这些函数或办法是否能够回来一个 nil 值。

在许多状况下,这种类型的信息的仅有来历是文档。但许多时分,文档要么没有,要么底子不完好。其结果是,咱们常常要猜想到底怎么运用一个特定的函数或办法,这就不可避免地导致了过错和难以发现的问题。

有了 Swift,状况就不同了。Swift 试图经过在言语中直接包括特别语法来处理这些问题,以处理值不存在的状况。这便是 Optionals 呈现的原由。

什么是可选类型

在 Swift 中,可选类型用来描绘一种类型或许不存在值的状况

可选类型标明两件事的结合。它们标明假如当时类型有值,这个值将是一个指定的类型,一起也标明或许底子没有值。与其他言语类似,Swift 运用 nil 来标明底子没有值的状况。

经过将可选类型直接纳入言语,Swift 迫使咱们清楚地了解一个特定的变量、常量、参数或回来值是否能够为零,并在代码中这样做,而不是迫使咱们依托文档来取得这类信息。

假如一个参数或回来值的类型被标记为可选类型,那么一个值或许是 nil。假如不是,那么咱们能够 100% 确定回来值永远不会是 nil,并且编译器会实际履行这一实际。

这样做有许多好处。

首要,它节省了咱们阅览 Swift 代码时的大量心理担负,大大减少了咱们对文档的依赖性。

其次,经过迫使咱们在编写代码时考虑并处理潜在的 nil 值,有助于消除曾经或许在运行时才会呈现的问题。

最后,经过清晰阐明什么时分值或许是或不或许是 nil,咱们还能够让编译器替咱们履行一些查看。这最后一点是一个巨大的优势。

无论咱们喜爱与否,人类大脑对代码可履行许多途径的考虑才能是有限的。在许多状况下,要辨认一切的途径和相关的边缘状况或许是非常困难的。再加上辨认其中哪些或许导致一个值为零的任务,这简直成为不或许的事。可是,编译器在这方面比咱们强得多,它能够帮助辨认和查看这些边缘状况,寻觅咱们或许遗漏的任何 nil 值。这样做能够避免一大类运行时问题的产生。

所以,咱们现已了解了什么是可选类型,咱们也知道了在言语中参加可选类型的一些好处,但咱们实际上怎么声明它们呢?让咱们接下来看看这个问题。

声明可选类型

在 Swift 中,咱们经过在一般类型的末尾增加一个问号(?)来声明一个可选类型。咱们能够对 Swift 中的任何类型的值都这样做,无论是引用类型仍是值类型。

例如,假如我想声明一个整数类型,我一般会经过声明常量或变量为 Int 类型来标明这一点。假如我想声明这个常量或变量是可选的,我会在类型后边增加一个 ? 来声明这个常量或变量是一个可选的整数类型:

var optionalInt: Int?

上面的代码将 optionalInt 变量声明为 Int 类型,这标明它能够包括一个 Int 类型的值,但在某些状况下能够包括 nil

现在,有一件事我应该在这一点上指出来。可选类型与非可选类型不是同一类型。假如咱们测验运行下面的代码,咱们就能够看到这一点:

var a: Int = 1
var b: Int? = 2
var c = a + b // 编译器会显现以下过错
// Value of optional type 'Int?' must be unwrapped to a value of type 'Int'

从编译器的角度来看,可选类型和非可选类型是两种不同的类型,不能在同一个表达式中一同运用。假如你仔细想想,这其实是符合逻辑的。试图将一个整数值增加到或许包括也或许不包括整数值的另一个实体中,其实是没有意义的。

从本质上讲,咱们能够把可选类型看作是一个盒子,它或许包括也或许不包括咱们指定类型的值。为了运用这个盒子里的值,咱们有必要先把它翻开,查看是否真的有一个值存在。在 Swift 中,这个翻开盒子的过程被称为解包(unwrapping),咱们能够经过三种首要办法来实现它。

解包可选类型

强制解包可选类型

在 Swift 中,解包可选类型的榜首种办法叫做强制解包(force unwrapping)。强制解包运用强制解包操作符,咱们把它写成一个感叹号(!),直接写在可选常量名或变量名的后边。

感叹号告诉编译器直接解包可选类型并运用它。没有 “假如”,没有 “可是”,也没有查看。从本质上讲,它封闭了编译器为保证盒子内实际包括一个值而进行的一切正常查看,而将这一职责转移给了作为程序员的你。这种职责带来了一些相关的危险。

许多时分,咱们并不真正知道一个可选类型是否包括值,假如咱们运用强制解包操作符,结果发现可选类型不包括一个值,咱们就会无意中触发运行时反常,然后导致运用程序溃散。

假如咱们想运用这个强制解包操作符,慎重的做法是在强制解包之前,首要查看可选类型是否包括一个值。最简略的办法是运用一个 if 句子来实现:

var c: Int = 3
var d: Int? = 4
var result: Int
if (d != nil) {
    result = c + d!
}

如你所见,在 Swift 中履行这些查看是很常见的,并且到处写 if 句子并不理想,所以 Swift 为咱们供给了一种代替语法,简直不需求运用强制解包。这便是所谓的可选绑定(optional binding)

可选绑定

可选绑定是 Swift 中解包可选类型的第二种办法,并且由语法直接构建在言语中来支撑它。

可选的绑定答应咱们在一行代码中一起查看一个可选项并将其值(假如存在的话)提取到一个暂时常量或变量中。Swift 中的 ifwhile 句子都供给了这方面的支撑。让咱们看一个比如:

if let e = d {
    // 在 if 句子块中,假如可选类型 `d` 不为 nil,
    // `e` 就包括解包后的值
    result = c + e
}

正如你所看到的,可选绑定的语法是相对简略的。

咱们运用 if 句子,后边是 varlet 要害字(取决于咱们是否要声明一个暂时变量或常量),后边是赋值运算符,然后是咱们要查看的可选类型称号。

简略的说,这段代码能够读作。“假如可选类型 d 包括一个值,则将一个名为 e 的新常量设置为该可选类型中包括的值”。 假如可选类型包括一个值(咱们运用 if 句子来查看),那么暂时常量 e 就能够作为 if 句子主体中的一个局部常量来运用,这样咱们就不需求强制解包 d 中的值了。

暗影?

这看起来很古怪,可是当咱们用这种办法解包一个可选类型时,咱们也能够将可选类型解包成一个新的暂时常量,其称号与原可选类型彻底相同。

if let d = d {
    // Inside the if statement `d` contains the unwrapped value
    // from the original optional `d` if it is not nil.
    result = c + d
}

这是一个被称为 暗影(shadowing) 的比如,其效果是创立了一个新的暂时常数,叫做 d,在 if 句子块的内部可用,在外部内隐藏或暗影了变量 d

假如你查看一下值的类型,你就能够看到这一点。而内部效果域中的 dInt 类型(一个正常的值类型)。经过运用暗影,这意味着你不必运用强制解包就能够在内部效果域中运用 d 中的值。

不过这种办法有优点也有缺点。

有些人喜爱这种办法,由于这意味着他们不必为解包后的值考虑或记住一个新的变量名。

但也有人不喜爱这种办法,由于他们以为解包后的版别应该有一个与本来不同的姓名(首要是为了杰出可选类型已被解包的实际)。

归根结底,这个问题的答案没有对错之分。我更倾向于运用不同的变量或常量称号,由于我以为这能使事情更清晰,但这纯粹是一种风格上的挑选,你有必要自己挑选。

在咱们结束可选的绑定之前,我还想给你看几个其他的比如。

在一行代码中绑定多个可选项

Swift 刚推出的时分,咱们一次只能对一个选项进行绑定。这就导致了与下面类似的状况(一般被称为厄运金字塔(pyramid of doom)),跟着咱们想要绑定的选项数量的增加,咱们会得到越来越多的嵌套 if 句子。

var k : Int? = 4
var l: Int? = 8
if let m = k {
    if let n = l {
        // Do something with m and n
    } else {
        print("l contained nil")
    }
} else {
    print("k contained nil")
}

可是,Swift 1.2 增加了在一行代码中绑定多个可选类型的才能。这有助于咱们以更紧凑的形式编写代码,if 句子的榜首个分支只要在一切可选绑定履行成功后才会履行:

if let m = k, n = l {
    print(m + n)
} else {
    print("k or l contained nil")
}
// prints "12"

咱们还能够运用 where 子句将一个或多个绑定与一个可选的布尔表达式结合起来。同样,一切这些都在一行代码中。在这种状况下,只要当一切的可选表达式都包括一个值并且 where 子句判定为真时,绑定才会产生:

var o : Int? = 4
if let p = o where o > 2 {
    print(p)
}
// prints "4"

隐式解包可选类型

假如你开端长期运用可选类型,你很快就会留意到与之相关的额定的语法层,这层语法往往会使咱们的代码更加难以阅览。但在某些特别状况下,当咱们知道咱们的可选类型将包括一个非 nil 值时,Swift 答应咱们革除额定的可选语法,答应咱们像其他常量或变量相同运用选项。但要做到这一点,咱们有必要将咱们的可选类型标记为隐式解包(implicitly unwrapped),这是咱们在 Swift 中解包可选类型的第三个机制。

隐式解包可选类型是一个有点古怪的野兽。一方面,它的行为类似于可选类型(由于它们能够被设置为 nil,并且咱们能够查看它是否为 nil),但另一方面,编译器会在每次拜访它时自动解包,这使得咱们能够省去到目前为止一向运用的一切解包可选类型的语法。

为了将一个可选类型标记为隐式解包(而不是一般的可选类型),咱们在类型后边运用感叹号(!),而不是问号(?) 。你能够在下面的比如中看到这一点:

// 不运用隐式解包
let possibleInt : Int? = 4
let forcedInt: Int = possibleInt!
// 运用隐式解包
let assumedInt : Int! = 4
let implicitInt = assumedInt

正如你所看到的,经过将 assumedInt 变量标记为隐式解包,咱们不再需求运用在本例榜首部分中运用的强制解包操作。相反,咱们能够简略地拜访该变量,就像它是一个非可选类型相同。

可是有一个问题。

经过将一个可选类型标记为隐式解包,咱们向编译器承诺,当拜访该可选类型时,该可选类型将一直包括一个非零值。

与咱们之前看到的强制解包操作符类似,假如咱们破坏了这个承诺(并且隐含解包的可选类型在被拜访时不包括一个值),Swift 将触发一个运行时过错,并导致运用程序溃散。

// 声明了一个隐式解包可选类型
var greeting: String! = "hello world"
greeting.capitalizedString // Returns "Hello World"
// 某些状况下,该隐式解包可选类型为 nil 时,直接拜访就会导致运用溃散
greeting = nil // As it's an optional we can set its value to nil
greeting.capitalizedString // CRASH! - Can't send messages to nil!

依据这一点,隐式解包可选类型与强制解包的留意事项是相同的。

只要当咱们 100% 确定一个可选类型在被拜访时不会为零时,咱们才应该将其标记为隐式解包,假如有任何疑问,咱们应该运用一个非可选类型的值(假如能够的话)或许一个正常的可选类型。

一般来说,隐式解包可选类型是非常危险的野兽,有导致运行时反常的高危险,但话虽如此,在 Swift 的一些特定状况下,运用它们是必不可少的。接下来让咱们来看看这些状况。

在结构器中运用隐式解包可选类型

说到 Swift 中类的初始化,有一些恰当严格的规矩。其中一条规矩(我直接引用 Swift 编程言语攻略 中的内容)是:

Classes and structures must set all of their stored properties to an appropriate initial value by the time an instance of that class or structure is created. Stored properties cannot be left in an indeterminate state

类和结构体有必要在创立该类或结构体的实例时将其一切的存储特点设置为恰当的初始值。存储特点不能被留在不确定的状况中。

在实际中,一直满意这种说法是恰当棘手的。这其中有许多原因。

有时咱们在创立一个类或结构体时没有足够的信息来供给一套合理的初始值。有时,初始化这些特点是没有意义的。不管是什么原因,在没有彻底初始化类或结构体的状况下退出初始化阶段是很常见的。UIViewControllers 便是这样的一个比如。

关于 UIViewController(以及许多其他依据视图的类)来说,初始化被分成两个不同的阶段。

在榜首阶段,UIViewController 类的实例被创立,初始值被分配给该实例的不同特点(一般是经过它们的 init 函数的一些变体)。但问题是,在这一点上,该类的 IBOutlets 还没有被连接,由于该类的视图还没有被加载。这使得该类在 init 函数结束时被部分初始化。

直到初始化的第二阶段,该类的视图才被加载。此时,视图和任何在 loadViewviewDidLoad 办法中创立的子视图都被增加到视图层次中,IBOutlets 在被呈现在屏幕上之前被连接起来。这儿需求留意的要害点是,这是在类的首要初始化完成后进行的。

那么问题来了,怎么满意 Swift 的要求,也便是说:即使咱们还不能连接视图和 IBOutlets,在初始化结束时,类的一切存储特点都有恰当的初始值。这便是隐式解包可选类型的用武之地。

经过将 IBOutlet 特点界说为隐式解包,咱们能够满意 Swift 的要求。这是由于作为可选类型,它们的初始值默以为 nil,因而在 Swift 编译器眼中被以为是初始化的。

把它们标记为隐式解包的可选类型(而不是一般可选类型)的好处是,一旦连接起来,这些特点依然能够像一般的非可选类型特点相同被引用,而不是运用咱们现已看到的额定的可选类型语法:

var label : UILabel!
//...
label.text = "Hello World"

正如你所看到的,这是一个恰当特别的状况,但在隐式解包的可选类型中作业得恰当好。

在可失利结构器中运用隐式解包可选类型

在 Swift 中运用隐式解包可选类型的第二个比如是在可失利结构器中运用它们。

在编写 Swift 代码时,界说一个初始化或许失利的类、结构体或枚举有时会很有用。这或许有许多原因。初始化参数不正确或缺失,没有一些外部资源或其他一系列的原因之一。

为了应对这些状况,Swift 答应咱们界说一个或多个可失利结构器作为结构体、类或枚举界说的一部分。这些可失利结构器是可失利的,因而它们能够回来一个指定类型的初始化目标,也能够失利并回来 nil

为了将一个结构器标记为可失利的,咱们在 init 要害字后边但在办法括号之前增加一个?

// This WON'T compile!!
class PersonClass {
    let name : String
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}
// Returns the error: All stored properties of a class must be
// initialized before returning nil from an initializer.

不过你能够看到,在这个比如中,咱们有一个问题。它不会被编译。

在这个比如中,我声明了一个有可失利结构器的类,在这种状况下,假如传入结构器的姓名是一个空的字符串,初始化就会失利。

现在,可失利结构器的一般规矩是,一旦遇到过错,就回来 nil。但依据界说,这意味着在结构器回来时,目标中的一切特点或许都现已被初始化。Swift 现已发现了这个实际,因而不会编译咱们的代码,由于它违反了 “一切特点有必要被初始化的规矩”。

正如咱们刚刚讨论的那样,隐式解包的可选类型答应咱们在有效值尚未分配的状况下,界说一个特点的初始值为 nil,但依然答应咱们拜访这些值,而不需求额定的可选类型语法的担负。咱们能够经过可失利结构器来运用这一实际。

在这个比如中,假如咱们把 name 特点从一般的String类型改为隐式解包的optional类型,咱们就能够满意 Swift 的要求,一起还能够用一般的特点语法来拜访这些特点:

class PersonClass {
    var name : String! // 将存储特点声明为隐式解包可选类型
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

顺便提一下,为了让这个办法在 Swift 2.0 中发挥效果,咱们还有必要将 name 特点从常量改为变量。

这是由于早期版别的 Swift 存在一个漏洞,或许导致常量特点在结构器中被多次赋值。苹果公司后来封闭了这个漏洞,但结果是,咱们现在有必要在这种状况下运用变量而不是常量。从这个论坛主题中能够看出,Chris Lattner 以为这是言语中的一个缺点,所以它或许会在未来的 Swift 版别中被修复,但现在,请记住这一点。


2021-12-22

译者注:我在 Xcode Version 13.2.1 中测试以上代码时,没有遇到编译器编译不经过的状况。

struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty { return nil }
        self.species = species
    }
}
let someCreature = Animal(species: "Giraffe")
// 这儿 someCreature 的类型是 Animal? 而不是 Animal
if let graffe = someCreature {
    print("An animal was initialized with a species of (graffe.species)")
}
// prints "An animal was initialized with a species of Giraffe"

总归,我打算暂时把隐式解包的可选类型留在那里。它们有许多东西,也有许多东西需求你去考虑,可是请记住,它们是为 Swift 中一些非常特别的用例而规划的,在这些状况下,它们非常好,可是除了这些特别状况,它们(就像强制解包)有很高的运行时过错危险,所以要小心运用它们。

跟着隐式解包可选类型的结束,让咱们来看看今日的事情,看看空合运算符,这是 Swift 中的一个新运算符,与可选类型一同运用时特别有用。

空合运算符

有时,当一个可选类型没有值时,你想供给一个默认值。显然,咱们能够用 if 句子或运用三元操作符来做到这一点:

let optionalValue = myFunc() // May return `nil`
7 + (optionalValue != nil ? optionalValue! : 0)

留意:在这种状况下,问号不是用来标明可选类型的,它是三元运算符的语法的一部分。

假如你不熟悉三元运算符,比如中的第二行代码基本上是说:“假如 optionalValue 有一个值,就运用它,不然运用0”。

不过,有了 nil 空合运算符,咱们能够对此进行改进。

nil 空合运算符被写成双问号(??),是一种缩短上述表达式的办法。它的语法与三元组运算符类似,但答应咱们省去对 nil 的查看:

7 + (optionalValue ?? 0)

它所做的只是在可选类型包括值的状况下回来解包的可选类型的值,或许在可选类型为 nil 的状况下回来运算符之后的值。这只是一种快捷语法,但的确能够让咱们的代码更简略阅览。

总归,今日就到这儿了。像平常相同,假如你有任何问题、意见,或许我有任何过错,请与我联系。