像其它很多编程语言一样,Swift中也有很多内建的基本类型,比如说,整型、浮点型、数组、字典、元组,以及类等等。按照基本类型实例在作为参数进行传递,或者赋值给另外一个实例时所遵循的不同机制,这些基本类型又可以划分为值类型
和引用类型
。结构体、枚举和元组是典型的值类型
数据结构,类则是引用类型
数据结构。
一、值类型和引用类型的基本概念
值类型
和引用类型
具有非常明显的区别,这主要体现在它们的实例在作为参数进行传递,或者赋值给另外一个实例的过程上。当我们传递一个值类型的实例时,实际上我们传递的是原始实例的拷贝,这意味着两个实例中不管哪一个发生了变化,最终的结果都不会反应到另一个身上。因为它们之间是相互独立的。当我们传递一个引用类型的实例时,实际上我们只是向原始实例传递了一个新的引用,这意味着两个不同的引用最终都指向了同一个实例,不管它们之间哪一个发生了变化,最终的结果都会反映到另一个引用身上。上述关系画图表示如下:
值类型和引用类型的概念非常的重要,必须掌握。如果只是用文字和图像来描述的话,我估计一时很难把它们的概念讲清楚,也无法直观的了解它们之间的区别。为此,我们可以通过代码演示来加深映像:
/// 定义一个值类型
struct MyValueType {
// 声明MyValueType的属性
var name: String
var nationality: String
var age: Int
// 我们都知道,某个类型的属性在定义的时候应该进行初始化,没有被初始化的属性是不能使用的。
// 但是,我们可以先声明属性,然后在构造函数中对其进行初始化(也就是第一次使用的时候初始化)。
// 如果是在类中,声明属性的时候没有对其进行初始化,那么就应该在定义构造函数时进行初始化。
// 不过,结构体比较特殊,我们既可以在声明属性时,不对属性进行初始化,又可以不用定义构造
// 函数(显示的定义也是可以的)。因为系统会默认给我们定义构造函数,然后在第一次使用结构体
// 实例时,对结构体的相关属性进行初始化
}
/// 定义一个引用类型
class MyReferenceType {
// 声明MyReferenceType的属性
var name: String
var nationality: String
var age: Int
// 定义init方法
init(name: String, nationality: String, age: Int) {
// 给MyReferenceType的属性赋值(属性初始化)
self.name = name
self.nationality = nationality
self.age = age
}
}
// 创建一个MyValueType实例。虽然我们在上面没有明确定义构造函数,但是在创建结构体实例的
// 时候,系统还是给我们提供了一个默认的构造函数,我们可以在这个构造函数中对结构体的相关属
// 性进行初始化处理
var val = MyValueType(name: "Enrica", nationality: "Chinese", age: 20)
// 创建一个MyReferenceType实例
var ref = MyReferenceType(name: "Enrica", nationality: "Chinese", age: 20)
在上面的代码中,我们定义了一个值类型MyValueType
和一个引用类型MyReferenceType
,并且用这两个类型分别创建了两个实例变量val
和ref
。接下来,我们要再定义两个函数,分别用来修改实例val
和ref
的属性age
的值:
// 修改MyValueType实例中属性age的值
func changeValueType(value: MyValueType, age: Int) {
// 'value' is a 'let' constant
var val = value
val.age += age
}
// 调用changeValueType(value: , age: )函数,并且将
// 实例val传递进去
changeValueType(value: val, age: 5)
print("MyValueType: \(val.name) -- \(val.age)")
// 修改MyReferenceType实例中属性age的值
func changeReferenceType(reference: MyReferenceType, age: Int) {
reference.age += age
}
// 调用changeReferenceType(reference: , age: )函数,并且将
// 实例ref传递进去
changeReferenceType(reference: ref, age: 5)
print("MyReferenceType: \(ref.name) -- \(ref.age)")
运行程序,我们可以看到MyValueType
类型实例val
的属性age
的值还是原来的20,而MyReferenceType
类型实例ref
的属性age
值已经从原来的20变为25了。为什么发生这种情况?首先,需要说明一下,函数changeValueType(value: , age: )
和changeReferenceType(reference: , age: )
基本上是一毛一样的,为什么在实现过程中,前面一个函数要特别多出一段代码var val = value
呢?这主要是因为,函数changeValueType(value: , age: )
是用来修改结构体类型实例的属性值的,它的形参value
默认是let
常量,而常量是无法直接对其进行修改的,所以我们要先定义一个变量val
来接收它,然后再修改这个变量val
的值。另外,我们在前面已经说过,值类型的实例在作为参数进行传递时,它不是直接传递该实例本身,而是传递该实例的一个副本(或者说是拷贝)。因此,尽管我们在调用函数changeValueType(value: , age: )
时,将实例val
作为实参传递了过去,但是它实际上是先将val
拷贝一份,然后再将这个拷贝传递过去。也就是说,最终传递到函数内部的参数已经不是最原始的那个val
,而是一个全新、与val
等值的替身。这也就是为什么我们在函数changeValueType(value: , age: )
中修改变量age
的值,但是最终打印出来的结果却还是显示原来的值。函数changeReferenceType(reference: , age: )
之所以能修改实例ref
的属性age
的值,主要是因为ref
是引用类型,而引用类型在作为参数进行传递时,它不是传递实例的拷贝,而是传递一个指向原始实例的引用。也就是说,ref
和reference
最终都指向同一个目标,这也就能解释为什么在函数changeReferenceType(reference: , age: )
中修改ref
的属性age
的值是行得通的。
我想我大概是说清楚了值类型
和引用类型
的概念。那么,接下来我们就应该看一下值类型
和引用类型
在实际开发中的应用。
二、值类型和引用类型的应用
与Objective-C语言不同,在Swift中,结构体和枚举都具有部分面向对象的特性,这就意味着,以前某些只能由类来实现的功能,现在用结构体同样也可以实现。如果你想知道结构体可以实现哪些原本是由类来实现的功能,可以参考官方文档中的内容:
(1)、优先使用值类型的数据结构
一般情况下,如果某种数据类型,既可以用类实现,也可以用结构体实现,那么最好是用结构体实现。为什么呢?这主要是因为,相对而言,值类型
比引用类型
更加的安全。在上面的概念中,我们已经说过,值类型
的实例在被作为参数传递给函数,或者被赋值给另外一个常量或者变量时,它传递或者赋值的是原始实例的一个副本,这就意味着原始数据被意外修改的可能性会比较低。因为原始实例的作用范围仅限定于创建它的函数或者类型之中。
另外,使用值类型
还可以防止我们对同一个实例进行多次引用。对同一个实例进行多次引用,在某些情况下并不是什么好事情。为了说明这个问题,我们可以编写一个对同一个实例进行多次引用的代码:
/// 我们假设MyReferenceType是用来表示人的,我们填写不同人的年龄
///
/// - Parameter referenceType: 表示人员的MyReferenceType对象
func setAgeFor(referenceType: MyReferenceType) {
// 随机一个年龄
let age = Int(arc4random_uniform(20) + 16)
// 将年龄设置上去
referenceType.age = age
// 打印人员的年龄
print("\(referenceType.name)今年\(referenceType.age)岁。")
}
// 创建一个存放人员的数组
var referenceTypes = [MyReferenceType]()
// 存放不同人员名字的数组
var names = ["张三", "李四", "王五", "赵六"]
// 创建MyReferenceType对象(名字为空,国籍默认为中国,年龄默认为0)
var referenceType = MyReferenceType(name: "", nationality: "Chinese", age: 0)
print("将人员信息添加到人员数组之前:")
// 遍历存放名字的数组
for name in names {
// 将名字设置上去
referenceType.name = name
// 调用setAgeFor(referenceType: )函数,将实例referenceType传递过去
setAgeFor(referenceType: referenceType)
// 将人员添加到存放人员的数组referenceTypes中
referenceTypes.append(referenceType)
}
print("将人员信息添加到人员数组之后:")
// 从存放人员的数组referenceTypes中取出人员
for referenceType in referenceTypes {
// 再次打印不同人员的年龄
print("\(referenceType.name)的年龄是\(referenceType.age)。")
}
我们的意图是,创建一个实例,然后给它设置不同的名字和年龄,并且在此过程中,先将其存储到一个数组中,最后再从数组中把它取出来。先来看一下运行的效果,再来说明它存在什么问题:
我们预期的结果是,从数组中取出来的是不同的名字对应不同的年龄,但是现实结果却是同一个名字和同一个年龄。这显然不符合我们的要求!为什么会出现这种情况?这主要是因为自始至终我们都只创建了一个MyReferenceType
实例,尽管我们给它赋值了不同的名字,以及传递了不同的年龄,但是因为MyReferenceType
是引用类型
类型的关系,这些不同的变量最终都指向了同一个实例。说白了,我们就是在不断的更新同一个实例的数据。因此,最终打印出来的效果就是最后一次更新的数据。为了达到预期效果,我们来修改一下上面的代码:
/// 还是设置年龄,程序几乎没变,只不过在形参类型前面多加了一个关键字inout
func setAgeFor(valueType: inout MyValueType) {
let age = Int(arc4random_uniform(20) + 16)
valueType.age = age
print("\(valueType.name)今年\(valueType.age)岁。")
}
var valueTypes = [MyValueType]()
var names = ["张三", "李四", "王五", "赵六"]
var valueType = MyValueType(name: "", nationality: "Chinese", age: 0)
print("将人员信息添加到人员数组之前:")
for name in names {
valueType.name = name
// 实参valueType前面加上了一个取地址符&
setAgeFor(valueType: &valueType)
valueTypes.append(valueType)
}
print("将人员信息添加到人员数组之后:")
for valueType in valueTypes {
print("\(valueType.name)的年龄是\(valueType.age)。")
}
代码基本上是一模一样的,我们只是修改了3处:将数据类型从之前的MyReferenceType
替换成了MyValueType
,在函数setAgeFor(valueType: )
的形参类型前面多添加了一个关键字inout
,相应的,在调用该函数时,在所传递的实参前面多加了一个取地址符&
,其它的统统都没有变。再来看一下运行效果:
同样是自始至终都只创建一个实例,而且也几乎是相同的业务逻辑,使用值类型
的数据结构,立马就呈现出我们所预期的效果。这主要是因为,值类型
的实例在作为参数进行传递,或者被赋值给另外一个变量或常量时,它是复制的,也就不存在多个变量或者常量同时指向同一个实例的情况。
(2)、合理的使用引用类型
值类型
的数据结构虽然很强大,但并不是什么地方都可以用。很多时候,我们还是得使用引用类型
的数据结构。比如说,像递归数据类型就不能使用值类型
的数据结构。所谓递归数据类型的数据结构,就是指在该数据类型中又包含同类型的属性,比如说像下面这样的:
/// 递归类型的数据结构
class SomeType {
// 该数据结构中又包含SomeType类型的属性
var someProperty: SomeType?
}
递归数据类型在计算机科学中很常见,是一种非常重要的数据类型。当我们想要定义动态数据结构时,就必须使用递归数据类型,比如说像链表,以及各种树(二叉树、森林)神马的。因为值类型
是不支持包含递归类型的存储属性的,因此我们没法用结构体来实现递归数据类型。
当然,如果仅仅只是用文字描述,肯定很难深刻的理解,为什么递归数据类型不能用值类型
的结构体来实现。为此,我决定使用代码和图片的方式来展开。首先,在语法层面上,我们先假设Swift是支持用结构体来实现递归数据类型的:
/// 表示一个结点的信息(Swift中的结构体不支持这种定义,这只是一种假设)
struct LinkedListValueType {
// 存储当前结点中的数据
var value: String
// 存储下一个结点的信息(如果当前结点是尾结点,则next的值为空)
var next: LinkedListValueType?
}
上面的代码描述的是一个单链表的数据结构。在该结构中,我们声明了两个存储属性,其中value
是用来存储当前结点的数据的(类似于C语言中的数据域),而next
则是用来存储下一个结点的信息的(类似于C语言中的指针域)。用图片描述,一个单链表的数据结构大概如下图所示:
注意,上图中next
所指向的是整个结点,并非是下一个节点中的value
值。除了语法层面上不支持结构体实现递归数据类型之外,我们还将从逻辑上说明结构体不适合用来实现递归数据类型。
我们都知道,单链表是由一系列的结点所组成的,并且除了尾结点之外,所有结点中都有一个指向下一个结点的引用。一旦某个结点的引用不再指向下一个结点,那么单链表就会失去该节点之后所有的结点。
// 创建第一个结点
var firstNode = LinkedListValueType(value: "firstNode", next: nil)
// 创建第二个结点
var secondNode = LinkedListValueType(value: "secondNode", next: nil)
// 创建第三个结点
var thirdNode = LinkedListValueType(value: "thirdNode", next: nil)
// 将这三个结点链接起来
firstNode.next = secondNode
secondNode.next = thirdNode
在Swift中,串联一个单链表所有结点的方式应该就是上面这样的。但是,实际情况是我们的单链表还是无法链接起来!为什么呢?这主要还是因为值类型
的实例在赋值给另外一个变量或者常量时,它不是赋值原始实例,而是赋值原始实例的拷贝(副本)。以firstNode.next = secondNode
这行代码为例,它其实并没有串联secondNode
,它串联的是secondNode
的副本。也就说,即便Swift从语法层面上支持值类型
的递归数据类型的实现,结构体本身从逻辑上也无法实现递归数据类型。因此,要想实现递归数据类型,就必须通过引用类型
来完成:
/// 表示一个结点的信息
class LinkedListReferenceType {
// 用于存储当前结点的数据
var value: String
// 用于存储下一个结点的信息(如果当前结点是尾结点,则next的值为空)
var next: LinkedListReferenceType?
// 创建结点的构造函数
init(value: String) {
// 对自己的属性进行初始化
self.value = value
}
}
// 创建第一个结点
var firstNode = LinkedListReferenceType(value: "firstNode")
// 创建第二个结点
var secondNode = LinkedListReferenceType(value: "secondNode")
// 创建第三个结点
var thirdNode = LinkedListReferenceType(value: "thirdNode")
// 串联firstNode和secondNode
firstNode.next = secondNode
// 串联secondNode和thirdNode
secondNode.next = thirdNode
当然,值类型
和引用类型
的应用肯定不只这么一点,在后面的篇幅中,我们会深入的学习Swift数据结构和算法相关的基础知识,里面会大量的用到结构体和类。因此,关于值类型
和引用类型
的基础知识必须要掌握。详细代码参见SwiftBooks,后续数据结构和算法相关的代码也会在这个仓库中更新。