参考资料

基础知识

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

基础知识

Swift 提供了许多基本数据类型,包括用于整数的 Int、用于浮点值的 Double、用于布尔值的 Bool 以及用于文本的 String。Swift 还提供了三种主要集合类型的强大版本:ArraySetDictionary,详见集合类型章节。

Swift 使用变量通过标识名称来存储和引用值。Swift 还大量使用值不可更改的变量。这些被称为常量,在 Swift 中被广泛使用,以便在处理不需要更改的值时使代码更安全、意图更清晰。

除了常见类型外,Swift 还引入了高级类型,如元组。元组使你能够创建和传递值的组合。你可以使用元组从函数中以单个复合值的形式返回多个值。

Swift 使用可选类型来处理值的缺失。可选类型表示”一个值,它等于 x”或者”根本没有值”。可选类型确保代码在使用值之前始终检查值是否缺失,而非可选值则保证永远不会缺失。

Swift 是一门安全的语言,这意味着它可以帮助你在开发过程中尽早发现和修复多种类型的错误,并让你保证某些类型的错误不会发生。类型安全使你能够清楚地了解代码所处理的值的类型。如果你的部分代码需要一个 String,类型安全可以防止你错误地传递一个 Int。内存安全确保你只处理有效数据,而不是未初始化的内存或已释放的对象,并确保你以安全的方式处理数据——即使在同时运行多段代码的程序中也是如此。Swift 在构建代码时执行大部分安全检查,在某些情况下还会在代码运行时执行额外的检查。

常量和变量

常量和变量将名称(如 maximumNumberOfLoginAttemptswelcomeMessage)与特定类型的值(如数字 10 或字符串 "Hello")关联起来。常量的值一旦设定就不能更改,而变量的值可以在将来设置为不同的值。

声明常量和变量

常量和变量必须在使用前声明。使用 let 关键字声明常量,使用 var 关键字声明变量。下面是一个如何使用常量和变量来跟踪用户登录尝试次数的示例:

let maximumNumberOfLoginAttempts = 10
var currentLoginAttempt = 0

这段代码可以理解为:

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

在这个示例中,允许的最大登录尝试次数被声明为常量,因为最大值永远不会改变。当前登录尝试计数器被声明为变量,因为每次登录失败后该值必须递增。

如果你代码中存储的值不会改变,请始终使用 let 关键字将其声明为常量。只有在存储需要更改的值时才使用变量。

当你声明常量或变量时,可以在声明时给它一个值,如上面的示例所示。或者,你可以稍后在程序中分配其初始值,只要在第一次读取之前保证它有一个值即可。

var environment = "development"
let maximumNumberOfLoginAttempts: Int
// maximumNumberOfLoginAttempts 还没有值。

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

在这个示例中,最大登录尝试次数是常量,其值取决于环境。在开发环境中,它的值是 100;在任何其他环境中,其值是 10。if 语句的两个分支都用某个值初始化 maximumNumberOfLoginAttempts,保证该常量始终获得一个值。有关 Swift 如何在你以这种方式设置初始值时检查代码的信息,请参见常量声明

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

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

类型标注

你可以在声明常量或变量时提供类型标注,以明确常量或变量可以存储的值的类型。通过在常量或变量名称后放置冒号,后跟空格,再后跟要使用的类型名称来编写类型标注。

这个示例为名为 welcomeMessage 的变量提供了类型标注,以指示该变量可以存储 String 值:

var welcomeMessage: String

声明中的冒号表示”……的类型是……”,因此上面的代码可以理解为:

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

“类型为 String”这个短语意味着”可以存储任何 String 值”。可以把它理解为可以存储的”东西的类型”(或”东西的种类”)。

welcomeMessage 变量现在可以设置为任何字符串值而不会出错:

welcomeMessage = "Hello"

你可以在单行上定义多个相同类型的相关变量,用逗号分隔,并在最后一个变量名后添加单个类型标注:

var red, green, blue: Double

注意 在实践中,你很少需要编写类型标注。如果你在定义常量或变量时提供了初始值,Swift 几乎总是可以推断出该常量或变量应该使用的类型,如类型安全和类型推断中所述。在上面的 welcomeMessage 示例中,没有提供初始值,因此 welcomeMessage 变量的类型是通过类型标注指定的,而不是从初始值推断的。

命名常量和变量

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

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

常量和变量名不能包含空白字符、数学符号、箭头、私有使用的 Unicode 标量值或线条和方框绘制字符。也不能以数字开头,尽管数字可以包含在名称的其他位置。

一旦你声明了特定类型的常量或变量,就不能用相同的名称再次声明它,也不能将其更改为存储不同类型的值。也不能将常量更改为变量或将变量更改为常量。

注意 如果你需要给常量或变量一个与 Swift 保留关键字相同的名称,请在使用它作为名称时用反引号(`)将关键字括起来。但是,除非你绝对没有选择,否则请避免使用关键字作为名称。

你可以将现有变量的值更改为另一个兼容类型的值。在这个示例中,friendlyWelcome 的值从 "Hello!" 更改为 "Bonjour!"

var friendlyWelcome = "Hello!"
friendlyWelcome = "Bonjour!"
// friendlyWelcome 现在是 "Bonjour!"

与变量不同,常量的值在设定后不能更改。尝试这样做会在编译代码时报告为错误:

let languageName = "Swift"
languageName = "Swift++"
// 这是编译时错误:languageName 不能被更改。

打印常量和变量

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

print(friendlyWelcome)
// 打印 "Bonjour!"

print(_:separator:terminator:) 函数是一个全局函数,可以将一个或多个值打印到适当的输出。例如,在 Xcode 中,print(_:separator:terminator:) 函数将其输出打印到 Xcode 的”控制台”窗格中。separatorterminator 参数具有默认值,因此你在调用此函数时可以省略它们。默认情况下,该函数通过添加换行符来结束它打印的行。要打印值后不带换行符,请传递空字符串作为终止符——例如,print(someValue, terminator: "")。有关具有默认值的参数的信息,请参见默认参数值

Swift 使用字符串插值将常量或变量的名称作为占位符包含在较长的字符串中,并提示 Swift 将其替换为该常量或变量的当前值。将名称放在括号中,并在开始括号前用反斜杠转义:

print("The current value of friendlyWelcome is \(friendlyWelcome)")
// 打印 "The current value of friendlyWelcome is Bonjour!"

注意 你可以在字符串插值中使用的所有选项都在字符串插值中描述。

注释

使用注释在代码中包含不可执行的文本,作为给自己的笔记或提醒。当你的代码被编译时,Swift 编译器会忽略注释。

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

// 这是一条注释。

多行注释以正斜杠后跟星号(/*)开头,以星号后跟正斜杠(*/)结尾:

/* 这也是一条注释
但写在多行上。 */

与 C 中的多行注释不同,Swift 中的多行注释可以嵌套在其他多行注释中。你可以通过先开始一个多行注释块,然后在第一个块内开始第二个多行注释来编写嵌套注释。然后关闭第二个块,接着关闭第一个块:

/* 这是第一个多行注释的开始。
    /* 这是第二个嵌套的多行注释。 */
这是第一个多行注释的结束。 */

嵌套多行注释使你能够快速轻松地注释掉大块代码,即使代码已经包含多行注释。

分号

与许多其他语言不同,Swift 不要求你在代码中的每条语句后写分号(;),尽管你可以根据需要这样做。但是,如果你想在单行上编写多个单独的语句,则需要分号:

let cat = "🐱"; print(cat)
// 打印 "🐱"

整数

整数是没有小数部分的整数,如 42-23。整数可以是有符号的(正数、零或负数)或无符号的(正数或零),其最大值和最小值取决于其大小(用于存储值的位数)。整数类型在其名称中包含其大小和符号——例如,8 位无符号整数的类型是 UInt8,32 位有符号整数的类型是 Int32。与 Swift 中的所有类型一样,这些整数类型都有大写的名称。在大多数情况下,当你不需要指定确切的整数大小时,可以使用下面描述的 Int 类型。

整数类型的行为与你手动进行的大多数算术一样;整数数学产生的结果没有近似值。这些特性使整数适合计数和其他表示精确数量的计算——例如,找到文本文件中最长的行、在游戏中应用分数乘数或计算收据上的价格总和。

虽然整数没有小数部分,但你可以通过计算小数部分来使用整数表示带小数的数量。例如,你可以通过在计算美分的整数中存储数字 123 来表示 $1.23。这种方法被称为定点数学,因为小数点在数字中的位置是固定的。在上面的示例中,数字 123 被理解为在最后两位数字之前有一个小数点。

注意 对于金融或建筑等受监管领域的计算,或在期望高精度结果的领域中,你可能需要一种特殊用途的数字类型,该类型根据该领域的要求实现舍入和截断等行为。

整数边界

你可以使用每个整数类型的 minmax 属性访问其最小值和最大值:

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,6482,147,483,647 之间的任何值,对于许多整数范围来说已经足够大了。

UInt

Swift 还提供了一个无符号整数类型 UInt,其大小与当前平台的原生字长相同:

  • 在 32 位平台上,UInt 的大小与 UInt32 相同。
  • 在 64 位平台上,UInt 的大小与 UInt64 相同。

注意 仅当你特别需要与平台原生字长大小相同的无符号整数类型时才使用 UInt。如果不是这种情况,Int 是首选,即使已知要存储的值是非负的。对整数值一致使用 Int 有助于代码互操作性,避免在不同数字类型之间转换,并匹配整数类型推断,如类型安全和类型推断中所述。

浮点数

浮点数是有小数部分的数字,如 3.141590.1-273.15。Swift 提供了多种浮点类型,支持不同大小的数字,就像它有不同大小的整数一样。如果你不需要指定确切的大小,请使用 Double。否则,请使用名称中包含所需大小的类型,如 Float16Float80。按照浮点数学的常用术语,Float 使用 32 位,Double 使用 64 位。你也可以将这些类型写成 Float32Float64。例如,图形代码通常使用 Float 以匹配 GPU 最快的数据类型。某些浮点类型仅在某些平台上受支持,但 FloatDouble 在所有平台上都可用。

浮点数让你可以处理非常小和非常大的数字,但不能表示该范围内的每个可能值。与总是产生精确结果的整数计算不同,浮点数学将结果舍入到最接近的可表示数字。例如,当将数字 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 等语言。常量和变量仍然是显式类型化的,但指定其类型的大部分工作都是为你完成的。

当你声明具有初始值的常量或变量时,类型推断特别有用。这通常是通过在声明时为常量或变量分配字面值(或字面量)来完成的。(字面值是直接出现在源代码中的值,如下面示例中的 423.14159。)

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

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

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

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

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

如果你在表达式中组合整数和浮点字面量,将从上下文中推断出 Double 类型:

let anotherPi = 3 + 0.14159
// anotherPi 也被推断为 Double 类型

字面值 3 本身没有显式类型,因此从作为加法一部分的浮点字面量的存在推断出适当的输出类型 Double

数字字面量

整数字面量可以写成:

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

所有这些整数字面量的十进制值都是 17

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

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

数字字面量可以包含额外的格式以使其更易于阅读。整数和浮点数都可以用额外的零填充,并且可以包含下划线以帮助提高可读性。这两种格式都不会影响字面量的底层值:

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

数字类型转换

对于代码中的所有通用整数常量和变量,请使用 Int 类型,即使它们已知是非负的。在日常情况下使用默认整数类型意味着整数常量和变量在你的代码中可以立即互操作,并将匹配整数字面值的推断类型。

仅当手头的任务特别需要时才使用其他整数类型,因为外部来源的数据具有明确的大小,或者出于性能、内存使用或其他必要的优化。在这些情况下使用明确大小的类型有助于捕获任何意外的值溢出,并隐式记录所使用数据的性质。

整数转换

每种数字类型可以存储在整数常量或变量中的数字范围是不同的。Int8 常量或变量可以存储 -128127 之间的数字,而 UInt8 常量或变量可以存储 0255 之间的数字。不适合指定大小整数类型的常量或变量的数字在编译代码时会报告为错误:

let cannotBeNegative: UInt8 = -1
// UInt8 不能存储负数,因此这会报告错误
let tooBig: Int8 = Int8.max + 1
// Int8 不能存储大于其最大值的数字,
// 因此这也会报告错误

因为每种数字类型可以存储不同范围的值,你必须逐个选择进行数字类型转换。这种选择加入的方法可以防止隐藏的转换错误,并帮助在代码中明确类型转换意图。

要将一种特定的数字类型转换为另一种,你用现有值初始化所需类型的新数字。在下面的示例中,常量 twoThousand 的类型是 UInt16,而常量 one 的类型是 UInt8。它们不能直接相加,因为它们不是同一类型。相反,这个示例调用 UInt16(one) 来创建一个用 one 的值初始化的新 UInt16,并使用这个值代替原来的值:

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 提供初始化器的类型。扩展现有类型以提供接受新类型(包括你自己的类型定义)的初始化器在扩展中有介绍。

整数和浮点数转换

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

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

这里,常量 three 的值用于创建 Double 类型的新值,以便加法两边是同一类型。如果没有这个转换,加法将不被允许。

浮点数到整数的转换也必须显式进行。整数类型可以用 DoubleFloat 值初始化:

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

以这种方式用于初始化新整数值时,浮点值总是被截断。这意味着 4.75 变成 4-3.9 变成 -3

注意 组合数字常量和变量的规则与数字字面量的规则不同。字面值 3 可以直接与字面值 0.14159 相加,因为数字字面量本身没有显式类型。它们的类型仅在编译器评估它们时才被推断。

类型别名

类型别名为现有类型定义一个替代名称。你使用 typealias 关键字定义类型别名。

当你想通过更具上下文适当性的名称来引用现有类型时,类型别名非常有用,例如在处理来自外部来源的特定大小的数据时:

typealias AudioSample = UInt16

一旦你定义了类型别名,你就可以在任何可能使用原始名称的地方使用别名:

var maxAmplitudeFound = AudioSample.min
// maxAmplitudeFound 现在是 0

这里,AudioSample 被定义为 UInt16 的别名。因为它是别名,所以对 AudioSample.min 的调用实际上调用的是 UInt16.min,它为 maxAmplitudeFound 变量提供了初始值 0

布尔值

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

let orangesAreOrange = true
let turnipsAreDelicious = false

orangesAreOrangeturnipsAreDelicious 的类型被推断为 Bool,因为它们是用布尔字面值初始化的。与上面的 IntDouble 一样,如果你在创建常量或变量时就将它们设置为 truefalse,则不需要将它们声明为 Bool

当你使用条件语句(如 if 语句)时,布尔值特别有用:

if turnipsAreDelicious {
    print("Mmm, tasty turnips!")
} else {
    print("Eww, turnips are horrible.")
}
// 打印 "Eww, turnips are horrible."

条件语句(如 if 语句)在控制流中有更详细的介绍。

Swift 的类型安全防止非布尔值被替换为 Bool。以下示例会报告编译时错误:

let i = 1
if i {
    // 这个示例将无法编译,并会报告错误
}

但是,下面的替代示例是有效的:

let i = 1
if i == 1 {
    // 这个示例将成功编译
}

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

与 Swift 中类型安全的其他示例一样,这种方法避免了意外错误,并确保特定代码部分的意图始终清晰。

元组

元组将多个值组合成单个复合值。元组中的值可以是任何类型,不必彼此相同。

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

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

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

你可以从任何类型的排列创建元组,它们可以包含任意多的不同类型。没有什么能阻止你创建类型为 (Int, Int, Int)(String, Bool) 的元组,或者你需要的任何其他排列。

你可以将元组的内容分解为单独的常量或变量,然后像往常一样访问它们:

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"

如果你只需要元组的部分值,在分解元组时用下划线(_)忽略元组的某些部分:

let (justTheStatusCode, _) = http404Error
print("The status code is \(justTheStatusCode)")
// 打印 "The status code is 404"

或者,使用从零开始的索引号访问元组中的各个元素值:

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"

你可以在定义元组时命名元组中的各个元素:

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

如果你命名了元组中的元素,可以使用元素名称来访问这些元素的值:

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

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber 的类型是"可选 Int"

因为上面代码中的初始化器可能失败,它返回一个可选 Int,而不是 Int

要编写可选类型,你在可选类型包含的类型名称后写一个问号(?)——例如,可选 Int 的类型是 Int?。可选 Int 始终包含某个 Int 值或根本没有值。它不能包含任何其他内容,如 BoolString 值。

nil

通过为可选变量分配特殊值 nil 来将其设置为无值状态:

var serverResponseCode: Int? = 404
// serverResponseCode 包含一个实际的 Int 值 404
serverResponseCode = nil
// serverResponseCode 现在不包含任何值

如果你定义可选变量而不提供默认值,则该变量会自动设置为 nil

var surveyAnswer: String?
// surveyAnswer 自动设置为 nil

你可以使用 if 语句通过将可选类型与 nil 进行比较来确定可选类型是否包含值。你可以使用”等于”运算符(==)或”不等于”运算符(!=)执行此比较。

如果可选类型有值,则它被认为”不等于” nil

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,而不仅仅是对象类型。

可选绑定

你使用可选绑定来确定可选类型是否包含值,如果是,则使该值作为临时常量或变量可用。可选绑定可以与 ifguardwhile 语句一起使用,以检查可选类型内部的值,并将该值提取到常量或变量中,作为单个操作的一部分。有关 ifguardwhile 语句的更多信息,请参见控制流

if 语句编写可选绑定如下:

if let <#constantName#> = <#someOptional#> {
   <#statements#>
}

你可以重写可选类型部分中的 possibleNumber 示例,使用可选绑定而不是强制解包:

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 语句的第一个分支中可用。它已经用可选类型中包含的值初始化,并具有相应的非可选类型。在这种情况下,possibleNumber 的类型是 Int?,因此 actualNumber 的类型是 Int

如果在访问可选类型包含的值后不需要引用原始的可选常量或变量,你可以为新常量或变量使用相同的名称:

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 指的是原始的可选整数常量。

因为这种代码非常常见,你可以使用更短的写法来解包可选值:只写你要解包的常量或变量的名称。新的解包常量或变量隐式使用与可选值相同的名称。

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 语句是等效的:

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 时使用通用问候语。

let name: String? = nil
let greeting = "Hello, " + (name ?? "friend") + "!"
print(greeting)
// 打印 "Hello, friend!"

有关使用 ?? 提供后备值的更多信息,请参见空合并运算符

强制解包

nil 表示不可恢复的失败(如程序员错误或损坏的状态)时,你可以通过在可选类型名称末尾添加感叹号(!)来访问底层值。这被称为强制解包可选类型的值。当你强制解包非 nil 值时,结果是其解包后的值。强制解包 nil 值会触发运行时错误。

! 实际上是 fatalError(_:file:line:) 的较短写法。例如,下面的代码显示了两种等效的方法:

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 访问时的行为差异:

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 没有显式类型,所以它是普通的可选类型。

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

如果隐式解包可选类型是 nil 并且你尝试访问其包装值,你将触发运行时错误。结果与你在不包含值的普通可选类型后写感叹号来强制解包完全相同。

你可以像检查普通可选类型一样检查隐式解包可选类型是否为 nil

if assumedString != nil {
    print(assumedString!)
}
// 打印 "An implicitly unwrapped optional string."

你也可以将隐式解包可选类型与可选绑定一起使用,在单个语句中检查并解包其值:

if let definiteString = assumedString {
    print(definiteString)
}
// 打印 "An implicitly unwrapped optional string."

内存安全

除了上面类型安全和类型推断中描述的防止类型不匹配的检查外,Swift 还保护代码免受处理无效内存的影响。这种保护被称为内存安全,包括以下要求:

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

如果你使用过不提供这些保证的语言,你可能熟悉上面列表中提到的一些错误和 bug。如果你没有遇到过这些问题,那也没关系;Swift 中的安全代码可以避免这些问题。有关 Swift 如何确保你设置初始值的信息,请参见初始化,有关 Swift 如何在并发代码中检查内存安全的信息,请参见并发,有关 Swift 如何检查重叠内存访问的信息,请参见内存安全

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

Swift 中的安全代码仍然可能遇到错误和意外失败,这可能会停止程序执行。安全并不能确保你的代码运行完成。Swift 提供了几种指示和恢复错误的方法,在下面的错误处理断言和先决条件中讨论。但是,在某些情况下,处理错误的唯一安全方法是停止执行。如果你需要保证服务永远不会意外停止,请在其整体架构中加入容错功能,以便它可以从其任何组件的意外停止中恢复。

错误处理

你使用错误处理来响应程序在执行过程中可能遇到的错误条件。

与可选类型(可以使用值的存在或缺失来传达函数的成功或失败)不同,错误处理允许你确定失败的根本原因,并在必要时将错误传播到程序的另一部分。

当函数遇到错误条件时,它抛出一个错误。然后,该函数的调用者可以捕获错误并做出适当的响应。

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

函数通过在其声明中包含 throws 关键字来表明它可以抛出错误。当你调用可以抛出错误的函数时,你需要在表达式前加上 try 关键字。

Swift 会自动将错误传播出其当前作用域,直到它们被 catch 子句处理。

do {
    try canThrowAnError()
    // 没有抛出错误
} catch {
    // 抛出了错误
}

do 语句创建一个新的包含作用域,允许将错误传播到一个或多个 catch 子句。

这是一个如何使用错误处理来响应不同错误条件的示例:

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:) 函数来编写断言。你向此函数传递一个计算为 truefalse 的表达式以及一条在条件结果为 false 时显示的消息。例如:

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,断言失败,终止应用程序。

你可以省略断言消息——例如,当它只是将条件重复为散文时。

assert(age >= 0)

如果代码已经检查了条件,你可以使用 assertionFailure(_:file:line:) 函数来指示断言已失败。例如:

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:) 函数来编写先决条件。你向此函数传递一个计算为 truefalse 的表达式以及一条在条件结果为 false 时显示的消息。例如:

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

你也可以调用 preconditionFailure(_:file:line:) 函数来指示发生了失败——例如,如果采用了 switch 的默认情况,但所有有效的输入数据应该已被 switch 的其他情况之一处理。

注意 如果你在未检查模式(-Ounchecked)下编译,则不会检查先决条件。编译器假定先决条件始终为真,并相应地优化你的代码。但是,无论优化设置如何,fatalError(_:file:line:) 函数始终会停止执行。

你可以在原型设计和早期开发期间使用 fatalError(_:file:line:) 函数为尚未实现的功能创建存根,方法是编写 fatalError("Unimplemented") 作为存根实现。因为致命错误永远不会被优化掉,与断言或先决条件不同,你可以确信如果遇到存根实现,执行将始终停止。