Swift值类型&引用类型

Swift值类型&引用类型

前言

值类型和引用类型是Swift中两种数据存储方式,简单来说值类型就是直接存储的值,引用类型就是存储的指针,在谈值类型和引用类型前可能你需要了解一些关于内存和Mach-O的知识。下面放上我以前写过的几篇文章,仅供参考。

iOS内存五大区
iOS 中的虚拟内存和物理内存
Mach-O探索

简单来说值类型可以理解为存储在栈区或者全局区,引用类型一般存储在堆区,下面我们来看个简单的例子。

16092321744318.jpg

我们可以看到at的地址都是在栈区,因为栈区通常都是0x7开头。但是a中存储的直接就是18这个值,t中存储的是个全局区的指针。这就是最简单的值类型和引用类型的区别。

1. 值类型

值类型,即每个实例保持一份数据拷贝。

Swift 中,structenum,以及 tuple 都是值类型。而平时使用的 IntDoubleFloatStringArrayDictionarySet 其实都是用结构体实现的,也是值类型。

Swift 中,值类型的赋值为深拷贝(Deep Copy),值语义(Value Semantics)即新对象和源对象是独立的,当改变新对象的属性,源对象不会受到影响,反之同理。

虽然说IntDoubleFloatStringArrayDictionarySet时使用结构体实现的,所以也是值类型,但是就我个人理解来说,这些作为值类型好像就是那么理所当然的,当然对于很长的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)

通过以上的代码我们可以知道:

  1. 类中的属性需要使用?!或者赋初始值才不会导致编译报错
  2. 结构体中的属性不需要赋初始值,也不用使用?!
  3. 结构体的初始化需要同时初始化结果图内部的属性
  4. 类的初始化可以不用初始化类中的属性
  5. 结构体中的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代码我们可以看到:

  1. 类中如果不实现自定义init方法就会有个init()方法
  2. 结构体中会提供默认的初始化方法

1.1.2 验证结构体是值类型

定义一个结构体:

struct Teacher {
    var age: Int
    var age1: Int
}

var t = Teacher(age: 18, age1: 20)

使用lldb调试:

16093081162277.jpg

此时我们可以看到,结构体内部直接存储的就是结构体中的属性的值。所以说结构体是值类型是没问题的。

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")
16093157152789.jpg

在修改t1的值后我们发现t中的数据并没有改变,所以说tt1之间是值传递,即tt1是存储在不同内存空间的,在var t1 = t时,是将t中的值,拷贝到t1中,t1修改时,只会修改自己内存中的数据,是不会影响到t的内存的。

另外在打印两个实例变量地址的时候也明显不是一样的。

1.1.4 通过sil验证struct是值类型

我们查看Teacherinit方法:

// 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)。

16093933472434.jpg

1.1.6 小结

至此我们就验证了结构体是值类型:

  1. 结构体不像类一样需要调用malloc等方法去开辟内存空间
  2. 结构体的内存中直接存储值
  3. 值类型的赋值是一个值传递的过程,相当于深拷贝

1.2 其他

关于enumtuple这里就不一一分析了,在后续的篇章中会陆续提到。

2. 引用类型

引用类型,即所有实例共享一份数据拷贝。

Swift 中,classclosure是引用类型。引用类型的赋值是浅拷贝(Shallow Copy),引用语义(Reference Semantics)即新对象和源对象的变量名不同,但其引用(指向的内存空间)是一样的,因此当使用新对象操作其内部数据时,源对象的内部数据也会受到影响。

2.1 验证类是引用类型

定义一个类

class Teacher {
    var age: Int = 28
    var age1: Int = 20
}

var t = Teacher()

print("end")

lldb调试

16093223424561.jpg

从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")
16093943159284.jpg

通过打印结果我们可以知道,虽然我们修改的是t1这个实例对象中age的值,但是当我们打印t这个实例变量的age的值的时候也随之改变了,所以我们就能够确定类对象之间是指针拷贝,并且在内存地址的打印中我们也可以清晰的看见,它们指向同一片内存空间,一个改变则全部都改变。

2.4 通过sil进一步验证类的引用类型

其实到这里也就没什么好说的的了,在类的初始化的时候肯定是会调用alloc方法来开辟内存空间的,这里借着上面的sil代码,我们来看看Info这个类的Info.__allocating_init()方法吧:

16093970325654.jpg

这里首先就调用了alloc_refInfo初始化一块内存空间。

2.5 常量引用类型

如果声明一个引用类型的常量,那么就意味着该常量的引用不能改变(即不能被同类型变量赋值),但指向的内存中所存储的变量是可以改变的,示例如下:

16098103813847.jpg

此处是不会报编译错误的,这点与值类型也是不同的。

2.6 小结

至此我们就验证了类是引用类型:

  1. 类需要调用alloc等方法去开辟内存空间
  2. 类的实例对象中存储的是指针地址,这个地址中存储的才是值
  3. 类的实例对象的赋值是一个指针拷贝的过程,相当于浅拷贝

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")
16093958789319.jpg

我们可以看到,在值类型中使用引用类型:

  1. 随着t1.info.weight的改变,t中的也改变了
  2. 所以说依旧是值拷贝,只不过是拷贝了引用类型数据的指针
  3. 这里的值传递是只传递了指针

那么真的这个引用类型会不会涉及到内存引用计数的管理呢?其实答案是肯定的,下面我们通过sil代码验证一下:

16093964609674.jpg

通过sil代码我们可以看到strong_retainstrong_release的调用,所以说在值类型的内部使用引用类型依旧是需要通过引用计数管理的。

所以说,应该尽量避免这种值类型中使用引用类型的写法,因为值类型的初衷就是为了不使用指针指向另一片内存区域,从而减少内存的使用,以提升效率。

3.2 值类型嵌套值类型

其实,在上面我们已经介绍过了,在SwiftInt的底层实现就是个结构体,所以也是值类型。

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
}

引用类型嵌套值类型时:

  • 赋值时创建了新的变量
  • 新变量和源变量指向同一块内存
  • 改变源变量的内部值,会影响到其他变量的值
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,565评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,021评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,003评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,015评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,020评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,856评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,178评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,824评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,264评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,788评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,913评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,535评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,130评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,102评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,334评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,298评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,622评论 2 343

推荐阅读更多精彩内容