从这一章开始,我们一起来学习复杂数据类型。
作为复杂数据类型的第一部份,我想要给大家的介绍的是:数组 Array,字典 Dictionary,集合 Set,枚举 Enum。在这一章,我们会了解它们之间的差异,知道什么时候使用它们以及如何使用。
不同于前两章我们学习的简单数据类型,这四种数据类型之所以被称为复杂数据类型,肯定的说是要比前面我们学习的内容要困难和复杂的。但是别担心:你会发现今天学习的这四种类型会涵盖将来我们对数据进行分组所需的绝大多数要求,即使你在学习的时候选择了错误的类型,你会发现其实也不会发生很严重的问题。
事实上,我们在学习Swift的时候最容易范的错误就是害怕运行代码,生怕产生什么错误。但实际上你会发现:如果你遵循我们所学的规范来做,即使写错了也不会产生任何严重的问题,它不会破坏你的计算机,也不会发生任何严重到不可收拾的问题。所以,不要害怕,尽管尝试去做,去运行你写的代码,有错误就去纠正它,直到你觉得满意为止。这对我们任何人来说都是有帮助的!
好了,也许扯远了,让我们回归今天的主题——复杂数据类型四兄弟:数组、字典、集合和枚举。
数组 – 如何将有序数据存储在数组中
我们都知道,在发明计算机以后人们使用计算机不仅仅是为了计算数据,更多时候是用计算机来存储数据。但是数据并非是像我们前面学习的字符串或整数那样,只是一个数据,我们经常会遇到一大堆相关的数据集合在一起,例如一个班级的学生名单、一个公司里所有职员的薪资清单,甚至是一个城市里所有常驻人口的身份信息,这样的数据是非常庞大的,这个时候我们该如何存储合操作它们呢?
在Swift中,我们可以使用数组 Array 来进行对数据的分组。数组是一种数据的组合类型,就像String、Int和Double,但是数组可以容纳空数据,一个数据、两个数据、三个、五个甚至是几万个、几千万个相同种类的数据。它可以自动适应以容纳你需要的任意数量数据,并始终按照你添加的顺序保存起来。
让我们从一些例子开始:
var beatles = ["John", "Paul", "George", "Ringo"]
let numbers = [4, 8, 15, 16, 23, 42]
var temperatures = [25.3, 28.2, 26.4]
这里创建了三个不同的数组:一个持有人名字符串,一个持有重要数字的整数,另一个持有以摄氏度为单位的温度小数。注意:我们使用方括号开始和结束数组,每个项目之间都有逗号。
当涉及到从数组中读取值时,我们要求通过它们在数组中出现的位置来获取值。项目在数组中的位置通常称为其索引 Index。
记住,Swift对于数组的索引是从0开始的,而不是1。例如:beatles[0]是第一个元素,而beatles[1]才是第二个元素。这对于初学者来说有点困惑,但相信我:习惯了就好。
因此,我们可以像这样从数组中读取一些值:
print(beatles[0])
print(numbers[1])
print(temperatures[2])
提示:请确保你要读取的项目索引是存在的,否则你的代码会崩溃——Swift会报错提示你,程序也会停止工作。
如果你给你的数组声明的是一个变量,那么你的这个数组是可以改变的,我们可以使用append()来增加项目:
beatles.append("Adrian")
你可以多次增加项目:
beatles.append("Allen")
beatles.append("Adrian")
beatles.append("Novall")
beatles.append("Vivian")
但是,我们说过Swift是一种安全语言,它会监视你尝试添加的数据类型,确保你的数组一次只能添加同类型的数据项目。因此,下面这种代码是不被允许的:
temperatures.append("Chris")
这也适用于从数组中读取数据——Swift知道 beatles 数组包含字符串,因此当你读取一个值时,你总是会得到一个字符串。如果你试图对numbers做同样的事情,你总是会得到一个整数。Swift不允许你将这两种不同的类型混合在一起,因此也不允许使用这种代码:
let firstBeatle = beatles[0]
let firstNumber = numbers[0]
let notAllowed = firstBeatle + firstNumber
当你像从空数组开始并逐个添加项目时,你必须告知Swift你的空数组里的数据是何种类型的数据:
var scores = Array<Int>()
scores.append(100)
scores.append(80)
scores.append(85)
print(scores[1])
第一行代码即告知了Swift:这是一个包含整数的空数组。
我们也可以这样声明一个空数组:
var scores: Array<Int> = []
var numbers: [Int] = []
var nameList = [String]()
方法有很多种,但是请记住:一旦创建完成,这个数组中就只能添加创建时声明的数据类型,不可以再放入其他类型的数据了。
Swift的类型安全意味着它必须始终知道数组正在存储哪种类型的数据。如下代码,如果你提供一些初始值,Swift可以自己计算出来:
var albums = ["Folklore"]
albums.append("Fearless")
albums.append("Red")
在我们完成这一节之前,我想提一下数组附带的一些有用的功能。
首先,我们也可以使用.count来读取数组中有多少个元素,就像字符串一样:
print(albums.count)
其次,你可以使用remove(at:)在特定索引处删除一个项目,或使用removeAll()删除所有内容,从数组中删除项目:
var characters = ["Lana", "Pam", "Ray", "Sterling"]
print(characters.count)
characters.remove(at: 2)
print(characters.count)
characters.removeAll()
print(characters.count)
当字符被删除时,这将打印4,然后是3,然后是0。
第三,你可以使用contains()检查数组是否包含特定项,例如:
let bondMovies = ["Casino Royale", "Spectre", "No Time To Die"]
print(bondMovies.contains("Frozen"))
第四,你可以使用sorted()对数组进行排序,例如:
let cities = ["London", "Tokyo", "Rome", "Budapest"]
print(cities.sorted())
这返回一个新数组,其项目按升序排序,这意味着字符串按字母顺序排列,但数字按数字排列,但原始数组保持不变。
最后,你可以通过调用reversed()来反转数组:
let presidents = ["Bush", "Obama", "Trump", "Biden"]
let reversedPresidents = presidents.reversed()
print(reversedPresidents)
提示:当你反转一个数组时,Swift 非常聪明——它实际上并不做重新排列所有项目的工作,而只是记住你要反转这些项。因此,当你打印出 reversedPresidents 时,不要惊讶于它不再是一个简单的数组!
数组在 Swift 中非常常见,随着学习的深入,你将有很多机会了解更多有关数组的知识。更棒的是,字符串也可以使用 sorted()、reversed() 和许多其他数组功能——使用 sorted() 可以将字符串的字母按字母顺序排列,从而将 “swift “变成 “fistw”。
数组的总结
Swift 的字符串、整数、布尔值和双精度浮点数允许我们临时存储单个值,但如果要存储多个值,通常会使用数组来代替。
我们可以像创建其他数据类型一样创建数组常量和变量,但不同的是,数组内部可以存储许多值。因此,如果您想存储工作日的名称、下周的气温预报或视频游戏的高分,你需要的是数组而不是单个值。
Swift 中的数组可大可小。如果数组是可变的,您就可以随意添加项目,随着时间的推移积累数据,也可以根据需要删除甚至重新排列项目。
我们从数组中读取数值时使用的是从 0 开始计数的数字位置。这种 “从 0 开始计数 “有一个专业术语:我们可以说 Swift 的数组是基于 0 的。如果尝试使用无效索引读取数组,Swift 会自动让程序崩溃。例如,创建一个有三个项的数组,然后尝试读取索引 10。
我知道你在想什么:应用程序崩溃很糟糕,对吗?没错。但请相信我:如果 Swift 没有崩溃,那么你很可能会得到错误的数据,因为你试图读取的值超出了数组的容量。
字典 – 如何在字典中存储和查找数据
你已经知道了数组是如何存储具有特定顺序的数据的,例如一周中的几天或城市的温度。当项目按照你添加的顺序存储时,或者当您可能有重复的项目时,数组是一个不错的选择,但通常通过其在数组中的位置访问数据可能会令人厌烦,甚至危险。
例如,这是一个包含员工详细信息的数组:
var employee = ["Taylor Swift", "Singer", "Nashville"]
我告诉过你,这些数据是关于员工的,所以你也许可以猜到各个部分的功能:
print("Name: \(employee[0])")
print("Job title: \(employee[1])")
print("Location: \(employee[2])")
但这有几个问题:首先,你不能真正确定employee[2]是他们的位置——也许那是他们的登陆密码。其次,不能保证第2项甚至在那里,特别是因为我们使数组成为变量。这种代码会造成严重的问题:
print("Name: \(employee[0])")
employee.remove(at: 1)
print("Job title: \(employee[1])")
print("Location: \(employee[2])")
现在将Nashville打印为职称,这是错误的,当它读取employee[2],会导致我们的代码崩溃,这很糟糕。
Swift对这两个问题都有一个解决方案,称为字典 Dictionary。字典不像数组那样根据其位置存储项目,而是让我们决定项目应该存储在哪里。
例如,我们可以重写之前的示例,以更明确地说明每个项目是什么:
let employee2 = ["name": "Taylor Swift", "job": "Singer", "location": "Nashville"]
如果我们把它分成几行,你会更好地了解代码的作用:
let employee2 = [
"name": "Taylor Swift",
"job": "Singer",
"location": "Nashville"
]
如你所见,我们现在非常清楚:名字是Taylor Swift,工作是Singer,地点是Nashville。Swift调用左侧的字符串——名称、工作和位置——我们称之为字典的键 key,右侧的字符串是值 value。
当涉及到从字典中读取数据时,您使用与创建时相同的键:
print(employee2["name"])
print(employee2["job"])
print(employee2["location"])
如果您在 playground 中尝试这样做,您会看到 Xcode 抛出各种警告,如 “Expression implicitly coerced from ‘String?’ to ‘Any'”。更糟糕的是,如果你查看你的 playground 的输出,您会发现它打印的是 Optional(“Taylor Swift”),而不仅仅是 Taylor Swift,这是怎么回事呢?
想想看:
print(employee2["password"])
print(employee2["status"])
print(employee2["manager"])
所有这些都是有效的 Swift 代码,但我们正在尝试读取没有附加值的字典键。当然,Swift 可能会在这里崩溃,就像读取一个不存在的数组索引时会崩溃一样,如果我们尝试读取一个不存在的数组索引的时候,Swift会立即崩溃发出红色警告。但是,我们尝试读取不存在的字典键时,你得到的是一个nil。这是为什么?
因为Swift 提供了另一种选择:当你访问字典中的数据时,它会告诉我们 “你可能会得到一个值,但也可能什么都得不到”。Swift 将这些称为可选项 optional,因为数据的存在是可选的——它可能存在,也可能不存在。
Swift 甚至会在你编写代码时发出警告,尽管警告的方式相当隐晦–它会说 “Expression implicitly coerced from ‘String?’ to ‘Any'”,但它的真正意思是 “这个数据可能实际上并不存在——你确定要打印它吗?”
可选项是一个相当复杂的问题,我们稍后会详细介绍,但现在我会向你展示一种更简单的方法:从字典中读取数据时,如果键不存在,你可以提供一个默认值。
如下所示:
print(employee2["name", default: "Unknown"])
print(employee2["job", default: "Unknown"])
print(employee2["location", default: "Unknown"])
你会发现原先的黄色警告就不见了。这是因为我们提供了默认值之后,Swift知道即使这个字典键不存在或者它没有值,Swift也知道会返回你已经设定好的默认值Unknown了。
所有示例都使用字符串作为键和值,但也可以使用其他数据类型作为键和值。例如,我们可以用字符串表示姓名,用布尔值表示毕业状态,从而跟踪哪些学生已经毕业:
let hasGraduated = [
"Eric": false,
"Maeve": true,
"Otis": false,
]
或者,我们可以追踪奥运会举办的年份和地点:
let olympics = [
2012: "London",
2016: "Rio de Janeiro",
2021: "Tokyo"
]
print(olympics[2012, default: "Unknown"])
你也可以使用任何要存储的显式类型创建一个空字典,然后逐个设置键:
var heights = [String: Int]()
heights["Yao Ming"] = 229
heights["Shaquille O'Neal"] = 216
heights["LeBron James"] = 206
请注意,我们现在需要写 [String: Int],表示字典的键是字符串,值是整数。
由于每个字典项必须存在于一个特定的键上,因此字典不允许存在重复的键。相反,如果你为已经存在的键设置值,Swift 会覆盖之前的值。
例如,如果你正在和朋友谈论超级英雄和超级坏蛋,您可能会将它们存储在这样的字典中:
var archEnemies = [String: String]()
archEnemies["Batman"] = "The Joker"
archEnemies["Superman"] = "Lex Luthor"
如果你的朋友不同意小丑是蝙蝠侠的头号敌人,你可以使用相同的键重写该值:
archEnemies["Batman"] = "Penguin"
最后,就像我们目前看到的数组和其他数据类型一样,字典也提供了一些有用的功能,您将来会用到,count 和 removeAll() 都适用于字典,其工作原理与数组相同。
为什么Swift既要有数组又要有字典?
字典和数组都是在一个变量中存储大量数据的方法,但它们的存储方式不同:字典让我们选择一个 “键 “来标识要添加的项目,而数组只是按顺序添加每个项目。
因此,与其记住数组索引 7 表示用户的国家,不如直接写 user[“country”] – 这样更方便。
字典不会使用索引来存储条目,而是会优化存储条目的方式,以便快速检索。因此,当我们输入 user[“country”]时,即使字典中有 100,000 个条目,它也会立即返回该键(或 nil)上的条目。
请记住,你无法保证字典中的键是否存在。这就是为什么从字典中读取一个值可能什么都不会返回的原因——因为你可能请求了一个不存在的键!
为什么Swift要有字典的默认值?
每当从字典中读取一个值时,可能会返回一个值,也可能返回 nil,即该键值可能没有值。没有值可能会给你的代码带来问题,尤其是因为你需要添加额外的功能来安全地处理丢失的值,这就是字典缺省值的作用所在:它可以让你提供一个备份值,以便在你要求的键不存在时使用。
例如,下面是一个存储学生考试成绩的字典:
let results = [
"english": 100,
"french": 85,
"geography": 75
]
如图所示,他们参加了三次考试,英语、法语和地理成绩分别为 100、85 和 75。如果我们想读取他们的历史成绩,如何读取取决于我们想要什么:
- 如果缺失的值表示该学生没有参加考试,那么我们可以使用默认值 0,这样就可以始终得到一个整数。
- 如果缺失的值意味着学生尚未参加考试,那么我们就应该跳过默认值,转而寻找一个nil值。
因此,在使用字典时,你并不总是需要默认值,但当你需要时就很容易了:
let historyResult = results["history", default: 0]
集合 – 如何使用集合快速数据查找
到目前为止,你已经了解了在 Swift 中收集数据的两种方法:数组和字典。还有第三种非常常见的数据分组方式,叫做集合 Set ——它们与数组类似,只是不能添加重复项,也不按特定顺序存储项。
创建集合的方法与创建数组很相似:告诉 Swift 它将存储哪类数据,然后继续添加。不过,它们有两个重要的不同点,最好用一些代码来演示。
首先,下面是创建演员名称集合的方法:
let people = Set(["Denzel Washington", "Tom Cruise", "Nicolas Cage", "Samuel L Jackson"])
注意到这实际上是先创建一个数组,然后将该数组放入集合中吗?这是有意为之,也是从固定数据创建集合的标准方法。请记住,集合会自动移除任何重复的值,而且不会记住数组中使用的确切顺序。
如果你想知道集合是如何排列数据的,可以试着打印出来:
print(people)
你可能会看到按原始顺序排列的名称,但也可能得到完全不同的顺序。集合并不关心其项目的顺序。
将项目添加到集合时的第二个重要区别在单独添加项目时也是可见的。下面是代码:
var people = Set<String>()
people.insert("Denzel Washington")
people.insert("Tom Cruise")
people.insert("Nicolas Cage")
people.insert("Samuel L Jackson")
注意到我们是如何使用 insert() 的了吗?当我们有一个字符串数组时,我们通过调用 append() 来添加项,但这个名称在这里没有意义–我们并没有在集合的末尾添加项,因为集合会按照它想要的任何顺序存储项。
现在,你可能会认为集合听起来就像简化了的数组,毕竟,如果不能有重复项,也不能改变项的顺序,那为什么不直接使用数组呢?其实,这两个限制都可以转化为优势。
首先,不存储重复项有时正是你想要的。在前面的例子中,我选择了演员,这是有原因的:美国演员工会(Screen Actors Guild)要求其所有成员都有一个唯一的艺名,以避免混淆,这就意味着绝不允许有重复的艺名。例如,演员迈克尔·基顿(《蜘蛛侠:英雄归来》、《玩具总动员 3》、《蝙蝠侠》等)其实名叫迈克尔-道格拉斯(Michael Douglas),但因为公会里已经有一个迈克尔-道格拉斯(《复仇者联盟》、《陨落》、《罗曼史》等),所以他必须有一个独一无二的名字。
其次,集合不会按照你指定的顺序存储项目,而是按照高度优化的顺序存储,这样就能很快找到项目。这其中的差别可不小:如果你有一个包含 1000 个电影名称的数组,并使用 contains() 来检查其中是否包含 “黑暗骑士”,Swift 需要遍历每一个条目,直到找到匹配的条目为止——这可能意味着在返回 false 之前要检查所有 1000 个电影名称,因为 “黑暗骑士 “不在数组中。
相比之下,在一个集合上调用 contains() 的运行速度非常快,你很难对它进行有意义的测量。即使集合中有一百万个条目,甚至一千万个条目,它也能立即运行,而数组可能需要几分钟或更长时间才能完成同样的工作。
大多数情况下,你会发现自己使用的是数组而不是集合,但有时,只是有时,你会发现集合正是解决特定问题的正确选择,它能让原本缓慢的代码瞬间运行起来。
提示:除了 contains() 之外,你还可以使用 count 来读取集合中的条目数,使用 sorted() 返回包含集合条目的排序数组。
为什么Swift中集合与数组不同?
集合和数组在 Swift 中都很重要,了解它们的区别有助于你了解在任何特定情况下应选择哪一种。
集合和数组都是数据集合,这意味着它们在单个变量中保存多个值。然而,它们如何保存值才是关键所在:集合是无序的,不能包含重复数据,而数组则保留了顺序,可以包含重复数据。
这两点区别看似很小,但却有一个有趣的副作用:因为集合不需要按照添加对象的顺序存储对象,它们可以按照看似随机的顺序存储对象,从而优化对象的快速检索。因此,当你说 “这个集合是否包含项目 X “时,无论集合有多大,你都能在一瞬间得到答案。
相比之下,数组必须按照你给出的顺序存储项,因此,要检查包含 10,000 个项的数组中是否存在项 X,Swift需要从第一个项开始,检查每一个项,直到找到为止,也可能根本找不到。
这种差异意味着,当你想说 “这个项目是否存在 “时,集合会更有用。例如,如果您想检查一个词是否出现在字典中,您应该使用集合:因为没有重复的词,而且你想快速查找。
枚举 – 如何创建和使用枚举
枚举 enum 是enumeration的缩写,它是一组命名的值,我们可以在代码中创建和使用它们。枚举对 Swift 没有任何特殊意义,但它更高效、更安全,因此你会在代码中经常使用它。
为了演示这个问题,假设你想编写一些代码,让用户选择一周中的某一天。一开始可以这样写:
var selected = "Monday"
之后在代码中进行修改,就像这样:
selected = "Tuesday"
这在非常简单的程序中可能会很有效,但请看看这段代码:
selected = "January"
哎呀!你不小心输入了 “月 “而不是 “日”——你的代码该怎么办?好吧,你可能会很幸运地让同事在查看你的代码时发现这个错误,但是这样如何呢?
selected = "Friday "
而在 Swift 看来,有空格的 “Friday “与没有空格的 “Friday “是不同的。同样,你的代码会怎么做呢?
使用字符串来处理这种事情需要非常谨慎的编程,而且效率很低——我们真的需要存储 “Friday”的所有字母来跟踪一天吗?
这就是枚举的用武之地:枚举可以让我们定义一种新的数据类型,并赋予它一些特定的值。想想布尔值,它只能有 “true “或 “false”——你不能将它设置为 “maybe “或 “probably”,因为这不在它能理解的值范围内。枚举也是一样:我们可以事先列出它的取值范围,Swift 会确保你在使用它们时不会出错。
因此,我们可以将工作日重写为一个新的枚举,就像下面这样:
enum Workday {
case monday
case tuesday
case wednesday
case thursday
case friday
}
这将调用新的枚举工作日,并提供五种情况来处理五个工作日。
现在,我们不再使用字符串,而是使用枚举。在你的playground中试试吧:
var day = Workday.monday
day = Workday.tuesday
day = Workday.friday
有了这一改动,你就不能在使用 “Friday “时不小心多留一个空格,或用月份名称代替——你必须始终选择枚举中列出的可能日子之一。当你输入 Workday 时,Swift 甚至会提供所有可能的选项,因为它知道你会选择其中一种情况。
Swift 做了两件事让枚举更容易使用。首先,当枚举中有很多情况时,你可以只写一次 case,然后用逗号分隔每个情况:
enum Workday {
case monday, tuesday, wednesday, thursday, friday
}
其次,请记住,一旦给变量或常量赋值,其数据类型就会固定下来——不能先把变量设为字符串,然后再设为整数。对于枚举来说,这意味着你可以在第一次赋值后跳过枚举名,就像下面这样:
var day = Workday.monday
day = .tuesday
day = .friday
Swift 知道 .tuesday 必须指向 Workday.tuesday,因为 day 必须始终是某种 Workday。
虽然在这里看不出来,但枚举的一个主要好处是 Swift 会以优化的形式存储枚举——当我们说 Workday.monday 时,Swift 很可能会使用单个整数(如 0)来存储,这比字母 M、o、n、d、a、y 的存储和检查效率要高得多。
为什么Swift需要枚举?
枚举是 Swift 的一项非常强大的功能,你会在很多地方看到枚举的应用。很多语言都没有枚举功能,但也能使用得很好,所以你可能会问 Swift 为什么需要枚举?
最简单来说,枚举就是为一个值取一个好听的名字。我们可以创建一个名为 “方向”的枚举,其中包括北、南、东和西,并在代码中引用这些值。当然,我们也可以用整数来代替,这样我们就可以引用 1、2、3 和 4,但你真的能记住 3 的含义吗?如果你不小心输入了 5 呢?
因此,枚举是我们用 Direction.north 来表示特定且安全含义的一种方式。如果我们写的是 Direction.thatWay,而不存在这种情况,Swift 会直接拒绝构建我们的代码——因为它不理解这种枚举的实例。在幕后,Swift 可以非常简单地存储枚举值,因此创建和存储枚举值要比字符串快得多。
随着学习的深入,你将了解 Swift 如何让我们为枚举添加更多的功能——在 Swift 中,枚举比我见过的任何其他语言都要强大。
好,至此我们完成了复杂数据类型的第一部份学习,下一章我们继续第二部份。