Swift值类型&引用类型
前言
值类型和引用类型是Swift
中两种数据存储方式,简单来说值类型就是直接存储的值,引用类型就是存储的指针,在谈值类型和引用类型前可能你需要了解一些关于内存和Mach-O
的知识。下面放上我以前写过的几篇文章,仅供参考。
iOS内存五大区
iOS 中的虚拟内存和物理内存
Mach-O探索
简单来说值类型可以理解为存储在栈区或者全局区,引用类型一般存储在堆区,下面我们来看个简单的例子。
我们可以看到a
和t
的地址都是在栈区,因为栈区通常都是0x7
开头。但是a
中存储的直接就是18
这个值,t
中存储的是个全局区的指针。这就是最简单的值类型和引用类型的区别。
1. 值类型
值类型,即每个实例保持一份数据拷贝。
在 Swift
中,struct
,enum
,以及 tuple
都是值类型。而平时使用的 Int
、Double
、Float
、String
、Array
、Dictionary
、Set
其实都是用结构体实现的,也是值类型。
Swift
中,值类型的赋值为深拷贝(Deep Copy
),值语义(Value Semantics
)即新对象和源对象是独立的,当改变新对象的属性,源对象不会受到影响,反之同理。
虽然说Int
、Double
、Float
、String
、Array
、Dictionary
、Set
时使用结构体实现的,所以也是值类型,但是就我个人理解来说,这些作为值类型好像就是那么理所当然的,当然对于很长的String
还是会通过存储指向堆区的指针来实现,当然也会通过TaggedPointer
等技术进行优化,这里大体还是和OC
相同的,感兴趣的可以看看我的另一篇文章iOS Objective-C 内存管理。说了这么多,其实我们纠结的一个问题就是struct
为什么是值类型,下面我们就来探索一番。
1.1 struct 为什么是值类型
1.1.1 结构体和类的区别
从代码看区别
class CTeacher {
var age: Int?
var name: String!
var height: Float = 185.3
}
struct STeacher {
var age: Int
}
let ct = CTeacher()
ct.name = "testC"
let st1 = STeacher(age: 20)
let st2 = STeacher(age: 21, name: "testS", height: 180.1)
通过以上的代码我们可以知道:
- 类中的属性需要使用
?
、!
或者赋初始值才不会导致编译报错 - 结构体中的属性不需要赋初始值,也不用使用
?
、!
- 结构体的初始化需要同时初始化结果图内部的属性
- 类的初始化可以不用初始化类中的属性
- 结构体中的
optional
属性,或者赋值的属性可以不在结构体初始化的时候初始化
从sil代码看区别
class CTeacher {
@_hasStorage @_hasInitialValue var age: Int? { get set }
@_hasStorage @_hasInitialValue var name: String! { get set }
@_hasStorage @_hasInitialValue var height: Float { get set }
@objc deinit
init()
}
struct STeacher {
@_hasStorage var age: Int { get set }
@_hasStorage @_hasInitialValue var name: String? { get set }
@_hasStorage @_hasInitialValue var height: Float { get set }
init(age: Int, name: String? = nil, height: Float = 185.3)
}
通过sil
代码我们可以看到:
- 类中如果不实现自定义
init
方法就会有个init()
方法 - 结构体中会提供默认的初始化方法
1.1.2 验证结构体是值类型
定义一个结构体:
struct Teacher {
var age: Int
var age1: Int
}
var t = Teacher(age: 18, age1: 20)
使用lldb调试:
此时我们可以看到,结构体内部直接存储的就是结构体中的属性的值。所以说结构体是值类型是没问题的。
1.1.3 验证结构体是值拷贝
此时我们创建个新的实例变量t1
,并将t
赋值给t1
,代码如下:
struct Teacher {
var age: Int
var age1: Int
}
var t = Teacher(age: 18, age1: 20)
var t1 = t
t1.age = 22
print("end")
在修改t1
的值后我们发现t
中的数据并没有改变,所以说t
和t1
之间是值传递,即t
和t1
是存储在不同内存空间的,在var t1 = t
时,是将t
中的值,拷贝到t1
中,t1
修改时,只会修改自己内存中的数据,是不会影响到t
的内存的。
另外在打印两个实例变量地址的时候也明显不是一样的。
1.1.4 通过sil验证struct是值类型
我们查看Teacher
的init
方法:
// Teacher.init(age:age1:)
sil hidden @main.Teacher.init(age: Swift.Int, age1: Swift.Int) -> main.Teacher : $@convention(method) (Int, Int, @thin Teacher.Type) -> Teacher {
// %0 "$implicit_value" // user: %3
// %1 "$implicit_value" // user: %3
// %2 "$metatype"
bb0(%0 : $Int, %1 : $Int, %2 : $@thin Teacher.Type):
%3 = struct $Teacher (%0 : $Int, %1 : $Int) // user: %4
return %3 : $Teacher // id: %4
} // end sil function 'main.Teacher.init(age: Swift.Int, age1: Swift.Int) -> main.Teacher'
我们可以看到init
方法中并没有调用malloc
相关的开辟内存的方法,这里也是只是将传入的两个值赋给初始化的结构体而已。
1.1.5 常量值类型
如果声明一个值类型的常量,那么就意味着该常量是不可变的(无论内部数据为 var
还是let
)。
1.1.6 小结
至此我们就验证了结构体是值类型:
- 结构体不像类一样需要调用
malloc
等方法去开辟内存空间 - 结构体的内存中直接存储值
- 值类型的赋值是一个值传递的过程,相当于深拷贝
1.2 其他
关于enum
和tuple
这里就不一一分析了,在后续的篇章中会陆续提到。
2. 引用类型
引用类型,即所有实例共享一份数据拷贝。
在 Swift
中,class
和closure
是引用类型。引用类型的赋值是浅拷贝(Shallow Copy
),引用语义(Reference Semantics
)即新对象和源对象的变量名不同,但其引用(指向的内存空间)是一样的,因此当使用新对象操作其内部数据时,源对象的内部数据也会受到影响。
2.1 验证类是引用类型
定义一个类
class Teacher {
var age: Int = 28
var age1: Int = 20
}
var t = Teacher()
print("end")
lldb调试
从lldb调试中我们可以看到,类实例对象指针内部存储的是一个指向全局区的指针,而这块内存区域才是存储的真正的实例变量的信息,所以说类是个引用类型。
2.2 验证类对象是指针拷贝
我们使用如下代码进行验证:
class Teacher {
var age: Int = 28
var name: String = "teacher1"
}
var t = Teacher()
print(t.age)
var t1 = t
t1.age = 18
print(t.age)
print("end")
通过打印结果我们可以知道,虽然我们修改的是t1
这个实例对象中age
的值,但是当我们打印t
这个实例变量的age
的值的时候也随之改变了,所以我们就能够确定类对象之间是指针拷贝,并且在内存地址的打印中我们也可以清晰的看见,它们指向同一片内存空间,一个改变则全部都改变。
2.4 通过sil进一步验证类的引用类型
其实到这里也就没什么好说的的了,在类的初始化的时候肯定是会调用alloc
方法来开辟内存空间的,这里借着上面的sil
代码,我们来看看Info
这个类的Info.__allocating_init()
方法吧:
这里首先就调用了alloc_ref
为Info
初始化一块内存空间。
2.5 常量引用类型
如果声明一个引用类型的常量,那么就意味着该常量的引用不能改变(即不能被同类型变量赋值),但指向的内存中所存储的变量是可以改变的,示例如下:
此处是不会报编译错误的,这点与值类型也是不同的。
2.6 小结
至此我们就验证了类是引用类型:
- 类需要调用
alloc
等方法去开辟内存空间 - 类的实例对象中存储的是指针地址,这个地址中存储的才是值
- 类的实例对象的赋值是一个指针拷贝的过程,相当于浅拷贝
3. 嵌套类型
所谓嵌套类型就是引用类型中有值类型,或者值类型中有引用类型,其实在上面的例子中已经涉及到了,下面我们通过两两组合,分四种情况来简单介绍一下。
3.1 值类型嵌套引用类型
这里是在结构体中添加一个引用类型的属性,示例代码如下:
class Info {
var height: Int = 185
var weight: Double = 60.5
}
struct Teacher {
var age: Int = 18
var name: String = "teacher1"
var info: Info = Info()
}
var t = Teacher()
print(t.info.weight)
var t1 = t
t1.info.weight = 80
print(t.info.weight)
print(t1.info.weight)
print("end")
我们可以看到,在值类型中使用引用类型:
- 随着
t1.info.weight
的改变,t
中的也改变了 - 所以说依旧是值拷贝,只不过是拷贝了引用类型数据的指针
- 这里的值传递是只传递了指针
那么真的这个引用类型会不会涉及到内存引用计数的管理呢?其实答案是肯定的,下面我们通过sil
代码验证一下:
通过sil
代码我们可以看到strong_retain
和strong_release
的调用,所以说在值类型的内部使用引用类型依旧是需要通过引用计数管理的。
所以说,应该尽量避免这种值类型中使用引用类型的写法,因为值类型的初衷就是为了不使用指针指向另一片内存区域,从而减少内存的使用,以提升效率。
3.2 值类型嵌套值类型
其实,在上面我们已经介绍过了,在Swift
中Int
的底层实现就是个结构体,所以也是值类型。
struct Teacher {
var age: Int = 18
}
值类型嵌套值类型:
- 在赋值的时候创建新的变量,两者是独立的。
- 嵌套的值类型变量也会创建新的变量,也可以说是深拷贝一份变量的值
3.3 引用类型嵌套引用类型
其实这也是我们经常用到的一种嵌套,比如类中嵌套类。
class Info {
var height: Int = 185
var weight: Double = 60.5
}
class Teacher {
var age: Int = 18
var name: String = "teacher1"
var info: Info = Info()
}
引用类型嵌套引用类型:
- 引用类型再赋值时创建了新的变量
- 新变量和源变量指向同一块内存,内部引用类型变量也指向同一块内存地址
- 改变引用类型嵌套的引用类型的值,也会影响到其他变量的值。
3.4 引用类型嵌套值类型
这个在上面我们也用到过,类中的Int
类型的属性就是很好的例子。
class Teacher {
var age: Int = 28
}
引用类型嵌套值类型时:
- 赋值时创建了新的变量
- 新变量和源变量指向同一块内存
- 改变源变量的内部值,会影响到其他变量的值