值类型和引用类型

  像其它很多编程语言一样,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,并且用这两个类型分别创建了两个实例变量valref。接下来,我们要再定义两个函数,分别用来修改实例valref的属性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是引用类型,而引用类型在作为参数进行传递时,它不是传递实例的拷贝,而是传递一个指向原始实例的引用。也就是说,refreference最终都指向同一个目标,这也就能解释为什么在函数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,后续数据结构和算法相关的代码也会在这个仓库中更新。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值