Swift中常用的数据结构(上)

Swift是一门功能强大的编程语言,不过,如果没有强大的标准库与之相匹配,功能再强大也都是徒劳。Swift标准库中定义了可用于编写应用程序的基本功能层,它包括基本数据类型、集合类型、函数和方法,以及大量的协议。

接下来,我们将研究一下Swift标准库中的集合类型,它主要包括数组(Array)、字典(Dictionary)与集合(Set),顺便研究一下元组(Tuple)。

一、Swift标准库中常用的集合类型

如果你有使用Objective-C、C++或者Python等其它面向对象的编程语言,你可能更习惯于使用类来定义结构体类型。但是在Swift中,通常使用结构体来定义标准库中的绝大多数类型。为什么这样做呢?这主要是因为,与类比起来,结构体的功能有限,使用结构体来创建标准库比使用类更加安全。因为在Swift中结构体是值类型,它只能有一个拥有者,在将其赋值给新的变量或者作为参数传递给函数时,它始终是通过复制来实现的。 这样做可以保证代码本身更加安全,因为对结构体的更改不会影响应用程序的其它部分。

在Swift中,结构体使用起来和类非常相似,其功能远比其它基于C语言而发展起来的编程语言中的结构体强大得多。Swift中的结构体主要有以下几个特点:

  • 除了拥有成员之外,结构体中还可以拥有方法;
  • 在结构体中还可以实现协议;

这样一来,你可能就要问了,那么什么时候使用结构体,什么时候使用类呢?关于这个问题,苹果官方文档中有详细的说明,只要是符合以下一个或者是多个条件的,都不要使用类而是使用结构体:

  • 你的主要目的是封装一些简单的数据;
  • 你希望把封装的数据赋值给其它变量,或者将其作为参数传递给函数时,是通过复制而不是引用来实现的;
  • 不需要使用继承关系。也就是说,如果你不希望从其它类型中继承属性或者方法时,最好是使用结构体;
  • 所有存储在结构体中的属性都是值类型。

除了上述几种情况之外,你都应该使用类。类实例在调用的时候,都是通过引用的方式来实现的。在对结构体进行简单的了解之后,下面我们就具体的操作一下。

(一)、Array

在学习Objective-C的时候我们知道,数组是有序列表。但是相比而言,Swift中的数组和Objective-C中的数组还是有很多不同的地方:

  • 首先,在Swift中,数组(Array)中存储的元素类型必须一致
  • 其次,在Objective-C中,数组(NSArray)中存储的元素必须是类对象,而在Swift中,只要类型一致,存储什么类型的元素都可以,比如说整型、浮点型、字符串儿、枚举或者是类对象;
  • 最后,与Objective-C中的数组不同,Swift数组是结构体而不是类。

Swift中有三种数组类型,分别是ArrayContiguousArrayArraySlice,每一种数组都有一个用来保存数组元素的连续内存区块,那么它们之间有什么联系或者区别呢?主要有以下几点:

  • 如果你准备在数组中存储类型为类或者@objc协议类型的元素,使用ContiguousArray效率可能会更高;
  • 大多数Array所能支持的属性,ContiguousArray同样也能支持,因为ContiguousArray和Array共享很多协议;
  • ContiguousArray和Array之间最主要的不同是,ContiguousArray不支持和Objective-C之间的桥接;
  • ArraySlice是Array、ContiguousArray,或者其它ArraySlice的子序列。和ContiguousArray一样,ArraySlice同样也不支持与Objective-C之间的桥接;
  • 正因为ArraySlice是表示一个已经存在的、更大的数组的子序列,所以在使用ArraySlice的时候一定要特别注意。如果原始数组的生命周期已经结束,并且不能再访问元素时,使用ArraySlice可能会出现内存泄漏。因此,ArraySlice并不适用于长期存储数据的场合。

当你创建一个Array、ContiguousArray,或者ArraySlice时,需要分配一定的空间来存储数组元素,而且这个空间容量必须保证能容纳潜在的数据量,最好不要出现因存储空间不够而需要重新分配的问题。不过好在Swift非常强大,一般不需要你手动去完成这一过程。Swift中有一个指数增长策略,当你把数组元素添加到数组中,并且出现存储空间不足时,数组将会自动调整存储空间的大小。当然,如果你在使用数组之前就已经知道将要存储大量的元素,手动分配一个足够大的存储空间,代码运行的效率可能会更高。因为这样可以有效避免不断的重新分配,减少系统消耗。下面我将通过代码来演示如何手动分配数组的存储空间:

// 完整声明一个数组
var myIntArr = Array<Int>()

// 快速声明一个数组
var myOtherIntArr = [Int]()

// 查看数组的容量
myIntArr.capacity
myOtherIntArr.capacity

// 手动分配数组的存储容量
myIntArr.reserveCapacity(100)
myOtherIntArr.reserveCapacity(1000)

// 再次查看数组的容量
myIntArr.capacity
myOtherIntArr.capacity

需要说明的是,在手动申请内存空间时,系统返回给你的数量并不一定等同于你申请的数量。出于性能上的考虑,系统返回给你的空间容量总是要大于或者等于你的索求,因为这样一定能保证满足你的存储需求:

手动申请存储空间.png

还需要知道的一点是,当你复制一个数组时,在使用之前并不会立即生成一个物理上的副本。Swift中有一个叫做copy-on-write(写时复制)的特性,当多个数组共享同一个缓冲区时,数组元素是不会被复制的,除非执行了mutating操作(后面会讲到)。mutating操作在时间和空间上的消耗是O(n),其中n是数组的长度。

1、数组的初始化

通过上面的学习,我们对Swift中的数组有一个大致的了解,下面我们就来学习一下Swift中数组的初始化。初始化一般是指通过一个叫做init的方法来创建一个struct、class或者enum,以供后续使用。在Objective-C中,init方法会直接返回它初始化的对象,然后调用者在初始化一个类时,会检查返回值是否为nil,以及初始化进程是否失败。而在Swift中,这种类似的行为被称之为可失败构造器(Failable Initializers)。对于Swift标准库中提供的三种数组类型,我们有四种方式来对它们进行初始化:

// 使用完整的语法来初始化一个数组
var intArray = Array<Int>()

// 使用快捷方式来初始化一个数组
var floatArray = [Float]()

// 使用类型推断来初始化一个数组
var doubleArray = [1.414, 1.732, 2.000, 2.236]

// 初始化数组时给它一个默认的值
var stringArray = [String](repeatElement("LeBron James", count: 3))

最后一种初始化方式主要使用在数组中元素相同的场合,其中参数count表示元素的个数。代码运行情况如下图所示:

Swift中数组的初始化.png

2、添加或者更新一个数组元素

如果要往数组中添加一个元素,可以使用append()方法。此操作会将一个新元素添加到数组的末尾:

// 比如说已经存在这样一个数组
var myYearArr = [2013, 2014, 2015, 2016]  // [2013, 2014, 2015, 2016]

// 添加一个元素到intArray中
myYearArr.append(2017)  // [2013, 2014, 2015, 2016, 2017]

除了可以添加单个元素之外,其实也可以添加一个数组,同样还是使用append方法,不过所传的参数不一样:

// 添加一个数组到原始数组中
myYearArr.append(contentsOf: [2018, 2019,2020])  // [2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020]

如果你准备往数组中插入一个元素,可以使用insert(newElement: at:)方法,但是不要越界

// 在数组中插入一个元素
myYearArr.insert(2012, at: 3)  // [2013, 2014, 2015, 2012, 2016, 2017, 2018, 2019, 2020]

如果你准备更新数组中已存在的某个元素,可以直接使用下标。比如说,我们准备将下标为3(也就是元素值为2012)的那个元素替换成0:

// 更新数组中某个元素原来的值
myYearArr[3] = 0  // [2013, 2014, 2015, 0, 2016, 2017, 2018, 2019, 2020]

3、检索或者删除数组中的元素

检索数组中元素的方法有很多种,如果你知道数组中某个元素的下标,或者说数组中某几个相邻元素的下标范围,可以使用下面的方法进行检索:

// 检索数组中下标为5的元素
myYearArr[5]  // 2017

// 检索数组中下标值从2到6(不包含下标值为6)的元素
myYearArr[2..<6]  // [2015, 0, 2016, 2017]

// 检索数组中下标值从2到6(包含下标值为6)的元素
myYearArr[2...6]  // [2015, 0, 2016, 2017, 2018]

还可以通过for...in遍历的方式来检索数组中的每一个元素:

// 遍历数组中的每一个元素
for xxx in myYearArr {
    // 打印叉叉叉
    print(xxx)
}

除此之外,还可以检查数组中是否包含指定元素:

// 检查数组中是否包含某一个元素
myYearArr.contains(2013)  // 返回true,也就是包含2013这个元素
myYearArr.contains(2046)  // 返回false,也就是不包含2046这个元素

上面只是列出了数组中常用的一些方法,如果你还想了解更多相关的知识,可以查看与数组有关的官方文档

(二)、Dictionary

字典是一个无序集合,它里面存储的是相互关联的键值对,并且所有键值对都是同一种类型。字典中值所对应的键是独一无二的,是它在字典中唯一的身份标识。字典中的键必须遵守Hashable协议

1、检索和初始化字典

和数组一样,有两种常用的方法可以声明一个字典,即完整的声明方法和快速声明方法:

// 使用完整的语法声明一个字典
var myDict = Dictionary<Int, String>()

// 快速声明一个字典
var yourDict = [Int : String]()

除此之外,还可以使用类型推断来声明一个字典。不过,在具体操作时一定要注意,字典的键和值之间用冒号分割,键值对之间用逗号分割,并且最重要的是,所有键值对的类型都必须连贯,否则编译器就会懵逼:

// 利用类型推断声明一个字典
var provenceDict = [1 : "Shanghai", 2 : "Hubei", 3 : "Jiangsu"]

当然,如果键值对的类型不连贯也是可以的,只是要多做一个操作,需要将对应的键转成AnyHashable,或者需要将对应的值转成Any:

// 键的类型不一致([AnyHashable : String])
var provenceDict1 = [1 : "Shanghai", 2 : "Hubei", "3" : "Jiangsu"] as [AnyHashable : String]

// 值的类型不一致([Int : Any])
var provenceDict2 = [1 : "Shanghai", 2 : 2017, 3 : "Jiangsu"] as [Int : Any]

// 键和值的类型都不一致([AnyHashable : Any])
var provenceDict3 = [1 : "Shanghai", "2" : "Hubei", 3 : 2017] as [AnyHashable : Any]

2、添加、修改或者删除键值对

要添加或者修改一个键值对,可以使用updateValue(value: forKey: )方法。如果你所添加的那个键在字典中不存在,那么这一操作就会向字典中添加一个新的键值对;如果你所添加的那个键已经在字典中存在,那么这一操作会修改原来字典中那个键所对应的值:

// 向字典中添加一个键值对
provenceDict.updateValue("Beijing", forKey: 7)  // [7: "Beijing", 2: "Hubei", 3: "Jiangsu", 1: "Shanghai"]
// 修改字典中的键值对
provenceDict.updateValue("Zhejiang", forKey: 1)  // [7: "Beijing", 2: "Hubei", 3: "Jiangsu", 1: "Zhejiang"]

除了上面这种方法之外,使用下标操作同样可以向一个字典中添加一个元素,或者修改字典中已经存在的键值对:

// 使用下标操作添加一个键值对
provenceDict[11] = "Guangdong"  // [7: "Beijing", 2: "Hubei", 3: "Jiangsu", 1: "Zhejiang", 11: "Guangdong"]

// 使用下标操作修改一个键值对
provenceDict[7] = "Fujian"  // [7: "Fujian", 2: "Hubei", 3: "Jiangsu", 1: "Zhejiang", 11: "Guangdong"]

最后需要注意,updateValue(value: forKey: )方法和下标操作之间还是有区别的。如果是向字典中添加一个新的键值对,updateValue(value: forKey: )方法会返回nil;如果是修改字典中已经存在的键值对,updateValue(value: forKey: )方法会返回新添加键值对的值。

如果要删除字典中已经存在的键值对,可以使用removeValue(forKey:)方法,或者用下标操作将字典中要删除的键值对的值置为nil:

// 使用removeValue(forKey:)方法删除字典中的键值对
provenceDict.removeValue(forKey: 2)  // [7: "Fujian", 3: "Jiangsu", 1: "Zhejiang", 11: "Guangdong"]

// 使用下标操作删除字典中的键值对
provenceDict[1] = nil  // [7: "Fujian", 3: "Jiangsu", 11: "Guangdong"]

与下标操作法不同,removeValue(forKey:)方法会返回字典中被删除键值对的值,或者该键值对不存在时,会直接返回nil。

3、检索字典中的键值对

你可以使用下标操作来检索字典中制定的键值对,而这个键值对可能存在,也可能不存在,所以最终返回的结果是一个可选类型。在实际开发时,你可以使用可选绑定或强制解包来检索并判断该键值对是否存在:

// 使用可选绑定来检索字典中的键值对
if let xxx = provenceDict[3] {
    print(xxx)
} else {
    print("该键值对并不存在!")
}

// 使用强制解包来检索键值对
let xxxxx = provenceDict[7]!
print(xxxxx)

特别注意,只有当你确定字典中一定存在你检索的那个键值对时,才能使用强制解包!如果该键值对不存在,强制解包会导致运行时异常!所以,平常最好习惯用可选绑定。

除了使用上述两种方法从字典中获取特定的值之外,其实还可以通过遍历的方式从字典中返回一个(Key, Value)元组:

// 初始化一个字典
var provences = ["沪" : "上海", "京" : "北京", "鄂" : "湖北", "湘" : "湖南", "粤" : "广东"]

// 遍历字典
for (provenceAbbr, provenceName) in provences {
    print("\(provenceName)的简称是\(provenceAbbr).")
}

在遍历过程中,如果你只想检索字典中的键或者值,同样也是可以的,只需要使用字典(准确来说是LazyMapCollection)的keys或则values属性就可以了:

// 只遍历字典中的键
for provenceAbbr in provences.keys {
    print("字典中各省份的简称是:\(provenceAbbr)")
}

// 只遍历字典中的值
for provenceName in provences.values {
    print("字典中的省份有:\(provenceName)")
}

我们在一开始就说过,字典中存储的键值对是无序的,所以遍历出来的结果可能是随机的。但是,在实际应用中,你可能希望遍历出来的结果能够按照某种顺序重新排序。针对这种情况,可以考虑使用全局排序方法sorted(by: )。该方法会返回一个已经排序的数组,其中数组中的元素就是这个字典:

// 初始化一个字典
var provences = ["SH" : "Shanghai", "BJ" : "Beijing", "HB" : "Hubei", "HN" : "Hunan", "GD" : "Guangdong"]

// 只遍历字典中的键
for provenceAbbr in provences.keys {
    print("字典中各省份的简称是:\(provenceAbbr)")
}

// 遍历出来的结果如下:
字典中各省份的简称是:SH
字典中各省份的简称是:BJ
字典中各省份的简称是:HN
字典中各省份的简称是:GD
字典中各省份的简称是:HB

// 对字典中的键值对进行重新排序,然后返回一个数组
let sortedArrFromDict = provences.sorted(by: {$0.0 < $1.0})

// 遍历数组
for (key) in sortedArrFromDict.map({$0.0}) {
    print("字典中的键分别为:\(key)")
}

// 遍历出来的结果如下:
字典中的键分别为:BJ
字典中的键分别为:GD
字典中的键分别为:HB
字典中的键分别为:HN
字典中的键分别为:SH

因为汉字无法体现出排序的效果,所以我将字典里面的键值对都换成了拼音。从遍历的结果来看,它实际上是按照A~Z的顺序进行重新排序的。在上面sorted(by: )方法中,我们给它传递了一个尾随闭包。关于闭包的相关知识,可以查阅官方文档中与闭包相关的基础知识。

(三)、Set

Set也是一种无序的集合,并且它非常的独特,里面存储的元素不能为nil。存储在Set中的元素类型必须遵守Hashable协议。因为Hashable协议本身又是遵守Equatable协议协议的,所以你只要保证遵守Hashable和Equatable协议,完全可以在Set中自定义元素的类型。还有,如果你能保证存储在Set中的元素不会重复,并且元素的排序不是那么重要,可以在任何使用Array的场合使用Set。另外,相比而言,访问Set的效率要比访问Array的效率高很多。在Array中,最坏的情况下访问一个元素需要的时间是O(n),而访问Set中的一个元素,其所需要的时间始终是常量O(1)。当然,这仅仅是指时间效率,并不考虑空间效率。

1、初始化一个Set

与其它集合类型不同,Set不能直接使用类型推断来进行声明。在声明一个Set的时候,必须明确指定Set元素的类型。但是,因为数组在声明的时候可以使用类型推断,所以你可以借助数组来实现类型推断:

// 使用完整的语法来初始化一个Set
var intSet = Set<Int>()

// 通过数组来实现Set的类型推断
var stringSet : Set = ["LeBron James", "Carmelo Anthony", "Dwyane Wade"]

对于借助数组来实现Set类型推断这种方法,如果有疑问,可以按住option键,然后点击变量名stringSet进行查看,可以清楚的显示它是Set<String>。另外,还可以打印stringSet.debugDescription在控制台进行查看。

2、修改或者检索Set中的元素

检索或者修改Set中元素的操作,和前面其它几个集合类型的相关操作差不多,主要分为以下几种情况:添加一个元素使用insert( )方法;查看Set中是否包含某个元素使用contains()方法,返回值为true是false;从Set中删除一个元素方法有多种,如果你知道要删除元素的值,可以使用remove()方法,如果你知道要删除元素的下标,可以使用remove(at: ),如果Set中元素的个数不为空,可以使用removeFirst()删除第一个元素,如果你想删除全部元素,可以使用removeAll()或者removeAll(keepingCapacity: ):

// 通过数组来实现Set的类型推断
var stringSet : Set = ["LeBron James", "Carmelo Anthony", "Dwyane Wade", "Kevin Durant", "James Harden", "Kawhi Leonard"]

// 添加一个元素
stringSet.insert("Chris Paul")

// 检查Set中是否包含某个元素
stringSet.contains("Russell Westbrook")  // 返回false

// 删除Set中自定的元素
stringSet.remove("Carmelo Anthony")

// 通过下标删除Set中的某个元素
if let index = stringSet.index(of: "Dwyane Wade") {
    stringSet.remove(at: index)
}

// 删除Set中第一个元素
stringSet.removeFirst()

// 删除Set中所有的元素
stringSet.removeAll()
stringSet.removeAll(keepingCapacity: true)

方法removeAll(keepingCapacity: )中的参数不管是true还是false,Set中的元素最终都会被删除,它们之间的区别是,如果设置为true,则表示Set集合缓冲区的容量将会被保留;如果设置为false,则表示Set集合缓冲区的容量将会被释放。另外,你也可以使用for...in来遍历Set:

// 遍历stringSet
for name in stringSet {
    print(name)
}

// 排序并遍历stringSet
for name in stringSet.sorted() {
    print(name)
}

3、比较操作

Swift提供了4种方法来操作两个Set集合,这些操作都会返回一个新的Set,并且新Set中的元素都和原来两个Set中的元素存在某种联系,具体情况如下:

  • union()方法会创建一个新的Set,并且新Set中的元素来自原来两个Set的并集;
  • intersection()方法创建一个新的Set,并且新Set中的元素来自原来两个Set的交集;
  • symmetricDifference()方法会创建一个新的Set,并且新Set中的元素来自原来两个Set的并集再减去它们之间的交集;
  • subtracting()方法会创建一个新的Set,并且新Set中的元素来自原来两个Set的差集。

关于上面这四个方法,分别有一个formUnion()、formIntersection()、formSymmetricDifference()和subtract()与之对应,功能和使用一模一样,但是在Swift 3中已经过期了。下面通过代码来演示一下这几个方法具体如何使用:

// 李雷喜欢吃的东西比较多
let liLei : Set = ["面条", "水饺", "大米", "猪肉", "鱼", "鸡", "青菜", "茄子",]

// 韩梅梅喜欢吃的东西比较少
let hanMeimei : Set = ["萝卜", "青菜", "茄子", "玉米", "水饺"]

// Lily减肥,喜欢吃的东西更少
let lilyKing : Set = ["牛肉", "青菜", "茄子", "面包"]

// JimGreen是广东人,什么都吃(李雷和Lily的并集)
let jimGreen : Set = liLei.union(lilyKing)

// Lucy还不会点菜,只吃李雷和韩梅梅都喜欢吃的东西(李雷和韩梅梅的交集)
let lucyKing : Set = liLei.intersection(hanMeimei)

// 除了韩梅梅和JimGreen同时喜欢吃的东西之外,只要是韩梅梅和JimGreen喜欢吃的东西,她都喜欢吃(JimGreen和韩梅梅的并集再减去他们的交集)
let kateGreen : Set = jimGreen.symmetricDifference(hanMeimei)

// 林涛喜欢JimGreen喜欢吃但是韩梅梅不喜欢吃的东西(JimGreen和韩梅梅的差集)
let linTao : Set = jimGreen.subtracting(hanMeimei)

4、相等操作与成员操作

如果两个Set中的元素全部相同,那么我们就可以认为这两个Set是相等的。我们可以使用"=="操作符来判断两个Set是否相等:

// 好基友
let bromance : Set = ["刘备", "关羽", "张飞"]

// 五虎上将
let fiveTigerGeneral : Set = ["关羽", "张飞", "赵云", "黄忠", "马超"]

// 上五虎将
let fuckFive : Set = ["黄忠", "马超", "张飞","关羽","赵云"]

// 判断好基友和五虎上将是否相等
let isEqual = bromance == fiveTigerGeneral  // 返回false

// 判断五虎上将和上五虎将是不是相等
let equalIs = fiveTigerGeneral == fuckFive  // 返回true

相等操作比较简单,成员操作稍微麻烦一点,因为它的方法比较多,主要有下面这几种:

  • isSubset(of:):这个方法用来判断一个Set中的元素是否全部包含在另一个指定Set中;
  • isStrictSubset(of:):这个方法用来判断一个Set是否是指定Set的子序列,但是它又不等于指定的Set;
  • isSuperset(of:):这个方法用来判断一个Set是否包含指定Set中所有的元素;
  • isStrictSuperset(of:):这个方法用来判断一个Set是否是指定Set的超集,但是它又不等于指定的Set;
    isDisjoint(with:):这个方法用来判断两个指定的Set是否不包含相同的元素。

关于上面这几个方法,具体的使用示例如下:

// 好基友
let bromance : Set = ["刘备", "关羽", "张飞"]

// 基友好
let ecnamorb : Set = ["张飞", "刘备", "关羽"]

// 刘备的员工
let employee : Set = ["关羽", "张飞"]

// 五虎上将
let fiveTigerGeneral : Set = ["关羽", "张飞", "赵云", "黄忠", "马超"]

// 上五虎将
let fuckFive : Set = ["黄忠", "马超", "张飞","关羽","赵云"]

// 曹操集团
let cao : Set = ["曹操", "荀彧", "郭嘉", "徐晃", "张辽"]

// 判断好基友是否是基友好的子序列(也即是基友好是否包含好基友全部的元素)
if bromance.isSubset(of: ecnamorb) {
    print("true")
} else {
    print("false")
}  // 结果为true

// 判断employee是否是bromance的子集,但是又不等于bromance
if employee.isStrictSubset(of: bromance) {
    print("true")
} else {
    print("false")
}  // 结果为true

// 判断五虎上将是否是上五虎将的超集
if fiveTigerGeneral.isSuperset(of: fuckFive) {
    print("true")
} else {
    print("false")
}  // 结果为true

// 判断bromance是否是employee的超集,但是又不等于employee
if bromance.isStrictSuperset(of: employee) {
    print("ture")
} else {
    print("false")
}  // 结果为true

// 判断五虎上将和曹操集团是否有瓜葛
if fiveTigerGeneral.isDisjoint(with: cao) {
    print("true")
} else {
    print("false")
}  // 结果为true

(四)、元组(Tuple)

元组是Swift中一种比较高级的类型,在Objective-C中是没有的。元组与Array、Dictionary和Set不同,它不是集合类型。但是,元组又与集合类型有着诸多相似之处。元组中可以像集合类型那样,存储一个或者多个元素,但是与集合类型所不同的是,元组中存储的元素不要求类型一致。因为元组不是集合类型,所以它没有遵守SequenceType协议,因此你不能像遍历集合类型那样遍历元组。

元组非常适合用来存储和传递一组数据。当你希望将方法中的多个值当成单个值返回,并且又不想新建一个结构体时,那么元组是非常好的选择。但是,元组的使用也是有局限性的,它不适合用来创建复杂的数据结构。尤其是当你希望创建持续性的数据结构时,模型、类或者结构体可能比元组更加适合。

1、匿名元组(Unnamed Tuples)

匿名元组的概念其实是相对于命名元组(Named Tuples)来说的,等后面讲到命名元组的时候,你就知道什么是匿名元组了。在创建元组时,里面的元素可以是任何类型,下面我们就创建一个包含Double、Int、String和UIView对象的元组:

// 定义一个匿名元祖
let myTuple = (1.414, "张飞", 2017, "😋", UIView())

在上面的代码中,我们没有明确指定元组中各元素的具体类型,但是编译器还是可以通过类型推断类判断出它们的真实类型。尽管类型推断非常方便,但是,有时候明确指明元素的类型是非常必要的:

整型溢出.png

上面图中的那个元组中,元素0x8ffffffffffffffff系统推断其类型为Int,但是实际上它已经超出了Int类型所能存储的最大值,直接产生溢出,从而导致编译器报错,因此需要对代码做一些修改,明确指定元素0x8ffffffffffffffff的类型为Double,这样编译器就不会报错了:

// 明确指定元组中各元素的类型
let anotherTuple : (String, Double) = ("这个元组中将包含一个非法的值", 0x8ffffffffffffffff)

通常情况下,有两种方式可以访问元组中的元素,第一种是通过索引,另一种是将元组中的元素分解为常量或者变量:

// 定义一个匿名元祖
let myTuple = (1.414, "张飞", 2017, "😋", UIView())

// 通过索引来访问myTuple中的元素
print(myTuple.1)  // 访问元组中索引为1的元素

// 先将元组分解为常量或者变量
let (squareRoot, name, year, smile, view) = myTuple
// 直接访问变量中的值
print(squareRoot)
print(name)
print(year)
print(smile)
print(view)

将元组分解为变量的方法非常简单,就是用一个元组类型的变量去接收需要分解的那个元组,然后直接访问元组变量中各元素的名称就可以了。

2、命名元组(Named Tuples)

其实从本质上来讲,我们在上面分解元组的时候,用的就是命名元组。命名元组就是给元组中各个元素取一个名字,这样可以方便我们对元组中的元素进行访问。要使用命名元组,只需要在定义元组的同时,给元组中各个元素取一个名字就可以了,其格式为“元素名称:元素”,具体演示代码如下:

// 命名元组
let foreignStars = (山东天后 : "Robyn Rihanna", 麻辣鸡 : "Nicki Minaj", 水果姐 : "Katy Perry")

// 依据元组中个元素的名称来快速访问
print(foreignStars.山东天后)
print(foreignStars.麻辣鸡)
print(foreignStars.水果姐)

使用命名元组同样也可以在定义的时候指明元组中个元素的具体类型,并且同样也可以按元组的索引进行访问,具体操作这里就不演示了。下面我们就演示一下在函数中返回一个命名元组的示例:

// 返回一个命名元组
func getIdentityInformation() -> (name : String, age : Int, height : Float) {
    return ("xiaoming", 20, 1.70)
}

let result = getIdentityInformation()

result.name
result.age
result.height

二、下标的实现

经过前面对集合基础知识的学习,我们可能对下标有了一定的认识。通过下标来访问数组中的元素,这种方式既简单又快捷。那么,能不能给类、结构体和枚举也定义下标呢?答案是肯定的。接下来,我们将学习下标操作的相关知识。

(一)、下标语法(Subscript Syntax)

如果在类中定义了下标,我们就可以通过下标来快速访问对象了。定义下标使用关键字subscript,参数个数根据具体需求而定,并且多个下标参数之间可以是不同的类型,下标的返回值可以是任何类型。在定义下标的时候,可以定义get方法和set方法,其中set方法不是必须的。具体的示例代码如下:

// 给类定义下标
class MovieList {
    private var movies = ["摔跤吧,爸爸", "银河护卫队2", "速度与激情8", "金刚 : 骷髅岛"]
    
    subscript(index: Int) -> String {
        
        get {
            
            // 为外界通过下标访问实例提供接口
            return self.movies[index]
        }
        
        set {
            
            // 用来接收外界传递进来的新值
            self.movies[index] = newValue
        }
    }
}

// 创建一个movieList实例
var movieList = MovieList()

// 通过下标来对实例进行访问
var aMovie = movieList[0]

// 通过下标来对实例进行赋值
movieList[1] = "汽车人总动员😂🤣🙄"

如果有需要的话,在类或者结构体中可以实现多个下标,这种特性又被称之为下标重载(subscript overloading),如果想了解更多相关的知识,可以参阅苹果官方文档,里面有详细的解释。

关于Swift常用的数据结构相关的知识,还剩与Objective-C之间的桥接,以及面向协议编程没有介绍,鉴于篇幅的原因,我准备把它们放在下一篇中介绍。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容