浅谈当Swift中defer和inout同时起作用
本文针对 Swift
中 defer
、inout
以及 return
时相互影响的时候的情况进行简单的分析,目的是更好的理解 defer
、inout
原理以及使用场合,减少踩坑和提高debug
效率。
本文中的示例代码,均为
Swift 4.2
版本
inout和defer
inout
有关 inout
关键字苹果官方描述
If you want a function to modify a parameter’s value, and you want those changes to persist after the function call has ended, define that parameter as an in-out parameter instead.
我们可以了解到,inout
关键字,有点类似于 C
语言中的引用传递或指针传递,目的是为了对入参进行修改。 但实际上 inout
的机制并不是引用传递,或者是指针传递。
以下是两段比较详细的苹果官方描述,摘出来看比较清晰:
You write an in-out parameter by placing the inout keyword right before a parameter’s type. An in-out parameter has a value that is passed in to the function, is modified by the function, and is passed back out of the function to replace the original value.
in-out parameters are passed as follows:
When the function is called, the value of the argument is copied.
In the body of the function, the copy is modified.
When the function returns, the copy’s value is assigned to the original argument.
This behavior is known as copy-in copy-out or call by value result.
从这两段我们可以看出,inout
的实现原理是:
- 参数传入,拷贝一份 临时变量
- 函数体中,修改的是这一份 临时变量
- 函数返回时,这份 临时变量 被赋予给 原始参数
所以,是先拷贝,再修改,再拷贝回去的逻辑。这一点至关重要。
使用示例:
1 | func f(x: inout Int) { |
对于对象,即非
Swift
类型的,因为本身即为指针,所以本身就是指针传递,函数内部调用对象方法进行操作进而导致对象内部值的变化时,并不需要inout
关键字。但是如果你想对这个对象变量整体进行的赋值替换操作,并且能反应到外部时,就需要inout
。
本文为了方便讨论,此处以及后续将仅仅以Swift
原生类型作为例子。
defer
有关 defer
关键字苹果官方的描述
A defer statement is used for executing code just before transferring program control outside of the scope that the defer statement appears in.
This means that a defer statement can be used, for example, to perform manual resource management such as closing file descriptors, and to perform actions that need to happen even if an error is thrown.
简单说来, defer
是用来定义一段代码,用来在 离开 所在 scope
的时候执行。 苹果官方对 defer
的建议,也是用来关闭描述符、处理一些错误等等。
使用示例
1 | func g() { |
这里注意一下, defer
的生效时机是 scope
离开的时候,而 不是函数执行完毕 的时候。 所以如果 defer
写在 if
或者 for
的内部, 会在 if
结束的时候立刻执行。
问题
因为很多写 Swift
的同学都是从 Objective-C
过来的,从使用习惯上来说,用 inout
会多一些, defer
并不太多。 但是当 inout
和 defer
以及 return
共同起作用的时候往往会有一些微妙的问题。
defer
本身是个block
,他内部也是可以修改变量。比如:defer { x += 1 }
return
的时候,也是可以执行一个block
的。 比如return { x + 1 }()
,同时自身也会往栈上压入值inout
也是会改变入参的值。(再强调下,是复制->修改->写回,而不是原地修改)
假如一个 inout
入参,在 defer
中被修改,在 return
的 block
中被修改。 那么问题来了:
- 这几处修改的生效顺序是什么
- 对于
inout
和return
而言,外界拿到的是什么时候的什么值
用代码来举例子
1 | func fdo(x: inout Int) -> Int { |
问题:
- 几处修改何时生效
fdo
返回的值是多少- 最后的
outX
值又是多少
分析
为了能知道结果,对上述代码进行扩充
1 | func print(tag: String, v:Int) { |
输出如下:
1 | x-before: 10 |
几个关注点
- 打印顺序
- 各个节点的值
Swift
执行的是写时拷贝
除了打日志分析之外,也可以检查汇编代码。
简单分析一下可知如下执行顺序:
- 正常调用
- 此时内部生成临时变量
x'
,后续均操作此x'
- 此时内部生成临时变量
return
中block
执行defer
执行 (如果多个,按倒序)return
返回- 此时返回出来的是 临时变量
- 只是返回,函数调用 所在的整个语句并未执行
inout
生效- 用内部生成
x'
回填栈顶,即将入参的x
替换成x'
,调用方取回
- 用内部生成
- 函数调用 所在的代码行生效,使用
return
返回的值对外操作,如外赋值、打印等等
总结
从实际的实现角度来说,执行顺序是:
return中代码 -> defer
-> return
-> inout
-> 调用处代码
从变量和内存的角度考虑
Swift
是写时拷贝inout
是创建临时变量 -> 返回时用临时变量覆盖传入变量,而不是原地修改
从实际使用来说
- 对于
defer
使用,应遵循苹果的官方建议,主要用于关闭描述符、处理一些错误。虽然可以做一些额外的副作用,甚至可以根据执行的特性,来在return
之后再影响外部的一些东东,但是不建议做这些操作。 - 对于
inout
,需要了解执行的机制,不能根据现象对机制有所误判。 - 建议尽量少用 副作用 来达成一些预期,如在函数中过多的对外部变量进行修改,尤其是值类型的变量。