变量和常量
在Swift中,有两种方法可以存储数据:变量和常量。变量是可以随时更改其值的数据存储,而常量是可以设置一次且永不更改的数据存储。因此,变量的值可以变化,常量的值是恒定的。
在Swift中,您可以使用var
关键字创建变量,您可以随时更改它,如下所示:
var name = "Tim McGraw"
name = "Romeo"
使用let
关键字创建常量,不可更改,如下所示:
let name = "Tim McGraw"
name = "Romeo"
//对常量进行更改时Xcode会报错:"you're trying to change a constant and you can't do that."
Swift开发人员非常倾向于尽可能使用常量,因为它使您的代码更易于理解。事实上,在最新版本的Swift中,Xcode实际上会告诉你,如果你做了一个变量,那么永远不要改变它!
重要说明:
1.变量和常量名称在代码中必须唯一。如果您尝试两次使用相同的变量名,则会出现错误。
2.如果知道值永远不会改变,则可以使用let
优化以使代码运行更快。
数据类型
字符串(String
)
整数(Int
)
浮点数(Float
)
双精度数(Double
)
布尔型(Bool
)
//字符串
var name: String
name = "Tim McGraw"
//整数
var age: Int
age = 25
//浮点数(32位浮点数)
var longitude: Float
longitude = -86.783333
//双精度数(64位浮点数)
var latitude: Double
latitude = 36.166667
//布尔型
var stayOutTooLate: Bool
stayOutTooLate = true
类型安全和类型推断
Swift 是一个类型安全(type safe)的语言。类型安全的语言可以让你清楚地知道代码要处理的值的类型。如果你的代码需要一个String,你绝对不可能不小心传进去一个Int。
由于 Swift 是类型安全的,所以它会在编译你的代码时进行类型检查(type checks),并把不匹配的类型标记为错误。这可以让你在开发的时候尽早发现并修复错误。
当你要处理不同类型的值时,类型检查可以帮你避免错误。然而,这并不是说你每次声明常量和变量的时候都需要显式指定类型。如果你没有显式指定类型,Swift 会使用类型推断(type inference)来选择合适的类型。有了类型推断,编译器可以在编译代码的时候自动推断出表达式的类型。原理很简单,只要检查你赋的值即可。
因为有类型推断,和 C 或者 Objective-C 比起来 Swift 很少需要声明类型。常量和变量虽然需要明确类型,但是大部分工作并不需要你自己来完成。
当你声明常量或者变量并赋初值的时候类型推断非常有用。当你在声明常量或者变量的时候赋给它们一个字面量(literal value 或 literal)即可触发类型推断。(字面量就是会直接出现在你代码中的值,比如 42 和 3.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
类型。
运算符
运算符分为一元、二元和三元运算符:
- 一元运算符对单一操作对象操作(如
-a
)。一元运算符分前置运算符和后置运算符,前置运算符需紧跟在操作对象之前(如!b
),后置运算符需紧跟在操作对象之后(如c!
)。 - 二元运算符操作两个操作对象(如
2 + 3
),是中置的,因为它们出现在两个操作对象之间。 - 三元运算符操作三个操作对象,和 C 语言一样,Swift 只有一个三元运算符,就是三目运算符(
a ? b : c
)。
赋值运算符
赋值运算符(a = b
),表示用 b
的值来初始化或更新 a
的值:
let b = 10
var a = 5
a = b
// a 现在等于 10
如果赋值的右边是一个多元组,它的元素可以马上被分解成多个常量或变量:
let (x, y) = (1, 2)
// 现在 x 等于 1,y 等于 2
与 C 语言和 Objective-C 不同,Swift 的赋值操作并不返回任何值。所以以下代码是错误的:
if x = y {
// 此句错误, 因为 x = y 并不返回任何值
}
这个特性使你无法把(==
)错写成(=
),由于 if x = y
是错误代码,Swift 能帮你避免此类错误发生。
算术运算符
Swift 中所有数值类型都支持了基本的四则算术运算符:
- 加法(
+
) - 减法(
-
) - 乘法(
*
) - 除法(
/
)
1 + 2 // 等于 3
5 - 3 // 等于 2
2 * 3 // 等于 6
10.0 / 2.5 // 等于 4.0
与 C 语言和 Objective-C 不同的是,Swift 默认情况下不允许在数值运算中出现溢出情况。但是你可以使用 Swift 的溢出运算符来实现溢出运算(如 a &+ b
)。
加法运算符也可用于 String 的拼接:
"hello, " + "world" // 等于 "hello, world"
求余运算符(a % b
)是计算 b
的多少倍刚刚好可以容入a
,返回多出来的那部分(余数)。
在 Swift 中可以表达为:
9 % 4 // 等于 1
为了得到 a % b
的结果,%
计算了以下等式,并输出余数
作为结果:
a = (b × 倍数) + 余数
当倍数
取最大值的时候,就会刚好可以容入 a
中。
注意:
求余运算符(%
)在其他语言也叫取模运算符。但是严格说来,我们看该运算符对负数的操作结果,「求余」比「取模」更合适些。
在对负数 b
求余时,b
的符号会被忽略。这意味着 a % b
和 a % -b
的结果是相同的。
一元负号符(-
)写在操作数之前,中间没有空格。数值的正负号可以使用前缀 -
(即一元负号符)来切换:
let three = 3
let minusThree = -three // minusThree 等于 -3
let plusThree = -minusThree // plusThree 等于 3, 或 "负负3"
一元正号符(+
)不做任何改变地返回操作数的值:
let minusSix = -6
let alsoMinusSix = +minusSix // alsoMinusSix 等于 -6
虽然一元正号符什么都不会改变,但当你在使用一元负号来表达负数时,你可以使用一元正号来表达正数,如此你的代码会具有对称美。
组合赋值运算符
如同 C 语言,Swift 也提供把其他运算符和赋值运算(=
)组合的组合赋值运算符,组合加运算(+=
)是其中一个例子:
var a = 1
a += 2
// a 现在是 3
表达式 a += 2
是 a = a + 2
的简写,一个组合加运算就是把加法运算和赋值运算组合成进一个运算符里,同时完成两个运算任务。
注意:
复合赋值运算没有返回值,let b = a += 2
这类代码是错误。这不同于上面提到的自增和自减运算符。
比较运算符(Comparison Operators)
所有标准 C 语言中的比较运算符都可以在 Swift 中使用:
- 等于(
a == b
) - 不等于(
a != b
) - 大于(
a > b
) - 小于(
a < b
) - 大于等于(
a >= b
) - 小于等于(
a <= b
)
注意: Swift 也提供恒等(
===
)和不恒等(!==
)这两个比较符来判断两个对象是否引用同一个对象实例。
每个比较运算都返回了一个标识表达式是否成立的布尔值:
1 == 1 // true, 因为 1 等于 1
2 != 1 // true, 因为 2 不等于 1
2 > 1 // true, 因为 2 大于 1
1 < 2 // true, 因为 1 小于2
1 >= 1 // true, 因为 1 大于等于 1
2 <= 1 // false, 因为 2 并不小于等于 1
比较运算多用于条件语句,如if
条件:
let name = "world"
if name == "world" {
print("hello, world")
} else {
print("I'm sorry \(name), but I don't recognize you")
}
// 输出 "hello, world", 因为 `name` 就是等于 "world"
如果两个元组的元素相同,且长度相同的话,元组就可以被比较。比较元组大小会按照从左到右、逐值比较的方式,直到发现有两个值不等时停止。如果所有的值都相等,那么这一对元组我们就称它们是相等的。例如:
(1, "zebra") < (2, "apple") // true,因为 1 小于 2
(3, "apple") < (3, "bird") // true,因为 3 等于 3,但是 apple 小于 bird
(4, "dog") == (4, "dog") // true,因为 4 等于 4,dog 等于 dog
在上面的例子中,你可以看到,在第一行中从左到右的比较行为。因为1
小于2
,所以(1, "zebra")
小于(2, "apple")
,不管元组剩下的值如何。所以"zebra"
大于"apple"
对结果没有任何影响,因为元组的比较结果已经被第一个元素决定了。不过,当元组的第一个元素相同时候,第二个元素将会用作比较-第二行和第三行代码就发生了这样的比较。
当元组中的元素都可以被比较时,你也可以使用这些运算符来比较它们的大小。例如,像下面展示的代码,你可以比较两个类型为 (String, Int)
的元组,因为 Int
和 String
类型的值可以比较。相反,Bool
不能被比较,也意味着存有布尔类型的元组不能被比较。
("blue", -1) < ("purple", 1) // 正常,比较的结果为 true
("blue", false) < ("purple", true) // 错误,因为 < 不能比较布尔类型
注意:
Swift 标准库只能比较七个以内元素的元组比较函数。如果你的元组元素超过七个时,你需要自己实现比较运算符。
三目运算符(Ternary Conditional Operator)
三目运算符的特殊在于它是有三个操作数的运算符,它的形式是 问题 ? 答案 1 : 答案 2
。它简洁地表达根据 问题
成立与否作出二选一的操作。如果 问题
成立,返回 答案 1
的结果;反之返回 答案 2
的结果。
三目运算符是以下代码的缩写形式:
if question {
answer1
} else {
answer2
}
这里有个计算表格行高的例子。如果有表头,那行高应比内容高度要高出 50 点;如果没有表头,只需高出 20 点:
let contentHeight = 40
let hasHeader = true
let rowHeight = contentHeight + (hasHeader ? 50 : 20)
// rowHeight 现在是 90
上面的写法比下面的代码更简洁:
let contentHeight = 40
let hasHeader = true
var rowHeight = contentHeight
if hasHeader {
rowHeight = rowHeight + 50
} else {
rowHeight = rowHeight + 20
}
// rowHeight 现在是 90
第一段代码例子使用了三目运算,所以一行代码就能让我们得到正确答案。这比第二段代码简洁得多,无需将 rowHeight
定义成变量,因为它的值无需在 if
语句中改变。
三目运算提供有效率且便捷的方式来表达二选一的选择。需要注意的事,过度使用三目运算符会使简洁的代码变的难懂。我们应避免在一个组合语句中使用多个三目运算符。
空合运算符(Nil Coalescing Operator)
空合运算符(a ?? b
)将对可选类型 a
进行空判断,如果 a
包含一个值就进行解封,否则就返回一个默认值 b
。表达式 a
必须是 Optional 类型。默认值 b
的类型必须要和 a
存储值的类型保持一致。
空合运算符是对以下代码的简短表达方法:
a != nil ? a! : b
上述代码使用了三目运算符。当可选类型 a
的值不为空时,进行强制解封(a!
),访问 a
中的值;反之返回默认值 b
。无疑空合运算符(??
)提供了一种更为优雅的方式去封装条件判断和解封两种行为,显得简洁以及更具可读性。
注意: 如果
a
为非空值(non-nil
),那么值b
将不会被计算。这也就是所谓的短路求值。
下文例子采用空合运算符,实现了在默认颜色名和可选自定义颜色名之间抉择:
let defaultColorName = "red"
var userDefinedColorName: String? //默认值为 nil
var colorNameToUse = userDefinedColorName ?? defaultColorName
// userDefinedColorName 的值为空,所以 colorNameToUse 的值为 "red"
userDefinedColorName
变量被定义为一个可选的 String
类型,默认值为 nil
。由于 userDefinedColorName
是一个可选类型,我们可以使用空合运算符去判断其值。在上一个例子中,通过空合运算符为一个名为 colorNameToUse
的变量赋予一个字符串类型初始值。 由于 userDefinedColorName
值为空,因此表达式 userDefinedColorName ?? defaultColorName
返回 defaultColorName
的值,即 red
。
另一种情况,分配一个非空值(non-nil
)给 userDefinedColorName
,再次执行空合运算,运算结果为封包在 userDefaultColorName
中的值,而非默认值。
userDefinedColorName = "green"
colorNameToUse = userDefinedColorName ?? defaultColorName
// userDefinedColorName 非空,因此 colorNameToUse 的值为 "green"
区间运算符(Range Operators)
Swift 提供了几种方便表达一个区间的值的区间运算符。
闭区间运算符(a...b
)定义一个包含从 a
到 b
(包括 a
和 b
)的所有值的区间。a
的值不能超过 b
。 闭区间运算符在迭代一个区间的所有值时是非常有用的,如在 for-in
循环中:
for index in 1...5 {
print("\(index) * 5 = \(index * 5)")
}
// 1 * 5 = 5
// 2 * 5 = 10
// 3 * 5 = 15
// 4 * 5 = 20
// 5 * 5 = 25
半开区间运算符(a..<b
)定义一个从 a
到 b
但不包括 b
的区间。 之所以称为半开区间,是因为该区间包含第一个值而不包括最后的值。
半开区间的实用性在于当你使用一个从 0 开始的列表(如数组)时,非常方便地从0数到列表的长度。
let names = ["Anna", "Alex", "Brian", "Jack"]
let count = names.count
for i in 0..<count {
print("第 \(i + 1) 个人叫 \(names[i])")
}
// 第 1 个人叫 Anna
// 第 2 个人叫 Alex
// 第 3 个人叫 Brian
// 第 4 个人叫 Jack
数组有 4 个元素,但 0..<count
只数到3(最后一个元素的下标),因为它是半开区间。
闭区间操作符有另一个表达形式,可以表达往一侧无限延伸的区间 —— 例如,一个包含了数组从索引 2 到结尾的所有值的区间。在这些情况下,你可以省略掉区间操作符一侧的值。这种区间叫做单侧区间,因为操作符只有一侧有值。例如:
for name in names[2...] {
print(name)
}
// Brian
// Jack
for name in names[...2] {
print(name)
}
// Anna
// Alex
// Brian
半开区间操作符也有单侧表达形式,附带上它的最终值。就像你使用区间去包含一个值,最终值并不会落在区间内。例如:
for name in names[..<2] {
print(name)
}
// Anna
// Alex
单侧区间不止可以在下标里使用,也可以在别的情境下使用。你不能遍历省略了初始值的单侧区间,因为遍历的开端并不明显。你可以遍历一个省略最终值的单侧区间;然而,由于这种区间无限延伸的特性,请保证你在循环里有一个结束循环的分支。你也可以查看一个单侧区间是否包含某个特定的值,就像下面展示的那样。
let range = ...5
range.contains(7) // false
range.contains(4) // true
range.contains(-1) // true
逻辑运算符(Logical Operators)
逻辑运算符的操作对象是逻辑布尔值。Swift 支持基于 C 语言的三个标准逻辑运算。
- 逻辑非(
!a
) - 逻辑与(
a && b
) - 逻辑或(
a || b
)
逻辑非运算符(!a
)对一个布尔值取反,使得 true
变 false
,false
变 true
。
它是一个前置运算符,需紧跟在操作数之前,且不加空格。读作 非 a
,例子如下:
let allowedEntry = false
if !allowedEntry {
print("ACCESS DENIED")
}
// 输出 "ACCESS DENIED"
if !allowedEntry
语句可以读作「如果非 allowedEntry」,接下一行代码只有在「非 allowedEntry」为 true
,即 allowEntry
为 false
时被执行。
在示例代码中,小心地选择布尔常量或变量有助于代码的可读性,并且避免使用双重逻辑非运算,或混乱的逻辑语句。
逻辑与运算符(a && b
)表达了只有 a
和 b
的值都为 true
时,整个表达式的值才会是 true
。
只要任意一个值为 false
,整个表达式的值就为 false
。事实上,如果第一个值为 false
,那么是不去计算第二个值的,因为它已经不可能影响整个表达式的结果了。这被称做短路计算(short-circuit evaluation)。
以下例子,只有两个 Bool
值都为 true
的时候才允许进入 if:
let enteredDoorCode = true
let passedRetinaScan = false
if enteredDoorCode && passedRetinaScan {
print("Welcome!")
} else {
print("ACCESS DENIED")
}
// 输出 "ACCESS DENIED"
逻辑或运算符(a || b
)是一个由两个连续的 |
组成的中置运算符。它表示了两个逻辑表达式的其中一个为 true
,整个表达式就为 true
。
同逻辑与运算符类似,逻辑或也是「短路计算」的,当左端的表达式为 true
时,将不计算右边的表达式了,因为它不可能改变整个表达式的值了。
以下示例代码中,第一个布尔值(hasDoorKey
)为 false
,但第二个值(knowsOverridePassword
)为 true
,所以整个表达是 true
,于是允许进入:
let hasDoorKey = false
let knowsOverridePassword = true
if hasDoorKey || knowsOverridePassword {
print("Welcome!")
} else {
print("ACCESS DENIED")
}
// 输出 "Welcome!"
我们可以组合多个逻辑运算符来表达一个复合逻辑:
if enteredDoorCode && passedRetinaScan || hasDoorKey || knowsOverridePassword {
print("Welcome!")
} else {
print("ACCESS DENIED")
}
// 输出 "Welcome!"
这个例子使用了含多个 &&
和 ||
的复合逻辑。但无论怎样,&&
和 ||
始终只能操作两个值。所以这实际是三个简单逻辑连续操作的结果。我们来解读一下:
如果我们输入了正确的密码并通过了视网膜扫描,或者我们有一把有效的钥匙,又或者我们知道紧急情况下重置的密码,我们就能把门打开进入。
前两种情况,我们都不满足,所以前两个简单逻辑的结果是 false
,但是我们是知道紧急情况下重置的密码的,所以整个复杂表达式的值还是 true
。
注意: Swift 逻辑操作符
&&
和||
是左结合的,这意味着拥有多元逻辑操作符的复合表达式优先计算最左边的子表达式。
为了一个复杂表达式更容易读懂,在合适的地方使用括号来明确优先级是很有效的,虽然它并非必要的。在上个关于门的权限的例子中,我们给第一个部分加个括号,使它看起来逻辑更明确:
if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowsOverridePassword {
print("Welcome!")
} else {
print("ACCESS DENIED")
}
// 输出 "Welcome!"
这括号使得前两个值被看成整个逻辑表达中独立的一个部分。虽然有括号和没括号的输出结果是一样的,但对于读代码的人来说有括号的代码更清晰。可读性比简洁性更重要,请在可以让你代码变清晰的地方加个括号吧!
字符串插值
字符串插值是一种构建新字符串的方式,可以在其中包含常量、变量、字面量和表达式。字符串字面量和多行字符串字面量都可以使用字符串插值。 您插入的字符串字面量的每一项都在以反斜线为前缀的圆括号中:
let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message 是 "3 times 2.5 is 7.5"
在上面的例子中,multiplier
作为\(multiplier)
被插入到一个字符串常量量中。 当创建字符串执行插值计算时此占位符会被替换为multiplier
实际的值。
multiplier
的值也作为字符串中后面表达式的一部分。 该表达式计算Double(multiplier) * 2.5
的值并将结果 (7.5
) 插入到字符串中。 在这个例子中,表达式写为\(Double(multiplier) * 2.5)
并包含在字符串字面量中。
注意:
插值字符串中写在括号中的表达式不能包含非转义反斜杠 (\
),并且不能包含回车或换行符。不过,插值字符串可以包含其他字面量。
数组(Arrays)
数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。
注意: Swift 的
Array
类型被桥接到Foundation
中的NSArray
类。
数组的简单语法
写 Swift 数组应该遵循像Array<Element>
这样的形式,其中Element
是这个数组中唯一允许存在的数据类型。我们也可以使用像[Element]
这样的简单语法。尽管两种形式在功能上是一样的,但是推荐较短的那种,而且在本文中都会使用这种形式来使用数组。
创建一个空数组
我们可以使用构造语法来创建一个由特定数据类型构成的空数组:
var someInts = [Int]()
print("someInts is of type [Int] with \(someInts.count) items.")
// 打印 "someInts is of type [Int] with 0 items."
注意,通过构造函数的类型,someInts
的值类型被推断为[Int]
。
或者,如果代码上下文中已经提供了类型信息,例如一个函数参数或者一个已经定义好类型的常量或者变量,我们可以使用空数组语句创建一个空数组,它的写法很简单:[]
(一对空方括号):
someInts.append(3)
// someInts 现在包含一个 Int 值
someInts = []
// someInts 现在是空数组,但是仍然是 [Int] 类型的。
创建一个带有默认值的数组
Swift 中的Array
类型还提供一个可以创建特定大小并且所有数据都被默认的构造方法。我们可以把准备加入新数组的数据项数量(count
)和适当类型的初始值(repeating
)传入数组构造函数:
var threeDoubles = Array(repeating: 0.0, count: 3)
// threeDoubles 是一种 [Double] 数组,等价于 [0.0, 0.0, 0.0]
通过两个数组相加创建一个数组
我们可以使用加法操作符(+
)来组合两种已存在的相同类型数组。新数组的数据类型会被从两个数组的数据类型中推断出来:
var anotherThreeDoubles = Array(repeating: 2.5, count: 3)
// anotherThreeDoubles 被推断为 [Double],等价于 [2.5, 2.5, 2.5]
var sixDoubles = threeDoubles + anotherThreeDoubles
// sixDoubles 被推断为 [Double],等价于 [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]
用数组字面量构造数组
我们可以使用数组字面量来进行数组构造,这是一种用一个或者多个数值构造数组的简单方法。数组字面量是一系列由逗号分割并由方括号包含的数值:
[value 1, value 2, value 3]
。
下面这个例子创建了一个叫做shoppingList
并且存储String
的数组:
var shoppingList: [String] = ["Eggs", "Milk"]
// shoppingList 已经被构造并且拥有两个初始项。
shoppingList
变量被声明为“字符串值类型的数组“,记作[String]
。 因为这个数组被规定只有String
一种数据结构,所以只有String
类型可以在其中被存取。 在这里,shoppingList
数组由两个String
值("Eggs"
和"Milk"
)构造,并且由数组字面量定义。
注意:
shoppingList
数组被声明为变量(var
关键字创建)而不是常量(let
创建)是因为以后可能会有更多的数据项被插入其中。
在这个例子中,字面量仅仅包含两个String
值。匹配了该数组的变量声明(只能包含String
的数组),所以这个字面量的分配过程可以作为用两个初始项来构造shoppingList
的一种方式。
由于 Swift 的类型推断机制,当我们用字面量构造只拥有相同类型值数组的时候,我们不必把数组的类型定义清楚。 shoppingList
的构造也可以这样写:
var shoppingList = ["Eggs", "Milk"]
因为所有数组字面量中的值都是相同的类型,Swift 可以推断出[String]
是shoppingList
中变量的正确类型。
访问和修改数组
我们可以通过数组的方法和属性来访问和修改数组,或者使用下标语法。
可以使用数组的只读属性count
来获取数组中的数据项数量:
print("The shopping list contains \(shoppingList.count) items.")
// 输出 "The shopping list contains 2 items."(这个数组有2个项)
使用布尔属性isEmpty
作为一个缩写形式去检查count
属性是否为0
:
if shoppingList.isEmpty {
print("The shopping list is empty.")
} else {
print("The shopping list is not empty.")
}
// 打印 "The shopping list is not empty."(shoppinglist 不是空的)
也可以使用append(_:)
方法在数组后面添加新的数据项:
shoppingList.append("Flour")
// shoppingList 现在有3个数据项,有人在摊煎饼
除此之外,使用加法赋值运算符(+=
)也可以直接在数组后面添加一个或多个拥有相同类型的数据项:
shoppingList += ["Baking Powder"]
// shoppingList 现在有四项了
shoppingList += ["Chocolate Spread", "Cheese", "Butter"]
// shoppingList 现在有七项了
可以直接使用下标语法来获取数组中的数据项,把我们需要的数据项的索引值放在直接放在数组名称的方括号中:
var firstItem = shoppingList[0]
// 第一项是 "Eggs"
注意:
第一项在数组中的索引值是0
而不是1
。 Swift 中的数组索引总是从零开始。
我们也可以用下标来改变某个已有索引值对应的数据值:
shoppingList[0] = "Six eggs"
// 其中的第一项现在是 "Six eggs" 而不是 "Eggs"
还可以利用下标来一次改变一系列数据值,即使新数据和原有数据的数量是不一样的。下面的例子把"Chocolate Spread"
,"Cheese"
,和"Butter"
替换为"Bananas"
和 "Apples"
:
shoppingList[4...6] = ["Bananas", "Apples"]
// shoppingList 现在有6项
注意:
不可以用下标访问的形式去在数组尾部添加新项。
调用数组的insert(_:at:)
方法来在某个具体索引值之前添加数据项:
shoppingList.insert("Maple Syrup", at: 0)
// shoppingList 现在有7项
// "Maple Syrup" 现在是这个列表中的第一项
这次insert(_:at:)
方法调用把值为"Maple Syrup"
的新数据项插入列表的最开始位置,并且使用0
作为索引值。
类似的我们可以使用remove(at:)
方法来移除数组中的某一项。这个方法把数组在特定索引值中存储的数据项移除并且返回这个被移除的数据项(我们不需要的时候就可以无视它):
let mapleSyrup = shoppingList.remove(at: 0)
// 索引值为0的数据项被移除
// shoppingList 现在只有6项,而且不包括 Maple Syrup
// mapleSyrup 常量的值等于被移除数据项的值 "Maple Syrup"
注意:
如果我们试着对索引越界的数据进行检索或者设置新值的操作,会引发一个运行期错误。我们可以使用索引值和数组的count
属性进行比较来在使用某个索引之前先检验是否有效。除了当count
等于 0 时(说明这是个空数组),最大索引值一直是count - 1
,因为数组都是零起索引。
数据项被移除后数组中的空出项会被自动填补,所以现在索引值为0
的数据项的值再次等于"Six eggs"
:
firstItem = shoppingList[0]
// firstItem 现在等于 "Six eggs"
如果我们只想把数组中的最后一项移除,可以使用removeLast()
方法而不是remove(at:)
方法来避免我们需要获取数组的count
属性。就像后者一样,前者也会返回被移除的数据项:
let apples = shoppingList.removeLast()
// 数组的最后一项被移除了
// shoppingList 现在只有5项,不包括 Apples
// apples 常量的值现在等于 "Apples" 字符串
数组的遍历
我们可以使用for-in
循环来遍历所有数组中的数据项:
for item in shoppingList {
print(item)
}
// Six eggs
// Milk
// Flour
// Baking Powder
// Bananas
如果我们同时需要每个数据项的值和索引值,可以使用enumerated()
方法来进行数组遍历。enumerated()
返回一个由每一个数据项索引值和数据值组成的元组。我们可以把这个元组分解成临时常量或者变量来进行遍历:
for (index, value) in shoppingList. enumerated() {
print("Item \(String(index + 1)): \(value)")
}
// Item 1: Six eggs
// Item 2: Milk
// Item 3: Flour
// Item 4: Baking Powder
// Item 5: Bananas
字典
字典是一种存储多个相同类型的值的容器。每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。和数组中的数据项不同,字典中的数据项并没有具体顺序。我们在需要通过标识符(键)访问数据的时候使用字典,这种方法很大程度上和我们在现实世界中使用字典查字义的方法一样。
注意:
Swift 的Dictionary
类型被桥接到Foundation
的NSDictionary
类。
字典类型简化语法
Swift 的字典使用Dictionary<Key, Value>
定义,其中Key
是字典中键的数据类型,Value
是字典中对应于这些键所存储值的数据类型。
注意:
一个字典的Key
类型必须遵循Hashable
协议,就像Set
的值类型。
我们也可以用[Key: Value]
这样简化的形式去创建一个字典类型。虽然这两种形式功能上相同,但是后者是首选,并且这本指导书涉及到字典类型时通篇采用后者。
创建一个空字典
我们可以像数组一样使用构造语法创建一个拥有确定类型的空字典:
var namesOfIntegers = [Int: String]()
// namesOfIntegers 是一个空的 [Int: String] 字典
这个例子创建了一个[Int: String]
类型的空字典来储存整数的英语命名。它的键是Int
型,值是String
型。
如果上下文已经提供了类型信息,我们可以使用空字典字面量来创建一个空字典,记作[:]
(中括号中放一个冒号):
namesOfIntegers[16] = "sixteen"
// namesOfIntegers 现在包含一个键值对
namesOfIntegers = [:]
// namesOfIntegers 又成为了一个 [Int: String] 类型的空字典
用字典字面量创建字典
我们可以使用字典字面量来构造字典,这和我们刚才介绍过的数组字面量拥有相似语法。字典字面量是一种将一个或多个键值对写作Dictionary
集合的快捷途径。
一个键值对是一个key
和一个value
的结合体。在字典字面量中,每一个键值对的键和值都由冒号分割。这些键值对构成一个列表,其中这些键值对由方括号包含、由逗号分割:
[key 1: value 1, key 2: value 2, key 3: value 3]
下面的例子创建了一个存储国际机场名称的字典。在这个字典中键是三个字母的国际航空运输相关代码,值是机场名称:
var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
airports
字典被声明为一种[String: String]
类型,这意味着这个字典的键和值都是String
类型。
注意:
airports
字典被声明为变量(用var
关键字)而不是常量(let
关键字)因为后来更多的机场信息会被添加到这个示例字典中。
airports
字典使用字典字面量初始化,包含两个键值对。第一对的键是YYZ
,值是Toronto Pearson
。第二对的键是DUB
,值是Dublin
。
这个字典语句包含了两个String: String
类型的键值对。它们对应airports
变量声明的类型(一个只有String
键和String
值的字典)所以这个字典字面量的任务是构造拥有两个初始数据项的airport
字典。
和数组一样,我们在用字典字面量构造字典时,如果它的键和值都有各自一致的类型,那么就不必写出字典的类型。 airports
字典也可以用这种简短方式定义:
var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
因为这个语句中所有的键和值都各自拥有相同的数据类型,Swift 可以推断出Dictionary<String, String>
是airports
字典的正确类型。
访问和修改字典
我们可以通过字典的方法和属性来访问和修改字典,或者通过使用下标语法。
和数组一样,我们可以通过字典的只读属性count
来获取某个字典的数据项数量:
print("The dictionary of airports contains \(airports.count) items.")
// 打印 "The dictionary of airports contains 2 items."(这个字典有两个数据项)
使用布尔属性isEmpty
作为一个缩写形式去检查count
属性是否为0
:
if airports.isEmpty {
print("The airports dictionary is empty.")
} else {
print("The airports dictionary is not empty.")
}
// 打印 "The airports dictionary is not empty."
我们也可以在字典中使用下标语法来添加新的数据项。可以使用一个恰当类型的键作为下标索引,并且分配恰当类型的新值:
airports["LHR"] = "London"
// airports 字典现在有三个数据项
我们也可以使用下标语法来改变特定键对应的值:
airports["LHR"] = "London Heathrow"
// "LHR"对应的值 被改为 "London Heathrow
作为另一种下标方法,字典的updateValue(_:forKey:)
方法可以设置或者更新特定键对应的值。就像上面所示的下标示例,updateValue(_:forKey:)
方法在这个键不存在对应值的时候会设置新值或者在存在时更新已存在的值。和上面的下标方法不同的,updateValue(_:forKey:)
这个方法返回更新值之前的原值。这样使得我们可以检查更新是否成功。
updateValue(_:forKey:)
方法会返回对应值的类型的可选值。举例来说:对于存储String
值的字典,这个函数会返回一个String?
或者“可选 String
”类型的值。
如果有值存在于更新前,则这个可选值包含了旧值,否则它将会是nil
。
if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") {
print("The old value for DUB was \(oldValue).")
}
// 输出 "The old value for DUB was Dublin."
我们也可以使用下标语法来在字典中检索特定键对应的值。因为有可能请求的键没有对应的值存在,字典的下标访问会返回对应值的类型的可选值。如果这个字典包含请求键所对应的值,下标会返回一个包含这个存在值的可选值,否则将返回nil
:
if let airportName = airports["DUB"] {
print("The name of the airport is \(airportName).")
} else {
print("That airport is not in the airports dictionary.")
}
// 打印 "The name of the airport is Dublin Airport."
我们还可以使用下标语法来通过给某个键的对应值赋值为nil
来从字典里移除一个键值对:
airports["APL"] = "Apple Internation"
// "Apple Internation" 不是真的 APL 机场, 删除它
airports["APL"] = nil
// APL 现在被移除了
此外,removeValue(forKey:)
方法也可以用来在字典中移除键值对。这个方法在键值对存在的情况下会移除该键值对并且返回被移除的值或者在没有值的情况下返回nil
:
if let removedValue = airports. removeValue(forKey: "DUB") {
print("The removed airport's name is \(removedValue).")
} else {
print("The airports dictionary does not contain a value for DUB.")
}
// prints "The removed airport's name is Dublin Airport."
字典遍历
我们可以使用for-in
循环来遍历某个字典中的键值对。每一个字典中的数据项都以(key, value)
元组形式返回,并且我们可以使用临时常量或者变量来分解这些元组:
for (airportCode, airportName) in airports {
print("\(airportCode): \(airportName)")
}
// YYZ: Toronto Pearson
// LHR: London Heathrow
通过访问keys
或者values
属性,我们也可以遍历字典的键或者值:
for airportCode in airports.keys {
print("Airport code: \(airportCode)")
}
// Airport code: YYZ
// Airport code: LHR
for airportName in airports.values {
print("Airport name: \(airportName)")
}
// Airport name: Toronto Pearson
// Airport name: London Heathrow
如果我们只是需要使用某个字典的键集合或者值集合来作为某个接受Array
实例的 API 的参数,可以直接使用keys
或者values
属性构造一个新数组:
let airportCodes = [String](airports.keys)
// airportCodes 是 ["YYZ", "LHR"]
let airportNames = [String](airports.values)
// airportNames 是 ["Toronto Pearson", "London Heathrow"]
Swift 的字典类型是无序集合类型。为了以特定的顺序遍历字典的键或值,可以对字典的keys
或values
属性使用sorted()
方法。
条件语句
根据特定的条件执行特定的代码通常是十分有用的。当错误发生时,你可能想运行额外的代码;或者,当值太大或太小时,向用户显示一条消息。要实现这些功能,你就需要使用条件语句。
Swift 提供两种类型的条件语句:if
语句和switch
语句。通常,当条件较为简单且可能的情况很少时,使用if
语句。而switch
语句更适用于条件较复杂、有更多排列组合的时候。并且switch
在需要用到模式匹配(pattern-matching)的情况下会更有用。
If
if
语句最简单的形式就是只包含一个条件,只有该条件为true
时,才执行相关代码:
var temperatureInFahrenheit = 30
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
}
// 输出 "It's very cold. Consider wearing a scarf."
上面的例子会判断温度是否小于等于 32 华氏度(水的冰点)。如果是,则打印一条消息;否则,不打印任何消息,继续执行if
块后面的代码。
当然,if
语句允许二选一执行,叫做else
从句。也就是当条件为false
时,执行 else 语句:
temperatureInFahrenheit = 40
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
} else {
print("It's not that cold. Wear a t-shirt.")
}
// 输出 "It's not that cold. Wear a t-shirt."
显然,这两条分支中总有一条会被执行。由于温度已升至 40 华氏度,不算太冷,没必要再围围巾。因此,else
分支就被触发了。
你可以把多个if
语句链接在一起,来实现更多分支:
temperatureInFahrenheit = 90
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
print("It's really warm. Don't forget to wear sunscreen.")
} else {
print("It's not that cold. Wear a t-shirt.")
}
// 输出 "It's really warm. Don't forget to wear sunscreen."
在上面的例子中,额外的if
语句用于判断是不是特别热。而最后的else
语句被保留了下来,用于打印既不冷也不热时的消息。
实际上,当不需要完整判断情况的时候,最后的else
语句是可选的:
temperatureInFahrenheit = 72
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
print("It's really warm. Don't forget to wear sunscreen.")
}
在这个例子中,由于既不冷也不热,所以不会触发if
或else if
分支,也就不会打印任何消息。
Switch
switch
语句会尝试把某个值与若干个模式(pattern)进行匹配。根据第一个匹配成功的模式,switch
语句会执行对应的代码。当有可能的情况较多时,通常用switch
语句替换if
语句。
switch
语句最简单的形式就是把某个值与一个或若干个相同类型的值作比较:
switch some value to consider {
case value 1:
respond to value 1
case value 2,
value 3:
respond to value 2 or 3
default:
otherwise, do something else
}
switch
语句由多个 case 构成,每个由case
关键字开始。为了匹配某些更特定的值,Swift 提供了几种方法来进行更复杂的模式匹配,这些模式将在本节的稍后部分提到。
与if
语句类似,每一个 case 都是代码执行的一条分支。switch
语句会决定哪一条分支应该被执行,这个流程被称作根据给定的值切换(switching)。
switch
语句必须是完备的。这就是说,每一个可能的值都必须至少有一个 case 分支与之对应。在某些不可能涵盖所有值的情况下,你可以使用默认(default
)分支来涵盖其它所有没有对应的值,这个默认分支必须在switch
语句的最后面。
下面的例子使用switch
语句来匹配一个名为someCharacter
的小写字符:
let someCharacter: Character = "z"
switch someCharacter {
case "a":
print("The first letter of the alphabet")
case "z":
print("The last letter of the alphabet")
default:
print("Some other character")
}
// 输出 "The last letter of the alphabet"
在这个例子中,第一个 case 分支用于匹配第一个英文字母a
,第二个 case 分支用于匹配最后一个字母z
。 因为switch
语句必须有一个case分支用于覆盖所有可能的字符,而不仅仅是所有的英文字母,所以switch语句使用default
分支来匹配除了a
和z
外的所有值,这个分支保证了swith语句的完备性。
不存在隐式的贯穿
与 C 和 Objective-C 中的switch
语句不同,在 Swift 中,当匹配的 case 分支中的代码执行完毕后,程序会终止switch
语句,而不会继续执行下一个 case 分支。这也就是说,不需要在 case 分支中显式地使用break
语句。这使得switch
语句更安全、更易用,也避免了因忘记写break
语句而产生的错误。
注意: 虽然在Swift中
break
不是必须的,但你依然可以在 case 分支中的代码执行完毕前使用break
跳出。
每一个 case 分支都必须包含至少一条语句。像下面这样书写代码是无效的,因为第一个 case 分支是空的:
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a": // 无效,这个分支下面没有语句
case "A":
print("The letter A")
default:
print("Not the letter A")
}
// 这段代码会报编译错误
不像 C 语言里的switch
语句,在 Swift 中,switch
语句不会一起匹配"a"
和"A"
。相反的,上面的代码会引起编译期错误:case "a": 不包含任何可执行语句
——这就避免了意外地从一个 case 分支贯穿到另外一个,使得代码更安全、也更直观。
为了让单个case同时匹配a
和A
,可以将这个两个值组合成一个复合匹配,并且用逗号分开:
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a", "A":
print("The letter A")
default:
print("Not the letter A")
}
// 输出 "The letter A
为了可读性,符合匹配可以写成多行形式。
注意: 如果想要显式贯穿case分支,请使用
fallthrough
语句。
区间匹配
case 分支的模式也可以是一个值的区间。下面的例子展示了如何使用区间匹配来输出任意数字对应的自然语言格式:
let approximateCount = 62
let countedThings = "moons orbiting Saturn"
let naturalCount: String
switch approximateCount {
case 0:
naturalCount = "no"
case 1..<5:
naturalCount = "a few"
case 5..<12:
naturalCount = "several"
case 12..<100:
naturalCount = "dozens of"
case 100..<1000:
naturalCount = "hundreds of"
default:
naturalCount = "many"
}
print("There are \(naturalCount) \(countedThings).")
// 输出 "There are dozens of moons orbiting Saturn."
在上例中,approximateCount
在一个switch
声明中被评估。每一个case
都与之进行比较。因为approximateCount
落在了 12 到 100 的区间,所以naturalCount
等于"dozens of"
值,并且此后的执行跳出了switch
语句。
元组
我们可以使用元组在同一个switch
语句中测试多个值。元组中的元素可以是值,也可以是区间。另外,使用下划线(_
)来匹配所有可能的值。
下面的例子展示了如何使用一个(Int, Int)
类型的元组来分类下图中的点(x, y):
let somePoint = (1, 1)
switch somePoint {
case (0, 0):
print("\(somePoint) is at the origin")
case (_, 0):
print("\(somePoint) is on the x-axis")
case (0, _):
print("\(somePoint) is on the y-axis")
case (-2...2, -2...2):
print("\(somePoint) is inside the box")
default:
print("\(somePoint) is outside of the box")
}
// 输出 "(1, 1) is inside the box"
在上面的例子中,switch
语句会判断某个点是否是原点(0, 0),是否在红色的x轴上,是否在橘黄色的y轴上,是否在一个以原点为中心的4x4的蓝色矩形里,或者在这个矩形外面。
不像 C 语言,Swift 允许多个 case 匹配同一个值。实际上,在这个例子中,点(0, 0)可以匹配所有四个 case。但是,如果存在多个匹配,那么只会执行第一个被匹配到的 case 分支。考虑点(0, 0)会首先匹配case (0, 0)
,因此剩下的能够匹配的分支都会被忽视掉。
值绑定(Value Bindings)
case 分支允许将匹配的值声明为临时常量或变量,并且在case分支体内使用 —— 这种行为被称为值绑定(value binding),因为匹配的值在case分支体内,与临时的常量或变量绑定。
下面的例子将下图中的点(x, y),使用(Int, Int)
类型的元组表示,然后分类表示:
let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
print("on the x-axis with an x value of \(x)")
case (0, let y):
print("on the y-axis with a y value of \(y)")
case let (x, y):
print("somewhere else at (\(x), \(y))")
}
// 输出 "on the x-axis with an x value of 2"
在上面的例子中,switch
语句会判断某个点是否在红色的x轴上,是否在橘黄色的y轴上,或者不在坐标轴上。
这三个 case 都声明了常量x
和y
的占位符,用于临时获取元组anotherPoint
的一个或两个值。第一个 case ——case (let x, 0)
将匹配一个纵坐标为0
的点,并把这个点的横坐标赋给临时的常量x
。类似的,第二个 case ——case (0, let y)
将匹配一个横坐标为0
的点,并把这个点的纵坐标赋给临时的常量y
。
一旦声明了这些临时的常量,它们就可以在其对应的 case 分支里使用。在这个例子中,它们用于打印给定点的类型。
请注意,这个switch
语句不包含默认分支。这是因为最后一个 case ——case let(x, y)
声明了一个可以匹配余下所有值的元组。这使得switch
语句已经完备了,因此不需要再书写默认分支。
Where
case 分支的模式可以使用where
语句来判断额外的条件。
下面的例子把下图中的点(x, y)进行了分类:
let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
print("(\(x), \(y)) is on the line x == y")
case let (x, y) where x == -y:
print("(\(x), \(y)) is on the line x == -y")
case let (x, y):
print("(\(x), \(y)) is just some arbitrary point")
}
// 输出 "(1, -1) is on the line x == -y"
在上面的例子中,switch
语句会判断某个点是否在绿色的对角线x == y
上,是否在紫色的对角线x == -y
上,或者不在对角线上。
这三个 case 都声明了常量x
和y
的占位符,用于临时获取元组yetAnotherPoint
的两个值。这两个常量被用作where
语句的一部分,从而创建一个动态的过滤器(filter)。当且仅当where
语句的条件为true
时,匹配到的 case 分支才会被执行。
就像是值绑定中的例子,由于最后一个 case 分支匹配了余下所有可能的值,switch
语句就已经完备了,因此不需要再书写默认分支。
复合匹配
当多个条件可以使用同一种方法来处理时,可以将这几种可能放在同一个case
后面,并且用逗号隔开。当case后面的任意一种模式匹配的时候,这条分支就会被匹配。并且,如果匹配列表过长,还可以分行书写:
let someCharacter: Character = "e"
switch someCharacter {
case "a", "e", "i", "o", "u":
print("\(someCharacter) is a vowel")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
"n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
print("\(someCharacter) is a consonant")
default:
print("\(someCharacter) is not a vowel or a consonant")
}
// 输出 "e is a vowel"
这个switch
语句中的第一个case,匹配了英语中的五个小写元音字母。相似的,第二个case匹配了英语中所有的小写辅音字母。最终,default
分支匹配了其它所有字符。 复合匹配同样可以包含值绑定。复合匹配里所有的匹配模式,都必须包含相同的值绑定。并且每一个绑定都必须获取到相同类型的值。这保证了,无论复合匹配中的哪个模式发生了匹配,分支体内的代码,都能获取到绑定的值,并且绑定的值都有一样的类型。
let stillAnotherPoint = (9, 0)
switch stillAnotherPoint {
case (let distance, 0), (0, let distance):
print("On an axis, \(distance) from the origin")
default:
print("Not on an axis")
}
// 输出 "On an axis, 9 from the origin"
上面的case有两个模式:(let distance, 0)
匹配了在x轴上的值,(0, let distance)
匹配了在y轴上的值。两个模式都绑定了distance
,并且distance
在两种模式下,都是整型——这意味着分支体内的代码,只要case匹配,都可以获取到distance
值。
For-In 循环
你可以使用 for-in
循环来遍历一个集合中的所有元素,例如数组中的元素、范围内的数字或者字符串中的字符。
以下例子使用 for-in
遍历一个数组所有元素:
let names = ["Anna", "Alex", "Brian", "Jack"]
for name in names {
print("Hello, \(name)!")
}
// Hello, Anna!
// Hello, Alex!
// Hello, Brian!
// Hello, Jack!
你也可以通过遍历一个字典来访问它的键值对。遍历字典时,字典的每项元素会以 (key, value)
元组的形式返回,你可以在 for-in
循环中使用显式的常量名称来解读 (key, value)
元组。下面的例子中,字典的键声明会为 animalName
常量,字典的值会声明为 legCount
常量:
let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
for (animalName, legCount) in numberOfLegs {
print("\(animalName)s have \(legCount) legs")
}
// ants have 6 legs
// spiders have 8 legs
// cats have 4 legs
字典的内容理论上是无序的,遍历元素时的顺序是无法确定的。将元素插入字典的顺序并不会决定它们被遍历的顺序。
for-in
循环还可以使用数字范围。下面的例子用来输出乘法表的一部分内容:
for index in 1...5 {
print("\(index) times 5 is \(index * 5)")
}
// 1 times 5 is 5
// 2 times 5 is 10
// 3 times 5 is 15
// 4 times 5 is 20
// 5 times 5 is 25
例子中用来进行遍历的元素是使用闭区间操作符(...
)表示的从 1
到 5
的数字区间。index
被赋值为闭区间中的第一个数字(1
),然后循环中的语句被执行一次。在本例中,这个循环只包含一个语句,用来输出当前 index
值所对应的乘 5 乘法表的结果。该语句执行后,index
的值被更新为闭区间中的第二个数字(2
),之后 print(_:separator:terminator:)
函数会再执行一次。整个过程会进行到闭区间结尾为止。
上面的例子中,index
是一个每次循环遍历开始时被自动赋值的常量。这种情况下,index
在使用前不需要声明,只需要将它包含在循环的声明中,就可以对其进行隐式声明,而无需使用 let
关键字声明。
如果你不需要区间序列内每一项的值,你可以使用下划线(_
)替代变量名来忽略这个值:
let base = 3
let power = 10
var answer = 1
for _ in 1...power {
answer *= base
}
print("\(base) to the power of \(power) is \(answer)")
// 输出 "3 to the power of 10 is 59049"
这个例子计算 base 这个数的 power 次幂(本例中,是 3
的 10
次幂),从 1
( 3
的 0
次幂)开始做 3
的乘法, 进行 10
次,使用 1
到 10
的闭区间循环。这个计算并不需要知道每一次循环中计数器具体的值,只需要执行了正确的循环次数即可。下划线符号 _
(替代循环中的变量)能够忽略当前值,并且不提供循环遍历时对值的访问。
在某些情况下,你可能不想使用闭区间,包括两个端点。想象一下,你在一个手表上绘制分钟的刻度线。总共 60
个刻度,从 0
分开始。使用半开区间运算符(..<
)来表示一个左闭右开的区间。
let minutes = 60
for tickMark in 0..<minutes {
// 每一分钟都渲染一个刻度线(60次)
}
一些用户可能在其UI中可能需要较少的刻度。他们可以每5分钟作为一个刻度。使用 stride(from:to:by:)
函数跳过不需要的标记。
let minuteInterval = 5
for tickMark in stride(from: 0, to: minutes, by: minuteInterval) {
// 每5分钟渲染一个刻度线 (0, 5, 10, 15 ... 45, 50, 55)
}
可以在闭区间使用 stride(from:through:by:)
起到同样作用:
let hours = 12
let hourInterval = 3
for tickMark in stride(from: 3, through: hours, by: hourInterval) {
// 每3小时渲染一个刻度线 (3, 6, 9, 12)
}
While 循环
while
循环会一直运行一段语句直到条件变成false
。这类循环适合使用在第一次迭代前,迭代次数未知的情况下。Swift 提供两种while
循环形式:
-
while
循环,每次在循环开始时计算条件是否符合; -
repeat-while
循环,每次在循环结束时计算条件是否符合。
While
while
循环从计算一个条件开始。如果条件为true
,会重复运行一段语句,直到条件变为false
。
下面是 while
循环的一般格式:
while condition {
statements
}
下面的例子来玩一个叫做蛇和梯子(也叫做滑道和梯子)的小游戏:
游戏的规则如下:
- 游戏盘面包括 25 个方格,游戏目标是达到或者超过第 25 个方格;
- 每一轮,你通过掷一个六面体骰子来确定你移动方块的步数,移动的路线由上图中横向的虚线所示;
- 如果在某轮结束,你移动到了梯子的底部,可以顺着梯子爬上去;
- 如果在某轮结束,你移动到了蛇的头部,你会顺着蛇的身体滑下去。
游戏盘面可以使用一个Int
数组来表达。数组的长度由一个finalSquare
常量储存,用来初始化数组和检测最终胜利条件。游戏盘面由 26 个 Int
0 值初始化,而不是 25 个(由0
到25
,一共 26 个):
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
一些方格被设置成特定的值来表示有蛇或者梯子。梯子底部的方格是一个正值,使你可以向上移动,蛇头处的方格是一个负值,会让你向下移动:
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
3 号方格是梯子的底部,会让你向上移动到 11 号方格,我们使用board[03]
等于+08
(来表示11
和3
之间的差值)。为了对齐语句,这里使用了一元正运算符(+i
)和一元负运算符(-i
),并且小于 10 的数字都使用 0 补齐(这些语法的技巧不是必要的,只是为了让代码看起来更加整洁)。
玩家由左下角空白处编号为 0 的方格开始游戏。玩家第一次掷骰子后才会进入游戏盘面:
var square = 0
var diceRoll = 0
while square < finalSquare {
// 掷骰子
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
// 根据点数移动
square += diceRoll
if square < board.count {
// 如果玩家还在棋盘上,顺着梯子爬上去或者顺着蛇滑下去
square += board[square]
}
}
print("Game over!")
本例中使用了最简单的方法来模拟掷骰子。 diceRoll
的值并不是一个随机数,而是以0
为初始值,之后每一次while
循环,diceRoll
的值增加 1 ,然后检测是否超出了最大值。当diceRoll
的值等于 7 时,就超过了骰子的最大值,会被重置为1
。所以diceRoll
的取值顺序会一直是 1
,2
,3
,4
,5
,6
,1
,2
等。
掷完骰子后,玩家向前移动diceRoll
个方格,如果玩家移动超过了第 25 个方格,这个时候游戏将会结束,为了应对这种情况,代码会首先判断square
的值是否小于board
的count
属性,只有小于才会在board[square]
上增加square
,来向前或向后移动(遇到了梯子或者蛇)。
注意:
如果没有这个检测(square < board.count
),board[square]
可能会越界访问board
数组,导致错误。
当本轮while
循环运行完毕,会再检测循环条件是否需要再运行一次循环。如果玩家移动到或者超过第 25 个方格,循环条件结果为false
,此时游戏结束。
while
循环比较适合本例中的这种情况,因为在 while
循环开始时,我们并不知道游戏要跑多久,只有在达成指定条件时循环才会结束。
Repeat-While
while
循环的另外一种形式是repeat-while
,它和while
的区别是在判断循环条件之前,先执行一次循环的代码块。然后重复循环直到条件为false
。
注意:
Swift语言的repeat-while
循环和其他语言中的do-while
循环是类似的。
下面是 repeat-while
循环的一般格式:
repeat {
statements
} while condition
还是蛇和梯子的游戏,使用repeat-while
循环来替代while
循环。finalSquare
、board
、square
和diceRoll
的值初始化同while
循环时一样:
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0
repeat-while
的循环版本,循环中第一步就需要去检测是否在梯子或者蛇的方块上。没有梯子会让玩家直接上到第 25 个方格,所以玩家不会通过梯子直接赢得游戏。这样在循环开始时先检测是否踩在梯子或者蛇上是安全的。
游戏开始时,玩家在第 0 个方格上,board[0]
一直等于 0, 不会有什么影响:
repeat {
// 顺着梯子爬上去或者顺着蛇滑下去
square += board[square]
// 掷骰子
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
// 根据点数移动
square += diceRoll
} while square < finalSquare
print("Game over!")
检测完玩家是否踩在梯子或者蛇上之后,开始掷骰子,然后玩家向前移动diceRoll
个方格,本轮循环结束。
循环条件(while square < finalSquare
)和while
方式相同,但是只会在循环结束后进行计算。在这个游戏中,repeat-while
表现得比while
循环更好。repeat-while
方式会在条件判断square
没有超出后直接运行square += board[square]
,这种方式可以比起前面 while
循环的版本,可以省去数组越界的检查。
网友评论