Swift中的引用类型和值类型 Reference and Value Types in Swift

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看165 回复回复3 收藏收藏1 分享淘帖 转播转播 分享分享 微信
查看: 165|回复: 3
收起左侧

Swift中的引用类型和值类型 Reference and Value Types in Swift

[复制链接]
cocoaswift 发表于 2016-9-1 16:14:26 | 显示全部楼层 |阅读模式
快来登录
获取优质的苹果资讯内容
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码
订阅梳理好了的知识点专辑

引用类型和值类型是编程中的一个重要却很基础的知识点,网络上也有很多关于两者的讨论,文章深入分析了两者的不同以及在Swift中该如何选择使用的问题,推荐编程新兵及对引用类型和值类型有疑问的Swift使用者阅读。

Swift中的引用类型和值类型 0

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 0

(文章略长,精读大概需要半小时到1小时)

内容提纲:

  • 引用类型
  • 值类型
  • 拷贝语意:浅拷贝、深拷贝
  • 引用类型的问题:隐式的数据共享
  • 值类型的实例:没有隐式的数据共享
  • 引用类型/值类型的不可变性
  • 引用类型/值类型该如何选择
  • 结论




我们将在这篇文章探讨Swift中引用类型和值类型的区别,介绍两者的概念和各自的优缺点,以及该如何使用。




1、引用类型

引用类型初始化后,无论是分配给变量还是常量,或是通过参数传递给函数,都将是同一个实例对象。


Object是一个典型的引用类型,一旦初始化完成,我们不管是将它作为一个值进行分配还是传递,我们都是分配或传递了一个原始对象的引用(实际上它就是内存中的一块区域),引用类型的分配我们一般叫它浅拷贝。

Swift中,用关键字class来定义objects:

[Swift] 纯文本查看 复制代码
class PersonClass {
    var name: String
    init(name: String) {
        self.name = name
    }
}
var person = PersonClass(name: "John Doe")





2、值类型

值类型每次分配给变量/常量或者作为参数传递到函数时,都会重新创建(复制)一个新的实例。


所有的基本类型都是典型的值类型,常用的基本类型也是值类型的有:Int, Double, String, Array, Dictionary, Set。值类型每次初始化以后,当我们将它分配或者传递时,实际上是分配或传递了它的一个拷贝。

Swift中最常用的值类型是structs,enums和tuples,值类型的分配叫做深拷贝。




3、拷贝语意

我会用图片来展示一个实际的例子以描述它们在语法上的区别。假设我们有一个树的数据结构:

[Swift] 纯文本查看 复制代码
class Node<T: Comparable> {
    let value: T
    var left: Node?
    var right: Node?
    convenience init(value: T) { […] }
    init(value: T, left: Node?, right: Node?){ […] }

    func add(value: T) { […] }
}


我们创建一个二叉树的实例如下:

[Swift] 纯文本查看 复制代码
let binaryTree = Node(value: 8)
tree.add(2)
tree.add(13)


Swift中的引用类型和值类型 1

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 1

一个二叉树实例

现在,我们来看一下复制语意在行为上的区别。



3.1、浅拷贝(引用类型)

复制一个引用类型时,Swift编译器将会复制实例的一个引用。但不包含实例的属性。因此,当对一个引用类型进行多次复制时,每一份复制都将共享同一份数据。

Swift中的引用类型和值类型 2

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 2

二叉树浅拷贝



3.2、深拷贝(值类型)

当我们复制一个值类型时,Swift编译器将复制一个全新的实例,包括它的所有属性。整个过程会复制它所有的值类型属性。因此,当对一个值类型进行多次复制时,每次复制都会产生一个单独的、没有数据共享的新实例。

Swift中的引用类型和值类型 3

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 3

二叉树深拷贝




4、引用类型的问题:隐式的数据共享

为了说明引用类型的这种典型问题,我们先定义一个类用来表示2D平面上的一个点。

[Swift] 纯文本查看 复制代码
class PointClass {
    var x: Int = 0
    var y: Int = 0
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}


现在,当我们实例化一个PointClass对象并将它分配给另一个变量时发生了什么?

[Swift] 纯文本查看 复制代码
var pointA = PointClass(x: 1, y: 3)
var pointB = pointA


因为 PointClass 是一个引用类型,最后一句的变量声明实际上是把分配给pointA的引用也分配给了pointB。我们可以用下面的图来形象的描述上述情形:

Swift中的引用类型和值类型 4

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 4

Reference type instances

在这种情形下,pointA和pointB 共享 同一个实例。因此,任何对pointA的改变也将反应到pointB上,反之亦然。这在很多情况下没什么问题,但它也会导致一些不太显而易见的bug。

让我们来看一个很普遍的隐式数据共享问题。假设实例化了一个 view controller 1,并分配了一个 Person 对象(Person是一个model)的实例给它。这时,用户进行操作,我们push了另一个 view controller 2 到上一个 view controller 1 的上面,并把分配了同一个Person的引用实例给 view controller 2。我们可以想象下图这样的特殊情景:

Swift中的引用类型和值类型 5

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 5

Assigning a reference type to more than one view controller

当两个 view controller 同时持有Person实例的用一个引用,如果我们在 view controlelr 2 中修改了Person的任何属性,将导致之前 view controller 1 中所持有Person的属性也同时被修改。因此,在view controller 2 中对数据模型的修改都将传递到 view controller 1 中。

回到我们最初那个例子,要避免数据隐式共享的问题,一个方法是创建一个实例真正的拷贝,以代替将变量pointA直接进行新变量的分配赋值,如下手动创建拷贝并分配给pointB:

[Swift] 纯文本查看 复制代码
var pointB = pointA.copy()


现在,pointB 有了它自己独立的引用,它和 pointA 之间不再共享数据。这个技巧可正常工作,但还是有一些缺点:

  • 不得不:
    • – 继承NSObject并实现NSCopying
    • – 实现一个新的Copyable协议
  • 每一次赋值都需要显示的调用 copy() 带来的额外开销
  • 很容易就忘记调用 copy()





5、值类型实例:没有隐式共享

当分配一个值类型时,编译器会自动创建(并返回)一个实例的拷贝。来看看发生了什么,我们用 struct (值类型) 的 PointStruct 来代替 class (引用类型) 的 Point 。

[Swift] 纯文本查看 复制代码
struct PointStruct {
    var x: Int = 0
    var y: Int = 0
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}


现在,我们可以创建一个 PointStruct 的实例 , 并将它分配给另一个变量。

[Swift] 纯文本查看 复制代码
var pointA = PointStruct(x: 1, y: 3)
var pointB = pointA


因为 PointStruct 是值类型, 最后一行的声明会先创建一个PointA的拷贝,并将拷贝分配给了PointB。这就使得两个实例分配是安全并独立的。上述情形图示如下:

Swift中的引用类型和值类型 6

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 6

Values type instances

我们看到 pointB 有它自己独立的引用,并且不会被 pointA 共享。这表明了使用值类型时,我们可以很容易的确保所有值类型的实例都是相互独立并且不会产生数据共享的。

从性能方面看,使用值类型不会产生巨大的开销:

低成本的拷贝:

  • 基础数据类型的复制花费恒定时间
  • 值类型(struct, enum, tuple)的复制花费恒定时间


可扩展的数据结构使用即写即拷:

  • 拷贝包含一个固定数量的引用计数操作
  • 这项技术常用于许多标准库的类型:String, Array, Set, Dictionary, …

除了以上说的,值类型的另一个性能方面的优点是栈分配,它相对于堆分配(引用类型)有着更高的效率。这将使得访问更快但无法支持继承。

有一点需要注意的是,只有当 structs, enums,