第 2 章 使用集合

第 2 章 使用集合

前一章讨论了很多Swift基础知识,并通过大量示例(包括有意编写的错误代码)来加深你对变量、常量和类型等概念的理解。你将感受到,第1章介绍的知识对你理解本章及后续内容很有帮助。

本章将注意力转向集合。

说到集合,你脑海中浮现的是什么呢?下面是几个你可能遇到的集合:

  • 各色动作人偶和配饰;

  • 不同国家发行的邮票或硬币;

  • 杂货店购物清单。

集合的分类也可能更为常见:

  • 停车场中车辆的制造商和型号;

  • 婚礼邀请名单中的人名。

这些例子都会让人想起将不同的元素或东西(玩具、邮票、车辆等)编组。集合由相关(甚至不相关)的元素组成,在Swift语言中非常重要。稍后你将看到,集合有多种。

本章重点讨论集合,它让你能够以各种方式将信息和数据编组。在本书后面以及Swift开发中,将大量使用集合,请务必花时间搞懂它们。

2.1 糖果罐

为探索Swift集合概念,想想商店柜台上的空糖果罐,你可以将各种糖果放入其中。我们将这个糖果罐称为容器,可存储一个或多个糖果()。在Swift中,可以用多种方式模拟糖果罐及其存放的东西,下面从数组开始。

数组不过是具有指定长度的有序值列表,它是计算机语言中常见的结构。在Swift中,声明数组很简单。

重新开始

本书将继续使用REPL来探索各种概念。本章假设重启了REPL,因此代码片段从行号1开始。别忘了,要退出REPL,可执行命令:quit,要在Terminal中启动REPL,可执行命令xcrun swift

下面的数组表示一个虚拟的糖果罐,其中包含三颗糖果:

  1> let candyJar = ["Peppermints", "Gooey Bears", "Happy Ranchers"]
candyJar: [String] = 3 values {
  [0] = "Peppermints"
  [1] = "Gooey Bears"
  [2] = "Happy Ranchers"
}
  2>

知道关键字let吗?这是用于声明常量的关键字。这里声明了一个名为candyJar的常量,并使用了特殊表示法:左中括号([)和右中括号(])。将数组的元素放在一对中括号内,Swift就知道声明的是数组。

这个数组包含三个String常量:

  • "Peppermints"

  • "Gooey Bears"

  • "Happy Ranchers"

数组元素之间的逗号指出了一个元素的结束位置和另一个元素的开始位置。另外,注意到Swift也根据每个元素的表示方式推断出了它们的类型是字符串,这是因为它们都用双引号括起来了。

注意到Swift报告了声明结果,它明确地指出了candyJar是一个String数组:

candyJar: [String] = 3 values {
  [0] = "Peppermints"
  [1] = "Gooey Bears"
  [2] = "Happy Ranchers"
}

Swift确认这个数组包含三个值,这些值按顺序排列,编号从0开始。事实上,所有Swift数组的元素编号都从0开始,并按顺序递增。这种编号被称为数组索引,对应于数组包含的值。

到目前为止,一切顺利。这很好。下面学习如何查看数组的特定值。假设你要查看这个数组的第二个元素;鉴于索引从0开始,因此第二个元素的索引为1。

  2> candyJar[1]
$R0: String = "Gooey Bears"
  3>

注意 使不使用print呢?别忘了,使用REPL时,可使用元素名来显示其值,而无需使用方法print。这样做时,表达式的结果将被赋给一个临时变量,这里为$R0

Swift显示第二个元素的值Gooey Bears,同时指出了其类型是String

如果将[1]替换为另一个数组索引,将显示相应位置处的值:

  3> candyJar[2]
$R1: String = "Happy Ranchers"
  4>

这种表示法很方便。要访问数组的元素,只需使用其索引并将其放在方括号中。

如果引用不存在的位置,结果将如何呢?下面使用了一个显然不在可用索引范围内的索引:

  4> candyJar[5]
fatal error: Array index out of range
Execution interrupted. Enter Swift code to recover and continue.
Enter LLDB commands to investigate (type :help for assistance.)
  5>

你请求了一个不存在的元素,这显然触犯了Swift的禁忌。这个数组没有第6个元素,它只有3个元素,索引分别为0、1和2。Swift是一种非常严格的语言,对于这种违规操作,它报以致命错误。

这是Swift优于其他一些计算机语言的地方:它很重视安全。Swift打造了一个安全的环境,绝不容忍使用不存在的数组索引等不恰当的行为。如果你使用过C语言数组,就知道完全能够访问不存在的数组部分。事实上,特洛伊木马病毒的编写者正是利用了这种漏洞来攻击计算机系统。

上述错误消息提出了一个很好的问题:如果要在数组中存储更多的值,该如何办呢?Swift提供了一个用于数组的特殊方法:append()。要在数组中添加值,只需使用这个方法附加即可。下面将我喜欢的糖果加入到这个糖果罐中:

  5> candyJar.append("Candy Canes")
repl.swift:5:1: error: immutable value of type '[String]' only has mutating members named 'append'
candyJar.append("Candy Canes")
^       ~~~~~~

  5>

又出现了错误消息!看来你想松口气都不行了。我们都是在犯错中成长的,在学习Swift的过程中犯错不可避免。

你能找出导致错误的原因吗?请仔细查看错误消息:

error: immutable value of type '[String]' only has mutating members named 'append'

Swift指出这个String数组是不可修改的,而你竟然试图修改常量数组的内容。别忘了,前面使用let将这个数组声明成了常量数组。不能修改常量的值,包括被声明为常量的数组的值。

你需要创建另一个数组——可变数组。这很容易,这里使用了前述不可变数组的内容:

  5> var refillableCandyJar = candyJar
refillableCandyJar: [String] = 3 values {
  [0] = "Peppermints"
  [1] = "Gooey Bears"
  [2] = "Happy Ranchers"
}
  6>

就这么简单!你声明了变量refillableCandyJar,并使用了常量数组candyJar的内容来初始化它。现在,前述常量数组的所有值都包含在这个可变数组中。

2.1.1 数组中所有元素的类型都必须相同

可在数组中包含不同类型的值吗?请尝试像下面这样做:

  6> var arrayTest = ["x", 3]
repl.swift:6:23: error: 'Int' is not convertible to 'IntegerLiteralConvertible'
var arrayTest = ["x", 3]
                   ^

  6>

显然,Swift禁止这样做,这表明一个数组的所有值的类型都必须相同。

说到类型,如果要在声明数组时指定其值的类型,该怎么做呢?

  6> var h2o:[String] = ["Hydrogen", "Hydrogen", "Oxygen"]
h2o: [String] = 3 values {
  [0] = "Hydrogen"
  [1] = "Hydrogen"
  [2] = "Oxygen"
}
  7>

要声明用于存储特定类型值的数组,需要在数组名后面加上冒号,再加上放在方括号([])的类型名。

2.1.2 增长数组

回到数组refillableCandyJar,前面的第5行将其声明成了可变数组。现在可以在这个数组末尾附加值吗?下面来试试:

  7> refillableCandyJar.append("Candy Canes")
  8>

除提示符外,没有任何指出这种操作是否成功的信息,但毕竟没有出现错误消息。来看看这个数组的内容,核实Candy Canes是否被添加到其中:

  8> refillableCandyJar
$R2: [String] = 4 values {
  [0] = "Peppermints"
  [1] = "Gooey Bears"
  [2] = "Happy Ranchers"
  [3] = "Candy Canes"
}
  9>

它确实出现在这个数组的第四个位置(数组索引为3),与我们预期的一致。

下面在这个糖果罐中再加入几颗糖果,但使用不同的语法:

  9> refillableCandyJar += ["Peanut Clusters"]
 10> refillableCandyJar += ["Banana Taffy", "Bubble Gum"]
 11> refillableCandyJar
$R3: [String] = 7 values {
  [0] = "Peppermints"
  [1] = "Gooey Bears"
  [2] = "Happy Ranchers"
  [3] = "Candy Canes"
  [4] = "Peanut Clusters"
  [5] = "Banana Taffy"
  [6] = "Bubble Gum"
}
 12>

第9~10行附加了一个数组,而不是单个String值,这充分说明了Swift的灵活性:只需使用运算符+=就可将一个数组的内容加入到另一个数组中。最后,第11行请求显示数组refillableCandyJar的内容,Swift显示了这个虚拟糖果罐内的所有东西:全部七种美味的糖果。

至此,你创建了一个常量数组和一个变量数组,还将常量数组的值赋给了变量数组,以创建一个可以修改的数组。你还使用方法append()和运算符+=成功地修改了这个数组。下面更深入地探索数组。

2.1.3 替换和删除值

替换数组的值很简单,只需指定数组索引并赋给它新值即可。下面将Happy Ranchers替换为另一种糖果:

 12> refillableCandyJar[2] = "Lollipops"
 13> refillableCandyJar
$R4: [String] = 7 values {
  [0] = "Peppermints"
  [1] = "Gooey Bears"
  [2] = "Lollipops"
  [3] = "Candy Canes"
  [4] = "Peanut Clusters"
  [5] = "Banana Taffy"
  [6] = "Bubble Gum"
}
 14>

通过以上操作成功地完成了替换。那么,该如何删除值呢?你可能不喜欢Gooey Bears糖果,能将其从这个虚拟糖罐中删除吗?当然可以。前面将值附加到了一个可变数组中,下面将一些值完全删除:

 14> refillableCandyJar.removeAtIndex(1)
$R5: String = "Gooey Bears"
 15> refillableCandyJar
$R6: [String] = 6 values {
  [0] = "Peppermints"
  [1] = "Lollipops"
  [2] = "Candy Canes"
  [3] = "Peanut Clusters"
  [4] = "Banana Taffy"
  [5] = "Bubble Gum"
}
 16>

第14行使用了数组的方法removeAtIndex(),它接受一个参数,即要删除的值的索引。调用这个方法的结果是,指定的值被删除并被赋给$R5

第15行显示变量refillableCandyJaron的内容,结果表明Gooey Bears确实被删除了,且后面的值都向前移了。现在,糖果罐中只有6种糖果,而不是7种。

别忘了,每当数组中有元素被删除时,后面的元素都将向前移,以填补它留下的空缺。

下面是删除数组最后一个值的另一种便利方式:

 16> refillableCandyJar.removeLast()
$R7: String = "Bubble Gum"
 17>

同样,这么操作将返回被删除的值,而数组从包含6个值缩短到只包含5个值:

 17> refillableCandyJar
$R8: [String] = 5 values {
  [0] = "Peppermints"
  [1] = "Lollipops"
  [2] = "Candy Canes"
  [3] = "Peanut Clusters"
  [4] = "Banana Taffy"
}
 18>

2.1.4 将值插入到指定位置

本章前面,你使用方法append()在数组末尾添加了值,还让Swift将指定位置的值从数组中删除。下面来看看插入值有多容易。

"Twirlers"插入到"Candy Canes"当前所属的位置,即索引2处(数组的第三个位置):

 18> refillableCandyJar.insert("Twirlers", atIndex: 2)
 19>

下面来查看这个数组的内容,看看插入是否成功了:

 19> refillableCandyJar
$R9: [String] = 6 values {
  [0] = "Peppermints"
  [1] = "Lollipops "
  [2] = "Twirlers"
  [3] = "Candy Canes"
  [4] = "Peanut Clusters"
  [5] = "Banana Taffy"
 20>

确实成功了,Twirlers现在位于索引2处。后面的值向后移了一个位置,为这个新值腾出空间,这都是方法insert()的功劳。

注意到方法insert()接受两个参数:要插入到数组中的值以及要插入到什么位置(索引)。这个方法的第二个参数很有趣,它前面有名称atIndex:。命名参数为传递参数提供了上下文,可提高Swift代码的可读性。稍后将更详细地介绍Swift中如何为方法命名。

2.1.5 合并数组

在Swift中,数组合并语法很自然,就像字符串拼合语法一样。为演示这一点,再创建一个数组,其中包含一组糕点:

 20> var anotherRefillableCandyJar = ["Sour Tarts", "Cocoa Bar", "Coconut Rounds"]
anotherRefillableCandyJar: [String] = 3 values {
  [0] = "Sour Tarts"
  [1] = "Cocoa Bar"
  [2] = "Coconut Rounds"
}
 21>

现在创建第三个数组,这是使用合并语法将前面包含6个值的数组refillableCandyJar与包含3个值的新数组anotherRefillableCandyJar合并得到的:

 21> var combinedRefillableCandyJar = refillableCandyJar + anotherRefillableCandyJar
combinedRefillableCandyJar: [String] = 9 values {
  [0] = "Peppermints"
  [1] = "Lollipops"
  [2] = "Twirlers"
  [3] = "Candy Canes"
  [4] = "Peanut Clusters"
  [5] = "Banana Taffy"
  [6] = "Sour Tarts"
  [7] = "Cocoa Bar"
  [8] = "Coconut Rounds"
}
 22>

这个数组包含9个值,其中6个来自第一个数组,另外3个来自第二个数组。另外,这些值的排列顺序不变。

前面介绍了很多有关数组的知识。数组非常适合用于存储值列表,这些值是否彼此相关没有关系。数组可以是不可变的(通过使用命令let将其声明为常量),也可以是可变的(可以添加、删除或替换值)。最后,在同一个数组中,所有值的类型都必须相同。

下一节介绍另一种集合:字典。

2.2 字典

说到字典,你脑海中浮现的可能是丹尼尔·韦伯斯特(Danieal Webster)1。图书馆书架上的字典由单词定义组成,你按字母顺序查找单词以获悉其定义。在Swift中,字典的工作原理与此类似。

1疑为原书错误,应该是诺亚·韦伯斯特(N.Webster),韦氏词典的最初编纂者。——编者注

与数组一样,Swift字典也由一个或多个条目组成。然而与数组所不同的是,字典中的条目包含两个不同的部分:(key)和(value)。虽然键和值的类型可以不同,但所有键的类型都必须相同,所有值的类型也必须相同。

为介绍字典,我将以自己最喜欢的佐料——辣椒为例。辣椒的辣度各不相同,辣度用史高维尔指标(Scoville)表示。下面使用了一个字典来定义一些辣椒的辣度,其中每个条目的键都是辣椒名,而值为辣椒的辣度:

 22> var scovilleScale = ["Poblano":1_000, "Serrano":700, "Red Amazon": 75_000, "Red Savina Habanero" : 500_000]
scovilleScale: [String : Int] = 4 key/value pairs {
  [0] = {
   key = "Serrano"
   value = 700
  }
  [1] = {
   key = "Red Savina Habanero"
   value = 500000
  }
  [2] = {
   key = "Poblano"
   value = 1000
  }
  [3] = {
   key = "Red Amazon"
   value = 75000
  }
}
 23>

这创建了一个包含四个条目的字典:辣度为1000的Poblano、辣度为700的Serrano、辣度为75 000的Red Amazon和辣度为500 000的Red Savina Habanero(这种辣椒真是变态辣)。

注意 字典键并不一定是按字母顺序排列的,Swift总是采用可最大限度地提高检索和访问效率的顺序来排列它们。

当你声明上述字典时,Swift也推断键和值的类型。显然,键的类型是String,值的类型为Int,REPL显示的声明结果印证了这一点。另外,这里在数字中将下划线用作千分位,别忘了这纯粹是语法糖,对Int的值没有影响:REPL显示的结果不再有下划线。

你发现了不正常的地方吗?如果仔细查看REPL的输出,将发现你声明字典时指定的条目顺序,与REPL报告中的条目顺序不同。Swift排列字典条目的顺序与你声明的顺序不同,这种差别说明了字典的一个重要特征:Swift通过排列键来确保检索和访问的效率。你不能根据声明顺序确定存储顺序。

2.2.1 查找条目

访问字典条目与访问数组值很像,只是方括号中包含的值不同。访问数组值时,使用的是值在数组中的位置(0、1、2、3等);而访问字典条目时,使用的是其键:

 23> scovilleScale["Serrano"]
$R10: Int? = 700
 24>

注意到Int后面也有问号。前面说过,问号表示值是可选的,即可以为nil。从字典返回的值总是可选的,但这并不意味着可以将字典条目的值设置为nil,如下所示:

 24> var myNilArray = ["someKey" : nil]
repl.swift:24:19: error: 'String' is not convertible to 'StringLiteralConvertible'
var myNilArray = ["someKey" : nil]
                  ^~~~~~~~~
 24>

这条错误消息有点难懂,它指出nil是一个无效值。另外,也不能将键指定为nil

 24> var myNilArray = [nil : "x"]
repl.swift:24:19: error: expression does not conform to type 'NilLiteralConvertible'
var myNilArray = [nil : "x"]
                  ^~~

 24>

那么,当你访问字典中的值时,Swift为何还要将其作为可选类型返回呢?再来看看包含辣度的字典:

 24> scovilleScale
$R11: [String : Int] = 4 key/value pairs {
  [0] = {
   key = "Serrano"
   value = 700
  }
  [1] = {
   key = "Red Savina Habanero"
   value = 500000
  }
  [2] = {
   key = "Poblano"
   value = 1000
  }
  [3] = {
   key = "Red Amazon"
   value = 75000
  }
}
 25>

这个字典依然包含以前的四个条目。如果访问字典中的值时,使用的键不存在,结果将如何呢?来看看这个字典是否记录了Tabasco的辣度:

 25> scovilleScale["Tabasco"]
$R12: Int? = nil
 26>

返回的值为nil。这正是将字典的值以可选类型返回的原因所在:查询字典时使用的键可能不存在。

2.2.2 添加条目

知道使用不存在的键查询辣度字典的结果后,我们将刚才查询的辣椒加入到这个字典中。由于这个字典是使用关键字var创建的可变字典,因此可以使用下面的语法在其中添加条目:

 26> scovilleScale["Tabasco"] = 50_000
 27>

来核实一下:

 27> scovilleScale
$R13: [String : Int] = 5 key/value pairs {
  [0] = {
   key = "Serrano"
   value = 700
  }
  [1] = {
   key = "Red Savina Habanero"
   value = 500000
  }
  [2] = {
   key = "Tabasco"
   value = 50000
  }
  [3] = {
   key = "Poblano"
   value = 1000
  }
  [4] = {
   key = "Red Amazon"
   value = 75000
  }
}
 28>

注意到新加入的辣椒在字典中的位置也是预先无法确定的。

2.2.3 更新条目

如果你熟悉各种辣椒,就知道前述Serrano的辣度不对。其辣度值差了一个数量级,是7000而不是700。下面就来修正这种错误:

 28> scovilleScale["Serrano"] = 7_000
 29>

更新字典条目的语法与添加条目相同,但使用的键是既有的。下面Swift将原来的值(700)替换为新值:

 29> scovilleScale
$R14: [String : Int] = 5 key/value pairs {
  [0] = {
   key = "Serrano"
   value = 7000
  }
  [1] = {
   key = "Red Savina Habanero"
   value = 500000
  }
  [2] = {
   key = "Tabasco"
   value = 50000
  }
  [3] = {
   key = "Poblano"
   value = 1000
  }
  [4] = {
   key = "Red Amazon"
   value = 75000
  }
}
 30>

2.2.4 删除条目

删除字典条目的语法与前两个示例很像。

 30> scovilleScale["Tabasco"] = nil
 31>

将值设置为nil就会将相应的条目删除。

 31> scovilleScale
$R15: [String : Int] = 4 key/value pairs {
  [0] = {
   key = "Serrano"
   value = 7000
  }
  [1] = {
   key = "Red Savina Habanero"
   value = 500000
  }
  [2] = {
   key = "Poblano"
   value = 1000
  }
  [3] = {
   key = "Red Amazon"
   value = 75000
  }
}
 32>

另一种删除字典条目的方式是使用方法removeValueForKey()

 32> scovilleScale.removeValueForKey("Poblano")
$R16: Int? = 1000
 33>

这里返回了被删除的条目的值(1000)。在有些情况下,这种删除字典条目的方式更佳,你将在本书后面发现这一点。

2.3 数组的数组

本章前面介绍数组和字典时,使用的是基本类型:字符串和整数。那么该如何声明字典数组或数组字典呢?在Swift中,数组和字典与字符串和整数一样也是类型,因此可以彼此包含对方。

下面将糖果罐类比再向前推进一步。Arceneaux先生定期给三家商店送糖果:Fontenot先生开的Grocery、Dupre先生开的Quick Mart和Smith先生开的Pick-n-Sack;这些商店都有糖果罐。Arceneaux先生每周前往这些商店一次,将他们的糖果罐填满。

Fontenot先生的顾客喜欢Choppers和Jaw Bombs,Dupre先生的顾客喜欢购买巧克力糖果,如Butterbar、Mrs. Goodbuys和Giggles,而Smith先生的顾客喜欢Jelly Munchers和Gooey Bears。如何使用数组或字典给这些糖果和商店建模呢?出于简化考虑,先从糖果罐着手。

 33> var fontenotsCandyJar = ["Choppers", "Jaw Bombs"]
fontenotsCandyJar: [String] = 2 values {
  [0] = "Choppers"
  [1] = "Jaw Bombs"
}
 34> var dupresCandyJar = ["Butterbar", "Mrs. Goodbuys", "Giggles"]
dupresCandyJar: [String] = 3 values {
  [0] = "Butterbar"
  [1] = "Mrs. Goodbuys"
  [2] = "Giggles"
}
 35> var smithsCandyJar = ["Jelly Munchers", "Gooey Bears"]
smithsCandyJar: [String] = 2 values {
  [0] = "Jelly Munchers"
  [1] = "Gooey Bears"
}
 36>

三个数组代表三个糖果罐,其中的变量名指出了糖果罐所属商店的名称。

现在,只需再创建一个数组:

 36> let arceneauxsCandyRoute = [fontenotsCandyJar, dupresCandyJar, smithsCandyJar]
arceneauxsCandyRoute: [[String]] = 3 values {
  [0] = 2 values {
   [0] = "Choppers"
   [1] = "Jaw Bombs"
  }
  [1] = 3 values {
   [0] = "Butterbar"
   [1] = "Mrs. Goodbuys"
   [2] = "Giggles"
  }
  [2] = 2 values {
   [0] = "Jelly Munchers"
   [1] = "Gooey Bears"
  }
}
 37>

Swift使用[[String]]指出arceneauxCandyRoute是一个元素为字符串数组的数组。

查询这个数组的第一个元素可显示该数组的第一个值,这个元素本身也是一个数组。

 37> arceneauxsCandyRoute[0]
$R17: [String] = 2 values {
  [0] = "Choppers"
  [1] = "Jaw Bombs"
}
 38>

注意,虽然糖果罐包含在数组arceneauxCandyRoute中,但并没有指出哪个糖果罐是哪家商店的。这是因为变量名并没有加入到数组中,加入的只有变量的值。

为让这个示例更清晰,我们不使用数组来存储糖果罐数组,而使用字典来存储它们:

 38> let arceneauxsOtherCandyRoute = ["Fontenot's Grocery": fontenotsCandyJar, "Dupre's Quick Mart": dupresCandyJar, "Smith's Pick-n-Sack": smithsCandyJar]
arceneauxsOtherCandyRoute: [String: [String]] = 3 key/value pairs {
  [0] = {
   key = "Dupre's Quick Mart"
   value = 3 values {
      [0] = "Butterbar"
      [1] = "Mrs. Goodbuys"
      [2] = "Giggles"
   }
  }
  [1] = {
   key = "Smith's Pick-n-Sack"
   value = 2 values {
      [0] = "Jelly Munchers"
      [1] = "Gooey Bears"
   }
  }
  [2] = {
   key = "Fontenot's Grocery"
   value = 2 values {
      [0] = "Choppers"
      [1] = "Jaw Bombs"
   }
  }
}
 39>

Swift使用[String : [String]]指出了arceneauxsOtherCandyRoute的类型,即这是一个字典,其中的键为字符串,而值为字符串数组。

相比于前面的数组的数组,这个数组字典的不同之处在于,其中的键(类型为String)对值(糖果罐)做了描述。现在获取所需的值时,可使用键而不是模糊不清的索引:

 39> arceneauxsOtherCandyRoute["Smith's Pick-n-Sack"]
$R18: [String]? = 2 values {
  [0] = "Jelly Munchers"
  [1] = "Gooey Bears"
}

使用数组、字典还是结合使用它们更合适呢?这取决于建模的具体情形。熟能生巧。

2.4 创建空数组和空字典

前面创建数组和字典时,都在声明时进行了初始化。在Swift开发中,有时必须在创建字典或数组时不对其进行初始化。原因可能是创建时还不知道它们的值,也可能是必须提供一个空数组或字典,由库或框架中的方法进行填充。

2.4.1 空数组

声明空数组的方式有两种:

 40> var myEmptyArray:Array<Int> = []
myEmptyArray: [Int] = 0 values
 41>

这是数组声明的普通方式,需要使用关键字Array,并在尖括号(<>)内指定数组的类型。还可使用Swift提供的简写方式:

 41> var myEmptyArray = [Int]()
myEmptyArray: [Int] = 0 values
 42>

这些示例都声明了一个用于存储Int值的可变空数组。由于是可变数组,可像其他数组一样修改或填充它。下面在这个数组中添加三个整数:

 42> myEmptyArray += [33, 44, 55]
 43> myEmptyArray
$R19: [Int] = 3 values {
  [0] = 33
  [1] = 44
  [2] = 55
}
 44>

还可将一个空数组赋给这个变量,从而将该数组的所有元素删除:

 44> myEmptyArray = []
 45> myEmptyArray
$R20: [Int] = 0 values
 46>

现在,这个数组的所有值都删除了,可使用它来存储其他数据。

2.4.2 空字典

空字典的创建方式与空数组类似,需要使用单词Dictionary和一对尖括号:

 46> var myEmptyDictionary = Dictionary<String, Double>()
myEmptyDictionary: [String : Double] = 0 key/value pairs
 47>

使用普通方式还是简写方式

Swift语法极其丰富而灵活,对于同一个操作,通常提供了多种表示方式。就数组和字典声明而言,使用简写方式可减少输入量,而普通方式更清晰。不管你决定采用哪种方式,建议始终采用同一种方式。

在这个示例中,指定了字典的键和值的类型:键的类型为String,值的类型为Double。这些类型是在尖括号内指定的,并用逗号分隔。现在,可以像下面这样在这个字典中添加条目了。

 47> myEmptyDictionary = ["MyKey":1.125]
 48> myEmptyDictionary
$R21: [String : Double] = 1 key/value pair {
  [0] = {
   key = "MyKey"
   value = 1.125
  }
}
 49>

2.5 迭代集合

介绍基本集合类型(数组和字典)后,该探索如何迭代它们了。迭代集合指的是查看数组或字典中的每个值,并可能对其做某种处理。

我们每天都在做迭代,当你按书面步骤清单完成一项任务时,其实就在迭代该清单,迭代数据也与之类似。迭代是一项极其常见的编码任务,稍后你将看到,Swift提供了多种结构让迭代集合易如反掌。

2.5.1 迭代数组

如果你使用过其他编程语言(如C语言),肯定非常熟悉for循环。Swift提供了多种for循环,它们的表达力比C语言中的for循环更强。即便你没有任何编程经验,也能很快学会这种概念。

for-in循环的结构类似于下面这样:

  for itemName in list {
   ... do something with itemName
  }

其中itemName可以是你想使用的任何名称;它将成为一个变量,迭代期间将依次把列表中的每个值赋给它。list是要迭代的对象,而大括号内的一切都是要执行的代码。

来重温一下本章前面的数组combinedRefillableCandyJar

 49> combinedRefillableCandyJar
$R22: [String] = 9 values {
  [0] = "Peppermints"
  [1] = "Lollipops"
  [2] = "Twirlers"
  [3] = "Candy Canes"
  [4] = "Peanut Clusters"
  [5] = "Banana Taffy"
  [6] = "Sour Tarts"
  [7] = "Cocoa Bar"
  [8] = "Coconut Rounds"
}
 50>

下面的代码段使用Swift的for-in结构显示该数组的每个值。这段代码包含多行,当你在REPL中输入它们时,提示符将从由数字和大于号组成变为由数字和句点组成。这是输入左大括号({)导致的,它告诉REPL接下来是一个代码块。

由于这种行为,等你输入右大括号(最后一行)后结果才会出现:

 50> for candy in combinedRefillableCandyJar {
 51.      print("I enjoy eating \(candy)!")
 52. }
I enjoy eating Peppermints!
I enjoy eating Lollipops!
I enjoy eating Twirlers!
I enjoy eating Candy Canes!
I enjoy eating Peanut Clusters!
I enjoy eating Banana Taffy!
I enjoy eating Sour Tarts!
I enjoy eating Cocoa Bar!
I enjoy eating Coconut Rounds!
 53>

下面详细解读这个代码块。数组combinedRefillableCandyJar的各个值依次被赋给变量candy。由于这个数组包含9个值,因此for-in循环将迭代9次。每次迭代时,都将执行大括号内的代码。这里将当前值与一个格式字符串合并,并将结果显示到屏幕上。

for-in循环还有另一个变种,它迭代数组的值及其索引:

 53> for (index, candy) in combinedRefillableCandyJar.enumerate() {
 54.     print("Candy \(candy) is in position \(index) of the array")
 55. }
Candy Peppermints is in position 0 of the array
Candy Lollipops is in position 1 of the array
Candy Twirlers is in position 2 of the array
Candy Candy Canes is in position 3 of the array
Candy Peanut Clusters is in position 4 of the array
Candy Banana Taffy is in position 5 of the array
Candy Sour Tarts is in position 6 of the array
Candy Cocoa Bar is in position 7 of the array
Candy Coconut Rounds is in position 8 of the array
 56>

在这里,将前面定义的数组combinedRefillableCandyJar作为参数传递给了方法enumerate()。这个方法返回一个元组(见第1章),其中包含数组的值及其索引。接下来,使用变量indexcandy创建了一个字符串。

2.5.2 迭代字典

使用for-in循环迭代字典时,方式与刚才介绍的数组示例相同。为演示这一点,我们重用前面为模拟Arceneaux先生给商店送糖果而创建的字典。

 56> for (key, value) in arceneauxsOtherCandyRoute {
 57.     print("\(key) has a candy jar with the following contents: \(value)")
 58. }
Dupre's Quick Mart has a candy jar with the following contents: [Butterbar, Mrs. Goodbuys, Giggles]
Smith's Pick-n-Sack has a candy jar with the following contents: [Jelly Munchers, Gooey Bears]
Fontenot's Grocery has a candy jar with the following contents: [Choppers, Jaw Bombs]
 59>

由于字典由键和值组成,因此使用for-in循环迭代时,将自动返回一个元组。该元组被捕获,其内容被赋给变量keyvalue。请注意,value本身是一个数组,而使用方法print显示该数组非常方便。另外,别忘了字典中的键并不一定是按字母顺序排列的。

为进一步演示迭代,我们直接对前面的for-in循环进行扩展,其中包含另一个for-in循环,如下所示:

 59> for (key, value) in arceneauxsOtherCandyRoute {
 60.      for (index, candy) in value.enumerate() {
 61.            print("\(key)'s candy jar contains \(candy) at position \(index)")
 62.      }
 63. }
Dupre's Quick Mart's candy jar contains Butterbar at position 0
Dupre's Quick Mart's candy jar contains Mrs. Goodbuys at position 1
Dupre's Quick Mart's candy jar contains Giggles at position 2
Smith's Pick-n-Sack's candy jar contains Jelly Munchers at position 0
Smith's Pick-n-Sack's candy jar contains Gooey Bears at position 1
Fontenot's Grocery's candy jar contains Choppers at position 0
Fontenot's Grocery's candy jar contains Jaw Bombs at position 1
 64>:quit

这是一个嵌套for-in循环。迭代字典arceneauxsOtherCandyRoute时,Swift将值(一个数组)捕获到变量value中,再将这个变量传递给方法enumerate()以便对其进行迭代。

注意 需要提醒你的是,虽然数组内容的存储顺序是固定不变的,但字典不是这样的。遍历字典时,不要假定字典内容的存储顺序与你加入的顺序相同。

2.6 小结

有关集合的旋风之旅已接近尾声。正如你看到的,数组和字典都非常适合用于编组各种数据类型,从文本到数字。这些集合甚至可以包含其他集合,让你能够打造出相当精致的数据引用方案。最后,你见识了Swift的数组和字典声明语法的灵活性:创建新集合时,你几乎能够完全控制代码的可读性。

下一章将介绍控制结构,它们让编程变得有趣起来。

目录