Swift系列十 - inout的本质
inout
是可以用来在函数内部修改外部属性内存的。
一、inout回顾
示例代码:
func test(_ num: inout int) { num = 20 } var a = 10 test(&a) print(a) // 输出:20 test(&a)
通过汇编分析,全局变量a
的地址0x6c52(%rip)
传递给了寄存器rdi
,rdi
作为参数传递给了test
函数,所以inout的本质就是引用传递(地址传递)。
二、inout本质
示例代码:
struct shape { var width: int var side: int { willset { print("willset", newvalue) } didset { print("didset", oldvalue, side) } } var girth: int { set { print("setgirth") width = newvalue / side } get { print("getgirth") return width * side } } func show() { print("width=\(width), side=\(side), girth=\(girth)") } } func test(_ num: inout int) { print("test") num = 20 } var s = shape(width: 10, side: 4)
2.1. 存储属性
test(&s.width) s.show() /* 输出: test getgirth width=20, side=4, girth=80 */
分析:
-
0x6c9d(%rip)
是全局变量s
的地址值; -
s
的内存地址和结构体shape
中第一个存储属性的地址是相同的(值类型); - 相当于把实例
s
中存储属性width
的内存地址传给了test
函数; - 所以结构体的存储属性使用
inout
的本质和全局/局部变量都一样。
结论:
由于存储属性有自己的内存地址,所以直接把存储属性的地址传递给需要修改的函数,在函数内部修改存储属性的值。
2.2. 计算属性
test(&s.girth) s.show() /* 输出: getgirth test setgirth getgirth width=5, side=4, girth=20 */
> 思考:上面的代码中s.girth
也是地址传递么?答案:不是,因为girth
不是存储属性,所以不占用结构体的内存,但是使用&s.girth
不会报错,并且正常读写值,所以编译器是允许我们这么做的。那它是如何传递修改值的呢?
分析:
- 执行代码
test(&s.girth)
首先调用了girth
的getter
方法; - 然后
getter
方法会返回一个值,这个值放在临时空间内(局部变量); - 调用
test
方法时是把getter
返回的临时变量作为参数传递的(传递的还是地址值),这时候在test
方法内部修改的是临时变量内存的值; - 当修改局部变量内存时,会调用
girth
的setter
方法,把局部变量的值作为参数传递; - 最终的结果就是值被修改了。
结论:
由于计算属性没有自己的地址值,所以会调用getter
方法获取一个局部变量,把局部变量的值传递给需要修改的函数,在函数内部修改局部变量的值,最后把局部变量的值传递给setter
方法。
2.3. 属性观察器
test(&s.side) s.show() /* 输出: test willset 20 didset 4 20 getgirth width=10, side=20, girth=200 */
分析:
- 取出
0x6cc3(%rip)
的前8个字节给rax
,而0x6cc3(%rip)
的本质就是存储属性side
(通过汇编注释可以看出s
偏移8个字节,而width
占用8个字节,跳过width
就是side
); -
rax
的值又给了局部变量-0x28(%rbp)
; - 然后把局部变量
rdi
的值传递给了test
函数,通过打印发现rdi
保存的值就是20; -
test
函数执行完成后,开始执行side
的setter
方法,并把之前的局部变量rdi
作为参数传递过去; -
willset
之前没有修改rdi
,所以rdi
保存的还是20,并且作为第一个参数传递给了willset
; - 由于
willset
之后才会真正修改属性值,并且didset
之前已经知道修改过的属性值,所以真正修改属性值是在willset
和didset
之间;
结论:
修改带有属性观察器的存储属性值时,和计算属性的过程有点类似。先拿到属性的值给局部变量,然后把局部变量的地址值传递给需要修改的函数,函数内部会修改局部变量的值。函数执行完成后把已经修改过的局部变量的值赋值给属性。赋值时,优先执行属性的willset
方法,willset
执行结束后,才会真正修改属性的值,最后调用didset
。
小技巧:需要传递
inout
参数的函数,业务逻辑是非常独立的,目的仅仅是修改传递过来的参数值,不会影响计算属性/存储属性(属性观察器)的逻辑,所以除了计算属性可以直接传地址,其他属性都需要一个局部变量做一个中转。
2.4. inout的本质总结
-
如果实参有物理内存地址,且没有设置属性观察器
- 直接将实参的内存地址传入函数(实参进行引用传递)
-
如果实参是计算属性或设置了属性观察器,采取copy in copy out的做法:
- 调用该函数时,先复制实参的值,产生一个副本(局部变量-执行
get
方法) - 将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值
- 函数返回后,再将副本的值覆盖实参的值(执行
set
方法)
- 调用该函数时,先复制实参的值,产生一个副本(局部变量-执行
总结:inout
的本质就是引用传递(地址传递)。
什么是copy in copy out?先copy到函数里,修改后再copy到外面。
上一篇: 一个数据库的起始
下一篇: 绝对干货!初学者也能看懂的DPDK解析
推荐阅读
-
牌面十足!荣耀50系列登上全国18城地标:有你的城市吗
-
荣耀9X系列新配色今日亮相:五光十色的白是什么白?
-
荐 你真的了解Js吗?用五个问题来回顾一下,留下你的答案,我们一起进步!(系列十)
-
五代十国中的“国”和“代”有什么本质区别?
-
笔记本质量十大排名你知道吗(全球销量最好的10款笔记本)
-
看完这十大升级 找到了荣耀60系列卖爆的原因:新年换机就它了
-
荐 数据分析师之所需要了解的产品系列知识(十)——如何驱动参与(4):愉悦的消费驱动了参与度
-
十个必知的排序算法|Python实例系列[1]
-
GO语言学习系列十——GO的接口(Interface)
-
GO语言学习系列十——GO的接口(Interface)