「这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战」。
【引用类型的内存分配】
- 引用类型的存储属性不会直接保存在栈上,系统会在栈上开辟空间用来保存实例的指针,栈上的指针负责去堆上找到相应的对象。
- 引用类型的赋值不会发生 “拷贝”,当你尝试修改示例的值的时候,实例的指针会 “指引” 你来到堆上,然后修改堆上的内容。
举例:
//因为 Point 是类,所以 Point 的存储属性不能直接保存在栈上
class Point {
var x: Double
var y: Double
init(x: Double, y: Double) {
self.x = x
self.y = y
}
}
let point1 = Point(x: 3, y: 5)
let point2 = point1
print(point1.x, point1.y) // 3.0 5.0
print(point2.x, point2.y) // 3.0 5.0
栈 堆
point1 [ ] --|
|--> 类型信息
point2 [ ] --| 引用计数
x: 3
y: 5
实际:公用一块堆
- 分析: 因为 Point 是类,所以 Point 的存储属性不能直接保存在栈上,系统会在栈上开辟两个指针的长度用来保存 point1 和 point2 的指针,栈上的指针负责去堆上找到对应的对象,point1 和 point2 两个实例的存储属性会保存在堆上。
- 当使用
“=”进行赋值时,栈上会生成一个 point2 的指针,point2 指针与 point1 指针指向堆的同一地址。 - 相比在栈上保存 point1 和 point2,堆上需要的内存空间要更大,除了保存 x 和 y 的空间,在头部还需要两个 8 字节的空间,一个用来索引类的类型信息的指针地址,一个用来保存对象的 “引用计数”
当尝试修改 point2 的值的时候,point2 的指针会 “指引” 你来到堆上,然后修改堆上的内容,这个时候 point1 也被修改了。
point2.x = 5
print(point1.x, point1.y) // 5.0 5.0
print(point2.x, point2.y) // 5.0 5.0
我们称 point1 和 point2 之间的这种关系为 “共享”。“共享” 是引用类型的特性,在很多时候会给人带来困扰,“共享” 形态出现的根本原因是我们无法保证一个引用类型的对象的不可变性。
三、值类型和引用类型的选择
想要创建一个新的类型,该如何选择呢?当你写Cocoa程序的时候,大多数APIs都需要从NSObject继承,你就已经是一个类了(引用类型),针对其他情况,这里有些指导规则:
-
使用值类型:
- 通过使用==去比较实例的数据
- 想得到一个实例的独立副本
- 数据在多线程环境下被修改
-
使用引用类型(比如使用一个类):
- 通过使用===去判断两个实例是否恒等
- 想要创建一个共享的,可变的对象
在Swift里,Array、String和Dictionary都是值类型,他们的行为和C语言中的int类似,
每个实例都有自己的数据,你不需要额外做任何事情,比如做一个显式的copy,防止其他代码在你不知情的情况下修改等,更重要的是,你能安全地在线程间传递它,而不需要使用同步技术。在提高安全性的精神下,这个模型将帮助你在Swift中写出更多可预知的代码。
面试题:说说Swift为什么将String,Array,Dictionary设计成值类型?
要解答这个问题,就要和Objective-C中相同的数据结构设计进行比较。Objective-C中,字符串,数组,字典,皆被设计为引用类型。
- 值类型相比引用类型,最大的优势在于
内存使用高效。值类型在栈上操作,引用类型在堆上操作。栈上的操作仅仅是单个指针的上下移动,而堆上的操作则牵涉到合并、移位、重新链接等。也就是说Swift这样设计,大幅减少了堆上的内存分配和回收的次数。同时copy-on-write又将值传递和复制的开销降到了最低。 - String,Array,Dictionary设计成值类型,也是
为了线程安全考虑。通过Swift的let设置,使得这些数据达到了真正意义上的“不变”,它也从根本上解决了多线程中内存访问和操作顺序的问题。 - 设计成值类型还可以
提升API的灵活度。例如通过实现Collection这样的协议,我们可以遍历String,使得整个开发更加灵活高效。