第4章 此字典非彼字典——数组和字典
Swift语言简化了集合的使用,在Swift语言中只提供了数组(Array)和字典(Dictionary)两种集合。分别用于管理单值集合和双值集合(key-value集合)。尽管在Swift语言中仍然可以使用cocoa library中的相应类型,如NSDictionary、NSArray等。但在实际应用中,应尽量避免使用 cocoa 中的类型,这是因为 Swift 中的内置类型提供了更强大的功能和错误校验机制。本章将会深入介绍这两种集合的功能和使用方法。
本章要点
□ 创建数组和字典
□ 初始化数组和字典
□ 创建空的数组和字典
□ 两个数组相加的规则
□ 获取和设置数组与字典的值
□ 向数组和字典中添加值
□ 枚举数组和字典中的值
□ 将字典中的keys和values转换为数组
4.1 数组(Array)
源代码文件:src/ch04/array/array/main.swift
Swift中的数组比其他语言中数组功能更强大。其他语言中的数组通常是一段连续的内存空间,可以快速地定位到任何一个数组元素,但不能对数组元素进行增加和删除操作。而Swift中的数组相当于数组和List的结合体,对数组中的元素进行增、删、改等操作非常容易。
4.1.1 创建和初始化数组
最简单的创建数组的方式是直接用数组初始值进行创建,这样创建和初始化数组就会同时完成。数组值用一对中括号([...])表示,数组值之间用逗号(,)分隔。例如,下面的代码创建了一个String类型的数组,并且为该数组初始化了两个数组元素,最后输出该数组中的元素值。
let firstArray = ["abc", "efg"]
println(firstArray) // 输出[abc, efg]
要注意的是,Swift中的数组如果声明为常量(let),不仅意味着不能修改数组本身,也不能修改数组中的元素值。这一点和其他语言中的数组常量还是有一定区别的。大多数语言将数组定义为常量,指标是数组本身不可改变,而数组元素还是可以改变的。
let array1 = ["abc", "efg"]
var array2 = ["abc", "efg"]
array1 = ["abc"] // 错误,不能修改数组本身
array1[0] = "a" // 错误,不能修改数组元素值
array2[1] = "xyz" // 正确,因为array2被声明为变量
Swift编译器是可以根据初始化时数组元素的类型推断数组类型的,例如,前面代码中的array1中的两个数组元素的类型都是String,所以array1是String类型数组。
我们还可以为数组指定类型,语法格式是Array<Type>,也可以简写为[Type ]。例如,下面的代码分别声明了typeArray1和typeArray2两个数组,其中typeArray1是String类型数组,typeArray2是Int类型数组。而且typeArray1只声明了一个数组,并未初始化。
var typeArray1:[String];
var typeArray2:Array<Int>= [1,2,3,4]
如果只是声明了数组,但并未初始化该数组,是不能被引用的。例如,typeArray1 是不能用下面的代码输出的。
println(typeArray1)
据官方文档解释 Swift 中的数组和 Objective-C 中的数组不同,在 Objective-C 中使用NSArray和NSMutableArray来处理数组,其中前者是只读数组,不可修改数组值,而后者是可变数组。不管是哪个数组,都可以存储任何类型的值(NSObject)。而Swift中的数组虽然也分为只读数组和可变数组,但对于同一个数组中的元素类型,必须都相同。
根据这个规则,如果在声明数组时指定了类型,毫无疑问,这个规则是适用的,例如,下面的代码是无法编译通过的。
// 由于最后一个数组元素类型是String,所以无法成功编译
var typeArray2:Array<Int>= [1,2,3,"xyz"]
如果在声明数组时不指定数据类型,好像这个规则并不适用,例如,下面的代码是可以编译通过的。
var myArray= [1,2,3,"xyz"] // 可以编译通过
虽然这行代码可以成功进行编译,但myArray的数据类型已经不再是Swift中的数组了。其实这是 Swift 编译器玩的一个魔法。当检测到数组中所有元素类型都相同时,就会将变量或常量类型设为 Swift 数组,如果发现元素类型不相同,那么就会将变量或常量类型设为NSArray。从这一点可以看出,Swift除了提供了自己的一套数据类型外,还可以使用CocoaLibrary中相应的类作为数据类型。当然,这一切都是Swift编译器弄的。
在默认情况下,myArray 的类型是 NSArray。也就是说,myArray 默认是只读的。如果想将myArray设为可变数组,并且每一个数组元素可以存储不同类型的值,那么就只用显示声明为NSMutableArray了。
var myArray:NSMutableArray= [1,2,3,"xyz"] // 可以编译通过
这样的话,就可以使用NSMutableArray的addObject方法以及其他方法修改myArray数组的值了。当然,如果数组中的元素类型都相同,建议还是使用 Swift 中的数组类型。因为数组类型中的元素类型是确定的,所以 Swift 编译器可以检查数组元素类型的正确性。而NSArray和NSMutableArray中的数组元素类型是NSObject,Swift编译器是不会检查这些数组元素的类型的,所以抛出运行时异常的可能性更高。
4.1.2 创建空数组
在上一节声明的数组里面都至少有一个数组元素。如果在初始化时不指定任何数组元素,那么Swift编译器会认为这是一个空的数组(数组元素个数为0的数组)。例如,下面的代码将创建一个空数组。
var nullArray = []
对于这行代码,Swift编译器并不能推导数组的类型,所以不会使用Swift自己的数组类型,而会创建一个NSArray对象,当然里面没有任何数组元素。
如果要创建Swift内置数组类型的空数组(需要指定数组元素的数据类型),可以使用下面3种形式。
var nullArray1 = [Int]() // 创建一个Int类型的空数组
var nullArray2:[String] = []; // 创建一个String类型的空数组
var nullArray3:Array<Float> = []; // 创建一个Float类型的空数组
4.1.3 创建固定长度的数组
初始化数组时还可以指定数组的初始长度,并且为所有的数组元素初始化同一个值。例如,下面的代码创建了一个初始化长度为4的Int类型的数组,并将这4个数组元素都初始化为2。
var fourInts = [Int](count:4, repeatedValue:2)
println(fourInts.count)
4.1.4 数组的加法
在 Swift 中指定数组的直接相加,数组相加也就是合并数组(将两个相加的数组前后连接)。例如,下面的代码将array1和array2相加得到arraySum,最后输出arraySum。
var array1 = ["a","d"]
var array2 = ["c","d"]
var arraySum = array1 + array2
println(arraySum) // 输出[a, b, c, b]
4.1.5 获取和设置数组元素值
Swift中的数组与其他语言中的数组在获取和设置数组元素值方面是相同的。不管是获取元素值,还是设置元素值,都可以使用 arrayName[index]形式。其中 arrayName 表示数组变量或常量名称,index 表示数组元素索引,该索引值从 0 开始。如果是设置数组元素值,arrayName只能是变量名。
var array1 = [1,2,3,4]
// 获取并输出第2个数组元素值
println(array1[1])
// 设置第3个数组元素的值
array1[2] = 20
println(array1)
4.1.6 数组区间赋值
Swift中的数组还支持使用闭区间操作符(...)和半闭半开操作符(..<)进行赋值,使用这样的赋值方法可以同时对多个数组元素进行赋值,就和初始化数组一样。赋值的语法规则如下。
arrayName[min...max] = [item1, item2, item3,...,itemn]
arrayName[min..<max] = [item1, item2, item3,...,itemn]
下面的代码是典型的区间赋值案例。
var products = ["iPhone4", "iPad2", "Nexus 5"]
products[0...2] = ["iPhone5", "iPad4", "Nexus 6"]
// 输出[iPhone5, iPad4, Nexus 6]
println(products)
既然是区间赋值,那么就可能出现区间的范围和右侧赋值的元素个数不同的情况,或者是max和min超过了数组的上下边界。如果是后者,那么会直接抛出异常。如果是前者,会分如下几种情况处理。
□ 用于赋值的元素个数超过了区间的范围,则追加或插入数组元素。例如,arrayName[0...1] = [item1,item2,item3]。很明显,item3是多余的。对于这种情况,会直接在arrayName[1]的后面追加一个item3。如果arrayName[1]后面还有元素,则插入。否则是最近。
□ 如果赋值的元素个数小于区间的范围,则删除多余的数组元素。例如,arrayName的初始值是[item1, item2,item3],使用arrayName[0...2] = [item1, item2],很明显,用于赋值的数组元素个数比区间范围少一个,所以arrayName[2]将被删除,最后arrayName的值是[item1, item2]
□ 如果区间赋值范围的min不是从0开始(如arrayName[1..2]),那么索引比min小的数组元素并不会被删除,而会被忽略(原封不动地放在哪,不去管它)。
下面几行代码演示了这几种区间赋值的情况,大家可以不看下面的答案,看看能不能猜出输出结果。
var products = ["iPhone4", "iPad2", "Nexus 5"]
products[0...2] = ["iPhone5", "iPad4", "Nexus 6"]
println(products)
products[0...1] = ["iPhone6", "iPad5", "Nexus 7"]
println(products)
products[0..<4] = ["iPhone6", "iPad5", "Nexus 7", "MBP", "iMac"]
println(products)
products[1...4] = ["iPhone7", "iPad5", "Nexus 7", "MBP"]
println(products)
products[0...4] = ["iPhone6", "iPad5"]
println(products)
运行这段代码,会输出如下的结果。
[iPhone5, iPad4, Nexus 6]
[iPhone6, iPad5, Nexus 7, Nexus 6]
[iPhone6, iPad5, Nexus 7, MBP, iMac]
[iPhone6, iPhone7, iPad5, Nexus 7, MBP]
[iPhone6, iPad5]
4.1.7 添加和删除数组元素
除了上一讲介绍的通过区间赋值可以添加和删除数组元素外,还可以通过一些方法添加和删除数组元素。添加数组元素的方法如下。
□ append:在数组末尾追加数组元素。
□ insert:在指定位置插入数组元素。
删除数组元素的方法如下。
□ removeLast:删除最后一个数组元素。
□ removeAtIndex:删除指定位置的数组元素。
□ removeAll:删除数组中所有的数组元素。
除了使用方法完成对数组元素的添加和删除外,还可以使用复合加法赋值(+=)来追加数组元素。如果追加一个数组元素,使用arrayName += item1形式,如果追加多个数组元素,可以使用arrayName += [item1, item2,...itemn]形式。
数组还提供了一些方法来判断数组中是否有元素,还有多少个元素。例如,下面两个方法分别用来判断数组是否为空,以及数组中的元素个数。
□ empty:判断数组是否为空。
□ count:返回数组中元素的个数。
下面是使用这几个方法添加和删除数组元素代码。
var cities = ["沈阳", "北京"]
// 在数组的最后追加一个数组元素
cities.append("上海")
// 输出结果:[沈阳, 北京, 上海]
println(cities)
cities.insert("广州", atIndex:1)
// 输出结果:[沈阳, 广州, 北京, 上海]
println(cities)
// 删除最后一个数组元素
cities.removeLast();
// 输出结果:[沈阳, 广州, 北京]
println(cities)
// 删除第2个数组元素
cities.removeAtIndex(1)
// 输出结果:[沈阳, 北京]
println(cities)
// 输出结果:2
println(cities.count)
// 清除所有的数组元素
cities.removeAll(keepCapacity: true)
// 判断cities数组是否为空(没有任何数组元素)
if cities.isEmpty
{
println("数组为空")
}
// 追加一个数组元素
cities += "兰州"
// 追加两个数组元素
cities += ["西安","云南"]
// 输出结果:[兰州, 西安, 云南]
println(cities)
4.1.8 枚举数组中的所有元素
枚举数组元素的方法通常可以使用如下两种方法。
□ 从数组的第 1 个元素循环到数组的最后一个元素,然后利用索引获取每一个数组元素。
□ 将数组当成一个集合,然后枚举集合中的所有元素。
如果采用第一种方法,可以指定枚举的访问,也就是数组的区间(min...max)。而使用第二种方法,只能枚举数组中的所有元素。
下面是使用这两种方法枚举数组中所有元素的代码。
let provinces = ["辽宁","广东","福建"]
var i = 0
// 使用第一种方法输出当前数组的索引和数组元素值
for i in 0..<provinces.count
{
print("\(i):{" + provinces[i] + "}")
}
println()
// 使用第二种方法输出数组中所有的元素值
for value in provinces
{
print("<"+value+">")
}
println()
// 使用第二种方法输出数组中所有的元素值和当前的数组索引
// 这里使用了一个全局的enumerate函数,用于获取存储数组索引和数组元素的结构体(struct)
// 这样就可以同时获取数组索引和数组元素了。关于struct的详细内容会在后面的章节中介绍
for (index, value) in enumerate(provinces)
{
println("数组索引:\(index) 数组元素:\(value)")
}
4.2 字典(Dictionary)
源代码文件:src/ch04/dictionary/dictionary/main.swift
字典和 Java 中的 Map 类似,用于存储 key-value 类型的数据,不过功能比传统的 Map更强大。本节将详细介绍字典的各种使用方法。
4.2.1 创建和初始化字典
在Swift中,字典使用Dictionary结构体表示,所以可以直接使用Dictionary创建一个字典,并且在创建的过程中要指定key和value 的数据类型(如Dictionary<String, String>),一旦指定,在向字典添加key-value数据时类型就必须一致了。所以Dictionary和Array一样。key和value的数据类型在创建字典之初就已经指定了,以后都不能更改。
在初始化字典时和数字一样,要使用一对中括号([...])。只是写法和JSON定义数组的样式类似。key-value 之间用逗号( , )分隔,key 和 value 之间用冒号分隔(如[key1:value1,key2:value2])。但有一点是不同的,在JSON中,所有的key和value都必须用双引号括起来,而字典的key和value如果不是String类型,就不需要使用双引号括起来,该使用什么类型的值,就是用什么类型的值。
如果要利用 Swift 的推导功能,可以省略 Dictionary(但指定类型时要改用方括号,如[String, String]),甚至可以不指定类型,只进行初始化。下面是这 3 种创建和初始化字典的方式。
// 标准的创建和初始化字典的方法
var employee1: Dictionary<String,String> = ["name":"bill", "company":"Microsoft"]
// 省略了Dictionary
var employee2: [String: String] = ["name":"bill", "company":"Microsoft"]
// 直接进行初始化
var employee3 = ["name":"bill", "company":"Microsoft"]
如果不指定key-value的数据类型,并且初始化时每一对key-value值的类型不一致,那么Swift编译器将会认为该字典变量或常量是NSDictionary类型。
// employee不是Swift中的字典类型,而是NSDictionary类型
var employee = ["name":"bill", "age":50]
如果想使用可修改的字典对象(NSMutableDictionary),可以直接将常量或变量声明为NSMutableDictionary类型。
var employee:NSMutableDictionary = ["name":"bill", "age":50]
注意
字典与数组一样,如果使用 let 声明,不仅意味着不能修改字典本身,连字典中的值也不能修改。
4.2.2 创建空的字典
空字典就是没有任何数据的字典。创建一个空的字典有如下两种方法。
// 方法1
var nullDict1 = Dictionary<Int, String>()
// 方法2
var nullDict2 = [Int: String]()
如果向空字典中添加数据,那么再将字典设为[:],就可以再次清空字典。
var nullDict = Dictionary<Int, String>()
nullDict[10] = "iPhone6"
nullDict = [:] //再次清空字典
不过要注意,不能在初始化字典时使用[:],否则 Swift编译器无法推导key和value的数据类型,这时就会认为nullDict是NSDictionary类型。
4.2.3 添加、修改和删除字典中的数据
向字典中添加数据可以使用类似于数组的方式,也就是dictionaryName[key] = value。如果key在字典中不存在,那么就会将这个key-value对添加到字典中;如果key存在,就是修改原来的value了。例如,下面的几行代码分别向product和person中添加了相应的数据。
var product = ["name":"iPhone", "company":"apple"]
var person = [Int:String]()
product["time"] = "2014-1-1"
person[10] = "Bill"
person[20] = "Mary"
如果想修改字典中的值,除了使用 dictionaryName 的形式外,还可以使用 updateValue方法,该方法返回了修改之前的值,具体代码如下。
if let oldValue = person.updateValue("John", forKey:10)
{
println("旧的值是\(oldValue)")
}
如果想更方便地获取修改之前的值,可以考虑使用updateValue方法。
删除字典中的一个key-value对,可以将value设为nil即可。例如,下面的代码删除了person中key的数据。
person[20] = nil
删除数据还可以使用 removeValueForKey 方法,该方法需要指定要删除数据的 key,并且返回待删除数据中的value。下面的代码将删除person中key为10的数据,并且返回相应的value。
let oldValue = person.removeValueForKey(10)
println(oldValue)
// 输出当前person中的key-value个数(应为0)
println(person.count)
4.2.4 获取字典中的值
如果想根据key获取value,只需要使用dictionaryName[key]即可。如果使用这种形式赋值,那么当key存在时就是修改value;如果key不存在,就会在字典中追加一条key-value。但使用这些形式获取value时,如果key存在,则会直接返回与key对应的value;如果key不存在,则会返回nil。
var persons = [10:"Bill", 20:"Mike"]
// 输出Bill
println(persons[10])
// 输出nil
println(persons[30])
4.2.5 将value转换为指定的类型值
有时我们需要获取与value原来类型不同的类型值。例如,value是String类型,在获取时,value 正好可以转换为 Int 类型,所以为了方便,在获取时需要将String 类型转换为 Int类型的值。另外,如果字典中的value的数据类型不同时,字典就会变成NSDictionary类型,这时每一个value都会变成NSObject。所以在获取value时,通常需要将这些value转换为相应的数据类型。下面的代码演示了如何在不同类型之间进行转换。
var myDict1 = ["key1":"20", "key2":"abc"]
// String转换为Int类型的值,转换时需要使用可选类型变量(类型后面加?)
var value1:Int? = myDict1["key1"]?.toInt()
var myDict2 = ["key1": "200", "key2": 30]
// 将原本是Int类型的NSObject类型值转换为Int类型的值,这时不需要使用可选类型变量
var value2:Int = myDict2["key2"] as Int
// 将原本是String类型的NSObject类型值转换为Int类型的值
var value3:Int? = (myDict2["key1"] as? String)?.toInt()
println(value1)
println(value2)
println(value3)
注意
经过测试,发现String类型的值不能直接使用as关键字转换为Int类型的值,而且也无法编译通过。除此之外,原类型是String的NSObject类型值尽管使用as关键字可以转换为Int类型,也可以成功编译。但运行时会抛出异常,这可能是Swift编译器的一个 Bug。大家在使用时要注意。例如,前面代码中的 myDict2["key1"]不能使用as关键字转换为Int类型的值,而需要先转换为String,然后再使用toInt方法转换为Int类型的值。
4.2.6 枚举字典中的key和value
在Swift中可以很容易地获取字典中的所有key和value。我们可以同时获取key和与其相对应的value,也可以单独获取key或value。通常获取字典中所有的key和value,可以使用for循环。如果同时获取key和value,可以使用下面的形式。
for (key,value) in dictionaryName
{
// key和value就是当前key-value对中的key和value
... ...
}
如果要分别获取key和value,可以使用字典的keys和values属性。
for key in dictionaryName.keys
{
... ...
}
for value in dictionaryName.values
{
... ...
}
当字典类型变成了 NSDictionary,可以先将其类型转换为 Dictionary,然后再进行使用keys和values属性获取相应的key和value。
for key in (dictionaryName as Dictionary).keys
{
... ...
}
下面是一个完整的演示各种情况获取字典中key和value的代码。
var products = [20:"iPhone", 30:"Nexus 6", 40:"iPad5"]
for (key,value) in products
{
println("\(key):\(value)")
}
for key in products.keys
{
println(key)
}
for values in products.values
{
println(values)
}
// city为NSDictionary类型
var city = [10:"沈阳", "province":"辽宁"]
for key in (city as Dictionary).keys
{
println(key)
}
4.2.7 将keys和values转换为数组
有时我们需要将keys和values单独拿出来使用。在这种情况下,将keys和values分别转换为数组(Array)是最好的方法。通常将字典中的keys和values分别转换为数组可以使用下面两个方法。
var 行星 = ["名称":"地球", "位置":"太阳系第3行星"]
// 方法1
let keys = Array(行星.keys)
println(keys)
// 方法2
let values = [String](行星.values)
println(values)
4.3 小结
Swift中的数组和字典用处很多,而且使用非常简单。例如,可以利用字典存储数据库中的一条记录。我们会发现,在后面的很多例子中,会使用到数组和字典,因此,建议读者仔细研究数组和字典的各种细节,以免在后面的学习中不知所措。