2.1 基础

原文链接: https://docs.swift.org/latest/documentation/the-swift-programming-language/thebasics/

2.1 基础

使用常见的数据类型,并掌握最基本的语法。

Swift 提供了许多基本数据类型,包括表示整数的 Int、表示浮点值的 Double、表示布尔值的 Bool 以及表示文本的 String。Swift 还提供了三种主要集合类型的强化版本:Array、Set 和 Dictionary,详见集合类型。

Swift 用变量按名字来存储和引用值。Swift 也大量使用值不可更改的变量,也就是常量;在不需要改变的值上使用常量,能让代码更安全,意图也更清晰。

除了这些熟悉的类型,Swift 还引入了元组(tuple)之类的高级类型。元组让你可以把若干值组合在一起创建和传递,也可以用它让函数以单个复合值的形式返回多个值。

Swift 用可选类型来处理值的缺失。可选类型表示"确实有一个值,它是 x",或者"压根儿就没有值"。可选类型保证代码在使用值之前总会检查值是否缺失,而非可选的值则保证永远不会缺失。

Swift 是一门安全的语言,这意味着在开发过程中,它能让你更早地发现并修复好几类 bug,也能让你确保某些类型的 bug 根本不会发生。类型安全让你能清楚地知道代码所处理的值是什么类型:如果某段代码需要 String,类型安全会阻止你误把 Int 传进去。内存安全则保证你只操作有效的数据,而不是未初始化的内存或已被销毁的对象,并且保证你以安全的方式操作这些数据——即使程序在同时运行多段代码时也是如此。Swift 的大部分安全检查都在构建代码时完成,某些情况下还会在代码运行时做额外的检查。

常量与变量

常量与变量把一个名字(例如 maximumNumberOfLoginAttempts 或 welcomeMessage)和某个特定类型的值(例如数字 10 或字符串 "Hello")关联起来。常量的值一旦设定就不能再改,而变量可以在之后被设为另一个值。

声明常量与变量

常量与变量必须先声明再使用。用 let 关键字声明常量,用 var 关键字声明变量。下面的例子演示如何用常量与变量记录用户尝试登录的次数:

1
2
let maximumNumberOfLoginAttempts = 10
var currentLoginAttempt = 0

这段代码可以读作:

“声明一个名为 maximumNumberOfLoginAttempts 的新常量,把它的值设为 10。然后声明一个名为 currentLoginAttempt 的新变量,把它的初始值设为 0。”

在这个例子中,允许尝试登录的最大次数被声明为常量,因为这个最大值永远不变;而当前登录尝试的计数器被声明为变量,因为每次登录失败之后这个值都要递增。

如果代码中存储的某个值不会改变,就总是用 let 关键字把它声明为常量。只有需要存放会变化的值时才使用变量。

声明常量或变量时,可以像上面的例子那样在声明的同时给它一个值;也可以稍后在程序中赋初始值,只要保证在第一次读取它之前已经有值即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var environment = "development"
let maximumNumberOfLoginAttempts: Int
// maximumNumberOfLoginAttempts 还没有值。

if environment == "development" {
    maximumNumberOfLoginAttempts = 100
} else {
    maximumNumberOfLoginAttempts = 10
}
// 现在 maximumNumberOfLoginAttempts 有值了,可以读取。

在这个例子里,登录尝试的最大次数是常量,它的取值取决于所处的环境:在开发环境中是 100,在其他任何环境中是 10。if 语句的两个分支都会给 maximumNumberOfLoginAttempts 赋一个值,从而保证这个常量总能得到值。关于 Swift 在这种赋值方式下如何检查你的代码,参见常量声明。

你可以在一行里声明多个常量或多个变量,用逗号分隔:

1
var x = 0.0, y = 0.0, z = 0.0

类型标注

声明常量或变量时,你可以加上类型标注,明确说明这个常量或变量能存放什么类型的值。类型标注的写法是:在常量或变量名后面加一个冒号,再跟一个空格,然后是所用类型的名字。

下面这个例子为变量 welcomeMessage 添加了类型标注,表示这个变量可以存放 String 值:

1
var welcomeMessage: String

声明中的冒号表示"……的类型是……",所以上面这行代码可以读作:

“声明一个名为 welcomeMessage、类型为 String 的变量。”

“类型为 String“的意思是"可以存放任意 String 值”。可以把它理解为"能够存放的东西的类型”(或者说"种类")。

现在 welcomeMessage 变量可以被赋为任何字符串值,不会出错:

1
welcomeMessage = "Hello"

你可以在一行里定义多个同类型的相关变量,用逗号分隔,只在最后一个变量名后面写一次类型标注:

1
var red, green, blue: Double

注意:实践中很少需要写类型标注。 如果在定义常量或变量时就提供了初始值, Swift 几乎总能推断出该常量或变量应该使用的类型, 详见类型安全与类型推断。 在上面的 welcomeMessage 例子里没有提供初始值, 因此 welcomeMessage 变量的类型是通过类型标注指定的, 而不是由初始值推断出来的。

为常量与变量命名

常量与变量名几乎可以包含任何字符,包括 Unicode 字符:

1
2
3
let π = 3.14159
let 你好 = "你好世界"
let 🐶🐮 = "dogcow"

常量与变量名不能包含空白字符、数学符号、箭头、私用 Unicode 标量值,以及线条与制表符绘制字符;也不能以数字开头,尽管数字可以出现在名字的其他位置。

一旦声明了某个类型的常量或变量,就不能用同一个名字再次声明它,也不能把它改成存放其他类型的值;同样,不能把常量变成变量,也不能把变量变成常量。

注意:如果需要给常量或变量起一个与 Swift 保留关键字相同的名字, 就把这个关键字用反引号(`)包起来再作为名字使用。 不过,除非实在没有别的选择,否则应避免用关键字作名字。

你可以把已有变量的值改为同一兼容类型下的另一个值。在下面的例子里,friendlyWelcome 的值从 "Hello!" 改成了 "Bonjour!":

1
2
3
var friendlyWelcome = "Hello!"
friendlyWelcome = "Bonjour!"
// friendlyWelcome 现在的值是 "Bonjour!"。

与变量不同,常量的值一旦设定就不能再改。试图修改它会被编译器当作错误报告出来:

1
2
3
let languageName = "Swift"
languageName = "Swift++"
// 这是编译期错误:languageName 不能被修改。

打印常量与变量

你可以用 print(_:separator:terminator:) 函数打印常量或变量的当前值:

1
2
print(friendlyWelcome)
// 输出 "Bonjour!"。

print(_:separator:terminator:) 是一个全局函数,它把一个或多个值输出到合适的位置。例如在 Xcode 中,它会把输出打印到 Xcode 的"控制台"面板里。separator 和 terminator 参数都有默认值,因此调用时可以省略。默认情况下,该函数会在输出内容的末尾加一个换行符来结束这一行。若想在输出之后不换行,就把空字符串作为终止符传入——例如 print(someValue, terminator: "")。关于带默认值的参数,参见默认参数值。

Swift 用字符串插值把常量或变量的名字作为占位符放进更长的字符串中,并让 Swift 用该常量或变量的当前值替换它。把名字放进圆括号,并在左圆括号前加一个反斜杠作为转义:

1
2
print("The current value of friendlyWelcome is \(friendlyWelcome)")
// 输出 "The current value of friendlyWelcome is Bonjour!"。

注意:字符串插值可以使用的全部选项, 都在字符串插值中介绍。

注释

用注释在代码中加入不会执行的文字,作为给自己的说明或提醒。编译代码时,Swift 编译器会忽略注释。

Swift 的注释与 C 语言的注释非常相似。单行注释以两个正斜杠(//)开头:

1
// 这是一条注释。

多行注释以正斜杠加星号(/*)开始,以星号加正斜杠(*/)结束:

1
2
/* 这也是一条注释,
但写在多行里。 */

与 C 语言的多行注释不同,Swift 的多行注释可以嵌套在其他多行注释内部。写嵌套注释的方法是:先开始一个多行注释块,然后在这个块里再开始第二个多行注释块,接着关闭第二个块,再关闭第一个块:

1
2
3
/* 这是第一个多行注释的开始。
    /* 这是第二个嵌套的多行注释。 */
这是第一个多行注释的结尾。 */

嵌套多行注释让你能够快速、方便地把大段代码注释掉,即使这段代码里已经含有其他多行注释也没问题。

分号

与许多其他语言不同,Swift 并不要求你在这条语句末尾写分号(;),当然你想写也可以。不过,如果你想在一行里写多条相互独立的语句,那么分号是必须的:

1
2
let cat = "🐱"; print(cat)
// 输出 "🐱"。

整数

整数是没有小数部分的整数,例如 42 和 -23。整数要么是有符号的(正数、零或负数),要么是无符号的(正数或零),其最大值和最小值取决于它的尺寸(用来存放值的位数)。整数类型的名字里同时包含了尺寸和符号——例如 8 位无符号整数的类型是 UInt8,32 位有符号整数的类型是 Int32。和 Swift 中所有类型一样,这些整数类型的名字以大写字母开头。大多数情况下,当你不需要指定确切的整数尺寸时,就使用下面介绍的 Int 类型。

整数类型的表现和你用笔算的算术差不多:整数运算给出的结果不做近似。这些特性使整数适合用于计数以及其他表示精确数量的计算——比如找出文本文件中最长的一行、在游戏里施加分数倍率,或者把收据上的价格加起来。

虽然整数没有小数部分,但你可以通过计数"分"的方式,用整数来表示带有小数部分的量。例如,可以把 123 存放在一个以"分"为单位计数的整数里,用它表示 1.23 美元。这种做法称为定点数运算,因为小数点在数字中的位置是固定的。在上面的例子里,数字 123 被理解为小数点位于最后两位数字之前。

注意: 对于金融、建筑这类受监管领域的计算, 或者对结果精度有较高要求的场景, 你可能需要一个专用的数值类型, 按照该领域的要求实现舍入、截断之类的行为。

整数的取值范围

你可以通过每个整数类型的 min 和 max 属性访问它的最小值和最大值:

1
2
let minValue = UInt8.min  // minValue 等于 0,类型是 UInt8
let maxValue = UInt8.max  // maxValue 等于 255,类型是 UInt8

这些属性的值是相应尺寸的数值类型(例如上面例子中的 UInt8),因此可以和同类型的其他值一起出现在表达式中。

会产生越界结果的计算——比如得到比 max 属性更大的数——会中止程序的执行,而不是存放一个无效结果。你也可以显式地让运算溢出,详见溢出运算符。

Int

大多数情况下,你不需要为代码中的整数挑选具体的尺寸。Swift 提供了另一种整数类型 Int,它的尺寸与当前平台的机器字长相同:

  • 在 32 位平台上,Int 与 Int32 尺寸相同。
  • 在 64 位平台上,Int 与 Int64 尺寸相同。

除非确实需要特定尺寸的整数,否则代码中的整数值应始终使用 Int。这有助于代码的一致性与互操作性。即使在 32 位平台上,Int 也能存放 -2,147,483,648 到 2,147,483,647 之间的任何值,对许多整数范围来说都足够大。

UInt

Swift 还提供了无符号整数类型 UInt,它的尺寸与当前平台的机器字长相同:

  • 在 32 位平台上,UInt 与 UInt32 尺寸相同。
  • 在 64 位平台上,UInt 与 UInt64 尺寸相同。

注意:只有当你明确需要一个与平台机器字长相同的无符号整数类型时, 才使用 UInt。 如果不是这种情况,就应当优先使用 Int, 即使待存放的值已知为非负数也是如此。 统一使用 Int 表示整数值有助于代码的互操作性, 免去在不同数值类型之间转换的麻烦, 也与整数类型推断保持一致,详见类型安全与类型推断。

浮点数

浮点数带有小数部分,例如 3.14159、0.1 和 -273.15。就像整数有不同尺寸一样,Swift 也提供了多种支持不同数值范围的浮点类型。如果你不需要指定确切的尺寸,就使用 Double;否则就使用名字里带有所需尺寸的类型,例如 Float16 或 Float80。按照浮点运算的常见叫法,Float 使用 32 位,Double 使用 64 位。这些类型也可以写成 Float32 或 Float64。例如,图形代码常用 Float,以便与 GPU 最快的类型保持一致。有些浮点类型只在特定平台上受支持,而 Float 和 Double 在所有平台上都可以使用。

浮点数可以处理非常小和非常大的数,但无法表示该范围内的每一个可能的值。与总能给出精确结果的整数运算不同,浮点运算会把结果舍入到最接近的可表示数值。例如,把 10,000 存为 Float 时,下一个更大的可表示数是 10,000.001——介于这两个数之间的值都会被舍入到其中之一。数值之间的间隔还是可变的:大数之间的间隔比小数之间的间隔大。例如,0.001 之后的下一个 Float 值是 0.0010000002,这个间隔比 10,000 之后的间隔要小。

浮点数还有负零、正无穷和负无穷这些取值,用来表示计算中的上溢和下溢;此外还有"非数"(NaN)值,用来表示无效或未定义的结果,比如零除以零。这种行为与整数不同:整数在无法表示结果时会中止程序。

如果你需要在所有可能取值之间保持相同的间隔,或者所做计算要求精确结果、不需要使用上面列出的那些特殊值,那么浮点数可能并不是合适的数据类型。这时可以考虑使用定点数,详见整数。

类型安全与类型推断

Swift 程序中的每个值都有类型,你存放值的每个地方——包括常量、变量和属性——也都有类型。你可以用类型标注显式写出类型,Swift 也可以根据初始值推断出类型。代码中每一处提供值的地方,值的类型都必须与使用它的地方相匹配。例如,如果某段代码需要 String,你就不能误把 Int 传给它。这类检查使 Swift 成为一门类型安全的语言。

类型安全的语言会促使你明确代码所处理值的类型。一种类型的值永远不会被隐式转换成另一种类型,不过有些类型可以被显式转换。构建代码时,Swift 会检查代码的类型安全性,并把任何类型不匹配标记为错误。

类型检查有助于你在处理不同类型的值时避免出错。但这并不意味着你必须为声明的每个常量和变量都指定类型。如果你没有指定所需的值的类型,Swift 会使用类型推断来推断出合适的类型。类型推断让编译器在编译代码时,仅仅通过检查你提供的值就能自动推断出某个表达式的类型。

有了类型推断,Swift 所需的类型声明远少于 C 或 Objective-C 之类的语言。常量和变量仍然有明确的类型,只是其中大部分指定类型的工作都由 Swift 替你完成了。

当你用初始值声明常量或变量时,类型推断特别有用。常见的做法是在声明的同时把一个字面量赋给该常量或变量。(字面量就是直接出现在源代码中的值,比如下面例子里的 42 和 3.14159。)

例如,如果你把一个字面量 42 赋给一个新常量而不说明它的类型,Swift 会推断你希望这个常量是 Int,因为你是用一个看起来像整数的数字来初始化它的:

1
2
let meaningOfLife = 42
// meaningOfLife 被推断为 Int 类型

同样,如果你没有为浮点字面量指定类型,Swift 会推断你想要创建一个 Double:

1
2
let pi = 3.14159
// pi 被推断为 Double 类型

在推断浮点数的类型时,Swift 总是选择 Double(而不是 Float)。

如果在一个表达式中同时使用整数字面量和浮点字面量,那么根据上下文会推断出 Double 类型:

1
2
let anotherPi = 3 + 0.14159
// anotherPi 同样被推断为 Double 类型

字面量 3 本身没有明确的类型,因此由加法中出现的浮点字面量推断出合适的输出类型为 Double。

数值字面量

整数字面量可以写成:

  • 十进制数,不带前缀
  • 二进制数,带 0b 前缀
  • 八进制数,带 0o 前缀
  • 十六进制数,带 0x 前缀

下面这些整数字面量的十进制值都是 17:

1
2
3
4
let decimalInteger = 17
let binaryInteger = 0b10001       // 二进制表示法的 17
let octalInteger = 0o21           // 八进制表示法的 17
let hexadecimalInteger = 0x11     // 十六进制表示法的 17

浮点字面量可以是十进制(不带前缀),也可以是十六进制(带 0x 前缀)。它们的十进制小数点两边必须各有一个数字(十六进制数同样如此)。十进制浮点数还可以带可选的指数,用大写或小写的 e 表示;十六进制浮点数则必须有指数,用大写或小写的 p 表示。

对于指数为 x 的十进制数,其基数值要乘以 10ˣ:

  • 1.25e2 表示 1.25 x 10²,即 125.0。
  • 1.25e-2 表示 1.25 x 10⁻²,即 0.0125。

对于指数为 x 的十六进制数,其基数值要乘以 2ˣ:

  • 0xFp2 表示 15 x 2²,即 60.0。
  • 0xFp-2 表示 15 x 2⁻²,即 3.75。

下面这些浮点字面量的十进制值都是 12.1875:

1
2
3
let decimalDouble = 12.1875
let exponentDouble = 1.21875e1
let hexadecimalDouble = 0xC.3p0

数值字面量可以包含额外的格式,以便阅读。整数和浮点数都可以用多余的零补齐,也可以包含下划线来帮助阅读。这两种格式都不会影响字面量本身的值:

1
2
3
let paddedDouble = 000123.456
let oneMillion = 1_000_000
let justOverOneMillion = 1_000_000.000_000_1

数值类型转换

代码中通用的整数常量和变量都应使用 Int 类型,即使它们已知为非负数。在日常场景中使用默认的整数类型,意味着整数常量和变量在代码中可以立即互操作,也与整数字面量推断出的类型一致。

只有在手头的任务确实需要时——比如外部数据明确指定了尺寸,或者出于性能、内存占用以及其他必要的优化考虑——才使用其他整数类型。在这些场景中使用明确尺寸的类型,有助于发现意外的值溢出,也隐式说明了所用数据的性质。

整数转换

每种数值类型的整数常量或变量能存放的数值范围各不相同。Int8 常量或变量可以存放 -128 到 127 之间的数,而 UInt8 常量或变量可以存放 0 到 255 之间的数。如果某个数放不进特定尺寸整数类型的常量或变量,编译代码时就会报错:

1
2
3
4
5
let cannotBeNegative: UInt8 = -1
// UInt8 不能存放负数,因此这里会报错
let tooBig: Int8 = Int8.max + 1
// Int8 不能存放比它最大值更大的数,
// 因此这里同样会报错

由于每种数值类型能存放的范围不同,你必须逐个地选择是否进行数值类型转换。这种"按需显式选择"的做法可以避免隐藏的转换错误,也让代码中类型转换的意图更加明确。

要把某个数值类型转换成另一种,就用已有的值初始化一个目标类型的新数值。在下面的例子里,常量 twoThousand 的类型是 UInt16,而常量 one 的类型是 UInt8。它们不能直接相加,因为类型不同。因此这个例子调用 UInt16(one) 创建了一个用 one 的值初始化的新 UInt16,并用它替换原来的值:

1
2
3
let twoThousand: UInt16 = 2_000
let one: UInt8 = 1
let twoThousandAndOne = twoThousand + UInt16(one)

现在加法两边都是 UInt16 类型,因此允许相加。输出常量(twoThousandAndOne)被推断为 UInt16 类型,因为它是两个 UInt16 值之和。

SomeType(ofInitialValue) 是调用 Swift 类型的构造器并传入初始值的默认写法。在背后,UInt16 有一个接受 UInt8 值的构造器,因此这里用这个构造器从已有的 UInt8 创建新的 UInt16。不过,你并不能在这里传入任意类型——它必须是 UInt16 为其提供构造器的类型。为已有类型(包括你自己定义的类型)扩展出接受新类型的构造器,会在扩展中介绍。

整数与浮点数转换

整数与浮点数值类型之间的转换必须显式进行:

1
2
3
4
let three = 3
let pointOneFourOneFiveNine = 0.14159
let pi = Double(three) + pointOneFourOneFiveNine
// pi 等于 3.14159,被推断为 Double 类型

这里用常量 three 的值创建了一个 Double 类型的新值,使加法两边类型相同。如果不做这次转换,加法就会被禁止。

浮点数到整数的转换同样必须显式进行。整数类型可以用 Double 或 Float 值来初始化:

1
2
let integerPi = Int(pi)
// integerPi 等于 3,被推断为 Int 类型

用这种方式初始化新的整数值时,浮点值总会被截断。也就是说,4.75 会变成 4,-3.9 会变成 -3。

注意:数值常量与变量之间的组合规则,和数值字面量之间的组合规则不同。 字面量 3 可以直接与字面量 0.14159 相加, 因为数字字面量本身没有明确的类型, 它们的类型要等到编译器对其求值时才被推断出来。

类型别名

类型别名为已有类型定义一个替代名字。用 typealias 关键字定义类型别名。

当你想用更符合上下文的名字来称呼某个已有类型时,类型别名就很有用,比如处理来自外部数据源的特定尺寸数据时:

1
typealias AudioSample = UInt16

定义类型别名之后,凡是能用原类型名的地方都可以使用这个别名:

1
2
var maxAmplitudeFound = AudioSample.min
// maxAmplitudeFound 现在的值是 0

这里把 AudioSample 定义为 UInt16 的别名。由于它只是一个别名,调用 AudioSample.min 实际上是在调用 UInt16.min,它为 maxAmplitudeFound 变量提供了初始值 0。

布尔值

Swift 有一个基本的布尔类型,称为 Bool。布尔值被称为逻辑值,因为它只可能是真或假。Swift 提供了两个布尔常量值:true 和 false:

1
2
let orangesAreOrange = true
let turnipsAreDelicious = false

orangesAreOrange 和 turnipsAreDelicious 的类型被推断为 Bool,因为它们是用布尔字面量初始化的。和上面的 Int、Double 一样,如果你在创建常量或变量时就把它设为 true 或 false,就不需要把它声明为 Bool。

在处理 if 语句之类的条件语句时,布尔值特别有用:

1
2
3
4
5
6
if turnipsAreDelicious {
    print("Mmm, tasty turnips!")
} else {
    print("Eww, turnips are horrible.")
}
// 输出 "Eww, turnips are horrible."。

if 语句之类的条件语句在控制流中有更详细的介绍。

Swift 的类型安全会阻止用非布尔值替代 Bool。下面这个例子会报编译期错误:

1
2
3
4
let i = 1
if i {
    // 这个例子无法通过编译,会报错
}

不过,下面这个替代例子是合法的:

1
2
3
4
let i = 1
if i == 1 {
    // 这个例子可以顺利编译
}

i == 1 比较的结果是 Bool 类型,因此第二个例子能通过类型检查。像 i == 1 这样的比较在基本运算符中讨论。

和 Swift 中其他体现类型安全的例子一样,这种做法可以避免意外错误,并确保某段代码的意图始终清晰。

元组

元组把多个值组合成一个复合值。元组中的值可以是任意类型,而且彼此不必是同一类型。

在这个例子里,(404, "Not Found") 是一个描述 HTTP 状态码的元组。HTTP 状态码是每次请求网页时 Web 服务器返回的特殊值。如果你请求的网页不存在,就会返回状态码 404 Not Found。

1
2
let http404Error = (404, "Not Found")
// http404Error 的类型是 (Int, String),等于 (404, "Not Found")

元组 (404, "Not Found") 把一个 Int 和一个 String 组合在一起,为 HTTP 状态码提供了两个独立的值:一个数字和一段人类可读的描述。它可以被描述为"一个 (Int, String) 类型的元组"。

你可以用任意类型组合出元组,其中也可以包含任意多种类型。没有什么能阻止你使用 (Int, Int, Int) 或 (String, Bool) 类型的元组,任何你需要的组合都可以。

你可以把元组的内容分解成几个独立的常量或变量,然后照常使用它们:

1
2
3
4
5
let (statusCode, statusMessage) = http404Error
print("The status code is \(statusCode)")
// 输出 "The status code is 404"。
print("The status message is \(statusMessage)")
// 输出 "The status message is Not Found"。

如果你只需要元组中的一部分值,可以在分解元组时用下划线(_)忽略掉其他部分:

1
2
3
let (justTheStatusCode, _) = http404Error
print("The status code is \(justTheStatusCode)")
// 输出 "The status code is 404"。

另一种方式是用从零开始的下标编号访问元组中各个元素的值:

1
2
3
4
print("The status code is \(http404Error.0)")
// 输出 "The status code is 404"。
print("The status message is \(http404Error.1)")
// 输出 "The status message is Not Found"。

你也可以在定义元组时为各个元素命名:

1
let http200Status = (statusCode: 200, description: "OK")

如果你给元组中的元素起了名字,就可以用元素名访问这些元素的值:

1
2
3
4
print("The status code is \(http200Status.statusCode)")
// 输出 "The status code is 200"。
print("The status message is \(http200Status.description)")
// 输出 "The status message is OK"。

元组作为函数的返回值特别有用。一个尝试获取网页的函数可以返回 (Int, String) 元组类型,用来描述网页获取的成功或失败。通过返回包含两个不同类型值的元组,函数能提供比"只能返回某个单一类型的单一值"更有用的结果信息。更多内容参见返回多个值的函数。

注意:元组适合用来组合简单的相关值, 但不适合用来创建复杂的数据结构。 如果你的数据结构可能会比较复杂, 就把它建模成类或结构体,而不是元组。 更多内容参见结构体与类。

可选类型

当某个值可能缺失时,你就需要用到可选类型。可选类型表示两种可能:要么存在一个指定类型的值,你可以解包这个可选值来访问它;要么压根儿不存在任何值。

为了举一个可能缺失的值的例子,Swift 的 Int 类型有一个构造器,它会尝试把 String 值转换成 Int 值。然而只有一部分字符串能转换成整数:字符串 "123" 可以转换成数值 123,而字符串 "hello, world" 没有对应的数值。下面这个例子用该构造器尝试把 String 转换成 Int:

1
2
3
let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber 的类型是 "optional Int"

因为上面代码中的构造器可能失败,所以它返回的是可选 Int,而不是 Int。

书写可选类型时,在可选类型所包含的类型名后面写一个问号(?)——例如可选 Int 的类型是 Int?。可选 Int 要么包含某个 Int 值,要么不包含任何值,不可能包含其他东西,比如 Bool 或 String 值。

nil

把特殊值 nil 赋给可选变量,就让它处于没有值的状态:

1
2
3
4
var serverResponseCode: Int? = 404
// serverResponseCode 包含实际的 Int 值 404
serverResponseCode = nil
// serverResponseCode 现在不包含任何值

如果你定义可选变量时没有提供默认值,这个变量会自动被设为 nil:

1
2
var surveyAnswer: String?
// surveyAnswer 被自动设为 nil

你可以用 if 语句把可选值与 nil 比较,来判断它是否包含值。这种比较使用"等于"运算符(==)或"不等于"运算符(!=)。

如果可选值有值,它就被认为"不等于" nil:

1
2
3
4
5
6
7
let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)

if convertedNumber != nil {
    print("convertedNumber contains some integer value.")
}
// 输出 "convertedNumber contains some integer value."。

nil 不能用于非可选的常量或变量。如果代码中的某个常量或变量在某些情况下需要表示值的缺失,就把它声明为相应类型的可选值。声明为非可选值的常量或变量保证永远不会包含 nil。如果你试图把 nil 赋给非可选值,就会得到编译期错误。

把可选值与非可选值区分开来,让你能够显式标明哪些信息可能缺失,也让处理缺失值的代码更容易编写。你不可能把可选值误当作非可选值使用,因为这种错误在编译期就会暴露。解包之后,操作该值的其他代码都不再需要检查 nil,因此也不必在代码的不同位置反复检查同一个值。

访问可选值时,你的代码总要同时处理 nil 和非 nil 两种情况。值缺失时可以采取多种做法,下面几节会分别介绍:

  • 当值为 nil 时跳过针对该值的代码。

  • 传递 nil 值,比如返回 nil,或使用 可选链中介绍的 ?. 运算符。

  • 用 ?? 运算符提供一个回退值。

  • 用 ! 运算符中止程序执行。

注意: 在 Objective-C 中,nil 是指向不存在对象的指针。 在 Swift 中,nil 不是指针——它表示某个类型的值缺失。 任何类型的可选值都可以设为 nil,不只是对象类型。

可选绑定

用可选绑定判断可选值是否包含值,如果包含,就把这个值作为临时的常量或变量来使用。可选绑定可以与 if、guard 和 while 语句一起使用,在一次操作中既检查可选值里是否有值,又把该值提取到常量或变量中。关于 if、guard 和 while 语句的更多内容,参见控制流。

为 if 语句写可选绑定的形式如下:

1
2
3
if let <#constantName#> = <#someOptional#> {
   <#statements#>
}

你可以把可选类型一节中的 possibleNumber 例子改写成使用可选绑定,而不是强制解包:

1
2
3
4
5
6
if let actualNumber = Int(possibleNumber) {
    print("The string \"\(possibleNumber)\" has an integer value of \(actualNumber)")
} else {
    print("The string \"\(possibleNumber)\" couldn't be converted to an integer")
}
// 输出 "The string "123" has an integer value of 123"。

这段代码可以读作:

“如果 Int(possibleNumber) 返回的可选 Int 包含值,就把这个值赋给一个名为 actualNumber 的新常量。”

如果转换成功,常量 actualNumber 就可以在 if 语句的第一个分支中使用。它已经用可选值中包含的值初始化好了,并且具有对应的非可选类型。在这个例子里,Int(possibleNumber) 的类型是 Int?,因此 actualNumber 的类型是 Int。

如果在访问过可选值中包含的值之后不再需要引用原来那个可选常量或变量,你可以给新常量或变量取同样的名字:

1
2
3
4
5
6
7
let myNumber = Int(possibleNumber)
// 这里 myNumber 是可选整数
if let myNumber = myNumber {
    // 这里 myNumber 是非可选整数
    print("My number is \(myNumber)")
}
// 输出 "My number is 123"。

这段代码首先检查 myNumber 是否包含值,和上一个例子完全一样。如果 myNumber 有值,就创建一个名为 myNumber 的新常量并把该值赋给它。在 if 语句体内,写 myNumber 指的就是这个新的非可选常量;而在 if 语句之前或之后写 myNumber,指的则是原来那个可选整数常量。

由于这种写法太常见了,你还可以用更简短的形式解包可选值:只写你要解包的那个常量或变量的名字。新的、已解包的常量或变量隐式使用与可选值相同的名字。

1
2
3
4
if let myNumber {
    print("My number is \(myNumber)")
}
// 输出 "My number is 123"。

可选绑定既可以使用常量也可以使用变量。如果你想在 if 语句的第一个分支里修改 myNumber 的值,可以改写成 if var myNumber,这样可选值中包含的值就会以变量而不是常量的形式提供。在 if 语句体内对 myNumber 所做的修改只作用于这个局部变量,不会影响你解包的那个原可选常量或变量。

你可以在一个 if 语句中按需要加入任意多个可选绑定和布尔条件,用逗号分隔。只要任意一个可选绑定得到的值是 nil,或者任意一个布尔条件求值为 false,整个 if 语句的条件就被视为 false。下面两个 if 语句是等价的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
    print("\(firstNumber) < \(secondNumber) < 100")
}
// 输出 "4 < 42 < 100"。

if let firstNumber = Int("4") {
    if let secondNumber = Int("42") {
        if firstNumber < secondNumber && secondNumber < 100 {
            print("\(firstNumber) < \(secondNumber) < 100")
        }
    }
}
// 输出 "4 < 42 < 100"。

在 if 语句中通过可选绑定创建的常量或变量,只在 if 语句体内可用。相比之下,用 guard 语句创建的常量或变量在 guard 语句之后的代码行中都可用,详见提前退出。

提供回退值

处理缺失值的另一种做法是用空合运算符(??)提供一个默认值。如果 ?? 左边的可选值不是 nil,就解包并使用该值;否则就使用 ?? 右边的值。例如,下面的代码在指定了名字时按名字问候,而当名字为 nil 时使用通用的问候语。

1
2
3
4
let name: String? = nil
let greeting = "Hello, " + (name ?? "friend") + "!"
print(greeting)
// 输出 "Hello, friend!"。

关于用 ?? 提供回退值的更多内容,参见空合运算符。

强制解包

当 nil 表示无法恢复的失败——比如程序员的错误或已损坏的状态——时,你可以在可选值名字末尾加一个感叹号(!)来访问它底层的值。这称为强制解包该可选值。强制解包一个非 nil 的值,得到的就是它解包后的值;而强制解包一个 nil 值会触发运行时错误。

可以说,! 是 fatalError(_:file:line:) 的简写形式。例如,下面的代码给出两种等价的写法:

1
2
3
4
5
6
7
8
let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)

let number = convertedNumber!

guard let number = convertedNumber else {
    fatalError("The number was invalid")
}

上面两种写法都依赖 convertedNumber 始终包含值。用其中任何一种方式把这一要求写进代码,都能让你的代码在运行时检查该要求是否成立。

关于强制满足数据要求、在运行时检查假设的更多内容,参见断言与前提条件。

隐式解包可选值

如前所述,可选值表示常量或变量允许"没有值"。可以用 if 语句检查可选值是否存在值,也可以用可选绑定有条件地解包,从而在值存在时访问它。

有时从程序结构上就能看出,某个可选值在第一次被赋值之后永远会有值。在这种情况下,每次访问都去检查和解包这个可选值就显得多余了,因为可以安全地假定它始终有值。

这类可选值被称为隐式解包可选值。书写隐式解包可选值时,在想要设为可选的那个类型后面写感叹号(String!)而不是问号(String?)。也就是说,不是在每次使用可选值时在其名字后面加感叹号,而是在声明时在可选值的类型后面加感叹号。

当可选值在第一次定义之后立即被确认有值、并且此后可以确定始终有值时,隐式解包可选值就很有用。在 Swift 中,隐式解包可选值最主要的用途是类初始化期间,详见无主引用与隐式解包可选属性。

如果变量有可能在之后变成 nil,就不要使用隐式解包可选值;如果在变量的整个生命周期中都需要检查 nil,就始终使用普通的可选类型。

隐式解包可选值在背后仍然是普通的可选值,但也可以像非可选值那样使用,每次访问时都不必再解包。下面的例子展示了在把包装的值作为显式 String 访问时,可选字符串与隐式解包可选字符串在行为上的差别:

1
2
3
4
5
let possibleString: String? = "An optional string."
let forcedString: String = possibleString! // 需要显式解包

let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString // 自动解包

你可以把隐式解包可选值理解为"允许需要时对这个可选值做强制解包"。使用隐式解包可选值时,Swift 首先尝试把它当作普通可选值使用;如果无法当作可选值使用,Swift 才会强制解包它。在上面的代码中,可选值 assumedString 在把值赋给 implicitString 之前被强制解包,因为 implicitString 有明确的非可选类型 String。而在下面的代码中,optionalString 没有显式类型,因此它是普通的可选值。

1
2
let optionalString = assumedString
// optionalString 的类型是 "String?",assumedString 没有被强制解包。

如果隐式解包可选值是 nil,而你又去访问它包装的值,就会触发运行时错误;结果完全等同于对一个不包含值的普通可选值写感叹号强制解包。

检查隐式解包可选值是否为 nil 的方式和检查普通可选值一样:

1
2
3
4
if assumedString != nil {
    print(assumedString!)
}
// 输出 "An implicitly unwrapped optional string."。

你也可以把隐式解包可选值用于可选绑定,在一条语句中同时检查和解包它的值:

1
2
3
4
if let definiteString = assumedString {
    print(definiteString)
}
// 输出 "An implicitly unwrapped optional string."。

内存安全

除了前面在类型安全与类型推断中介绍的防止类型不匹配的检查之外,Swift 还会保护代码不去操作无效的内存。这种保护称为内存安全,它包括以下要求:

  • 值在被读取之前已经设置。防止与未初始化内存区域打交道的保护也称为确定性初始化。
  • 数组和缓冲区只在有效的下标处被访问。防止越界访问的保护也称为边界安全。
  • 内存只在值的生命周期内被访问。防止"释放后使用"错误的保护也称为生命周期安全。
  • 对内存的访问只以可证明安全的方式重叠。防止并发代码中可能的数据竞争的保护也称为线程安全。

如果你用过不提供这些保证的语言,那么上面列出的某些错误和 bug 你可能很熟悉。没见过这些问题也没关系:Swift 中的安全代码本来就避开了它们。关于 Swift 如何确保你设置了初始值,参见初始化;关于 Swift 如何在并发代码中检查内存安全,参见并发;关于 Swift 如何检查对内存的重叠访问,参见内存安全。

有时你需要在安全边界之外工作——例如受语言或标准库的限制——因此 Swift 也为某些 API 提供了不安全的版本。当你使用名字中含有 “unsafe”、“unchecked” 或 “unmanaged” 等字样的类型或方法时,你就要自己承担安全责任。

Swift 中的安全代码仍然可能遇到错误和意外失败,它们可能中止程序的执行。安全并不保证你的代码一定执行到底。Swift 提供了多种表示错误并从错误中恢复的方式,分别在下文的错误处理和断言与前提条件中讨论。不过在某些情况下,处理错误的唯一安全方式就是停止执行。如果你需要保证某项服务永远不会意外停止,就要把容错能力纳入其整体架构,使它能够从任何组件意外停止中恢复过来。

错误处理

你用错误处理来应对程序在执行过程中可能遇到的错误状况。

可选类型通过"值存在或缺失"来传达函数的成功或失败,与之不同,错误处理让你能够确定失败的底层原因,并在必要时把错误传递到程序的其他部分。

当函数遇到错误状况时,它会抛出错误;该函数的调用方随后可以捕获这个错误并做出恰当的处理。

1
2
3
func canThrowAnError() throws {
    // 这个函数可能抛出错误,也可能不抛出
}

函数在声明中包含 throws 关键字,就表示它可能抛出错误。调用可能抛出错误的函数时,要在表达式前面加上 try 关键字。

Swift 会自动把错误从当前作用域向外传播,直到被某个 catch 子句处理为止。

1
2
3
4
5
6
do {
    try canThrowAnError()
    // 没有抛出错误
} catch {
    // 抛出了错误
}

do 语句会创建一个新的包含作用域,使错误能够被传播到一个或多个 catch 子句。

下面是一个用错误处理来应对不同错误状况的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func makeASandwich() throws {
    // ...
}

do {
    try makeASandwich()
    eatASandwich()
} catch SandwichError.outOfCleanDishes {
    washDishes()
} catch SandwichError.missingIngredients(let ingredients) {
    buyGroceries(ingredients)
}

在这个例子里,如果没有干净的盘子或者缺少任何食材,makeASandwich() 函数就会抛出错误。由于 makeASandwich() 可能抛出错误,函数调用被包在 try 表达式中。而把函数调用包在 do 语句中,则使抛出的任何错误都会被传播到所提供的 catch 子句中。

如果没有抛出错误,就会调用 eatASandwich() 函数。如果抛出的错误匹配 SandwichError.outOfCleanDishes 这个成员,就会调用 washDishes() 函数;如果抛出的错误匹配 SandwichError.missingIngredients 这个成员,就会用 catch 模式捕获到的关联值 [String] 调用 buyGroceries(_:) 函数。

错误的抛出、捕获与传播在错误处理中有更详细的介绍。

断言与前提条件

断言和前提条件是在运行时进行的检查。你可以用它们确保在执行后续代码之前,某个必不可少的条件已经满足。如果断言或前提条件中的布尔条件求值为 true,代码照常继续执行;如果条件求值为 false,说明程序当前的状态无效,代码执行就此结束,应用也会被终止。

你可以用断言和前提条件把你编写代码时所做的假设与期望表达出来,并把它们写进代码里。断言帮助你在开发过程中发现错误和错误的假设,前提条件则帮助你在生产环境中发现问题。

除了在运行时验证你的预期之外,断言和前提条件还是一种有用的代码内文档。与前面错误处理中讨论的错误状况不同,断言和前提条件并不用于可恢复的或预期内的错误。因为断言或前提条件失败意味着程序状态无效,而无效状态是无法恢复的,也就没有办法捕获失败的断言:断言失败时,程序中至少有一处数据是无效的——但你既不知道它为什么无效,也不知道是不是还有别的状态同样无效。

使用断言和前提条件,并不等于可以不去精心设计代码使无效状况难以出现。不过,用它们来强制保证数据与状态的有效性,能让应用在出现无效状态时更可预测地终止,也更容易调试。如果不去检查这些假设,你可能要等到很久以后、当别处的代码开始明显失败时才发现这类问题,而那时用户数据可能已经被悄悄破坏了。在检测到无效状态时立刻停止执行,也有助于限制该无效状态造成的损害。

断言与前提条件的区别在于检查的时机:断言只在调试构建中检查,前提条件在调试构建和生产构建中都会检查。在生产构建中,断言里的条件不会被求值。这意味着你在开发过程中可以放心地写任意多的断言,而不会影响生产环境的性能。

用断言调试

调用 Swift 标准库中的 assert(_:_:file:line:) 函数就可以写一条断言。传给这个函数的是一个求值为 true 或 false 的表达式,以及当条件结果为 false 时要显示的消息。例如:

1
2
3
let age = -3
assert(age >= 0, "A person's age can't be less than zero.")
// 这条断言会失败,因为 -3 不 >= 0。

在这个例子里,如果 age >= 0 求值为 true,也就是 age 的值为非负数,代码就继续执行;如果 age 是负数(如上面的代码),那么 age >= 0 求值为 false,断言失败,应用随即终止。

你可以省略断言消息——比如当这段消息只是用文字重复条件本身时:

1
assert(age >= 0)

如果代码中已经检查过该条件,就用 assertionFailure(_:file:line:) 函数来表明断言已经失败。例如:

1
2
3
4
5
6
7
if age > 10 {
    print("You can ride the roller-coaster or the ferris wheel.")
} else if age >= 0 {
    print("You can ride the ferris wheel.")
} else {
    assertionFailure("A person's age can't be less than zero.")
}

强制执行前提条件

只要某个条件有可能为假、但代码要继续执行就必须一定为真,就使用前提条件。例如,可以用前提条件检查下标没有越界,或者检查传给函数的值是有效的。

调用 precondition(_:_:file:line:) 函数就可以写一个前提条件。传给这个函数的是一个求值为 true 或 false 的表达式,以及当条件结果为 false 时要显示的消息。例如:

1
2
// 在下标的实现中……
precondition(index > 0, "Index must be greater than zero.")

你也可以调用 preconditionFailure(_:file:line:) 函数来表明发生了失败——例如某个 switch 走到了 default 分支,而所有合法输入数据本应由其他分支处理。

注意:如果以未检查模式(-Ounchecked)编译, 前提条件不会被检查。 编译器假定前提条件总为真, 并据此优化你的代码。 不过,无论优化设置如何,fatalError(_:file:line:) 函数总是会中止执行。

在原型设计和开发早期,你可以用 fatalError(_:file:line:) 函数 为尚未实现的功能创建占位实现, 写法是 fatalError("Unimplemented")。 由于致命错误不像断言或前提条件那样会被优化掉, 你可以确信:只要执行到占位实现,程序就一定会中止。