前言
- 值类型和引用类型是Swift中的核心概念,了解它们是每位Swift开发人员的基础, 在以后的探索中多次提到值类型和引用类型,所以在这里做个笔记。供以后参考。
内容
- 值类型和引用类型的概念
- 值类型和引用类型的内存管理
- 值类型和引用类型的选择
一 、值类型和引用类型的定义
值类型(Value Type):即每个实例保持一份数据拷贝。
引用类型(Reference Type):即所有实例共享一份数据拷贝。
Swift有三种声明类型的方式:class
,struct
和enum
。它们可以分为值类型
(struct和enum)和引用类型
(class)。它们在内存中的存储方式不同决定它们之间的区别:
- 值类型是这样一种类型,当它被赋值给一个变量、常量或者被传递给一个函数的时候,其值会被拷⻉。
- 实际上,Swift 中所有的
基本类型
:整数 (integer)、浮点数(floating-point number)、布尔值(boolean)、字符串串(string)、数组 (array)和字典(dictionary),都是值类型
,其底层
是使用结构体
实现的。Swift 中所有的结构体和枚举类型都是值类型。这意味着它们的实例例,以及实例例中所包含的任何 值类型的属性,在代码中传递的时候都会被复制。- 与值类型不同,引⽤类型在被赋予到一个变量量、常量或者被传递到一个函数时,其值不会被拷贝。因此,使用的是已存在实例的引⽤,而不是其拷贝。
验证值类型:
import UIKit
struct Point {
var x: Double
var y: Double
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let point1 = Point(x: 3, y: 5)
var point2 = point1
print(point1) // Point(x: 3.0, y: 5.0)
print(point2) // Point(x: 3.0, y: 5.0)
point2.x = 5
print(point1) // Point(x: 3.0, y: 5.0)
print(point2) // Point(x: 3.0, y: 5.0)
}
}
//打印point1 不随着 point2 而变化 。说明他们内存独立
Point(x: 3.0, y: 5.0)
Point(x: 3.0, y: 5.0)
Point(x: 5.0, y: 5.0)
验证引用类型:
import UIKit
class Point {
var x: Double = 0.0
var y: Double = 0.0
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let point1 = Point()
point1.x = 3.0
point1.y = 5.0
let point2 = point1
print(point1)
print(point2)
point2.x = 5
print(point1.x , point1.y)
print(point2.x , point2.y)
}
}
//打印point1 随着 point2 而变化 。说明他们公用一块内存
5.0 5.0
5.0 5.0
二、 值类型和引用类型的内存管理
值类型存储在栈区
。每个值类型变量都有其自己的数据副本,并且对一个变量的操作不会影响另一个变量。引用类型存储在其他位置(堆区)
,我们在内存中有一个指向该位置的引用。引用类型的变量可以指向相同类型的数据。因此,对一个变量进行的操作会影响另一变量所指向的数据
栈区
存储临时数据:方法的参数和局部变量。每次我们调用一个方法时,都会在栈上分配一块新的内存。该方法退出时,将释放该内存。除特殊情况(下面会讲),所有Swift值类型都在此处。
堆区
存储具有生存期的对象。这些都是Swift引用类型,还有一些值类型的情况。堆和栈朝着彼此增长堆区的分配一般按照地址从小到大进行,而栈区的分配一般按照地址从大到小进行分配。
【堆与栈分配的成本】
栈区内存分配和销毁的工作原理与数据结构中的栈相同。你只能从栈顶压栈或出栈。指向栈顶的指针足以实现这两个操作。因此,栈指针可以腾出空间来分配其他更多的内存。当函数执行完退出时,我们将栈指针增加到调用此方法之前的位置。(为什么增加才能回到调用之前的地址,刚说了栈是从大到小进行分配的)
- 栈分配和释放的成本相当于整数复制的成本
堆分配过程涉及的东西很多。我们必须搜索堆区以找到适合它大小的空内存块。我们还必须同步堆,因为多个线程可能同时在其中分配内存。为了从堆中释放内存,我们必须将该内存重新插入适当的位置。
堆分配和释放的成本比栈要大得多
【引用类型的内存分配】
引用类型的存储属性不会直接保存在栈上,系统会在栈上开辟空间用来保存实例的指针,栈上的指针负责去堆上找到相应的对象。
引用类型的赋值不会发生 “拷贝”,当你尝试修改示例的值的时候,实例的指针会 “指引” 你来到堆上,然后修改堆上的内容。
举例:
//因为 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,使得整个开发更加灵活高效。