Swift 初阶 —— inout 参数 & 独占访问问题 (Exclusive access)
一、前言
我们都知道, 在 swift 里 inout 参数是用来修改实参的. 但其实 inout 参数的使用是要遵循“独占访问”的, 要不然编译器会报错或警告.
inout 参数实际上只把对象的地址传了进来.
二、独占访问 (Exclusive accesses)
1、独占访问是什么
在 Swift 官方文档中,它是这样描述的:
意思就是: 当一个值类型的变量作为 inout 或 mutating 参数传进当前函数时, 你的代码不能通过以下方式再次访问这个变量:
- 在执行当前函数期间, 通过某个闭包的捕获并访问这个值类型变量
- 给当前函数的其它 inout 参数传同一个值类型变量
否则就会报类似 “error: overlapping accesses to 'count', but modification requires exclusive access; consider copying to a local variable” 的错误.
2、独占访问如何产生
当一个变量以 inout 参数或以 mutating + self 的方式传入当前函数, 整个函数体就成为了这个变量的独占访问区. 意思就是说, 你不能在这个独占访问区通过上面两种方式再次访问这个变量.
3、为什么只有值类型才会有独占访问, 引用类型没有
这是官方的解释:
意思就是说: 值类型的任何一个实例属性都是这个值的一部份, 因此如果要访问值类型的实例属性, 就要访问整个值(实例对象).
但类是引用类型, 因此可以把类属性的访问看成访问独立的内存,而不是访问整个实例对象, 因此独占访问只有值类型才有.
4、特例(编译器优化)

对于 struct 来说, 如果在同一个语句开始访问两个不相交的存储形属性,编译器并不会报访问重叠/独占访问的错误.
举个🌰:
图 2 没报错原因:
首先这是在同一个语句下, 其次 appState 的 counter 和 appState 的 limit 这两个属性是没有交集的, 因此把这两块内存传到 value 和 max, 不会引起访问重叠的错误. 但这只是对 struct 的优化, 并不是对所有值类型对象的优化.
再举一个🌰:
func modifyX(x: inout Int, updater: (Int)->Int) {
x = updater(x)
}
func testDisjointStructProperties(point: inout Point) {
modifyX(x: &point.x, updater: { // First `point` access
let oldy = point.y // Second `point` access
point.y = $0; // ...allowed as an exception to the rule.
return oldy
})
}
testDisjointStructProperties(point:) 中的 modify 调用也是一个语句.
5、独占访问/访问重叠经典例子
5.1. 由多个 inout 参数创建的独占访问重叠
a. 同一个变量

b. 同一个数组(因为数组是内存连续的)

c. 同一个 struct 对象
图 1 报错原因:
因为 state 传的是整个 appState, 所以 update() 函数体成为了 appState 的独占访问区, 但 max 传的又是 appState 的属性;且 appState 又是值类型, 所以其实 max 又会创建一个 appState 的独占访问. 于是现在就有两个 appState 的独占访问就重叠了. 所以就报错了.
5.2. 闭包导致的独占访问重叠
func modifyTwice(_ value: inout Int, by modifier: (inout Int) -> ()) {
modifier(&value)
modifier(&value)
}
func testCount() {
var count = 1
modifyTwice(&count) { $0 += count } // error: overlapping accesses to 'count', but modification requires exclusive access; consider copying to a local variable
print(count)
}
报错原因: modifyTwice 传入了 count, 所以整个 modifyTwice 函数体都是 count (value) 独占访问区, 但是 modifyTwice 传入的闭包捕获并访问了外部的 count, 也就是 modifyTwice 传入的 count, 但此时 modifyTwice 只能通过 value 来访问 count, 因此用闭包来访问 count, 从而避开了通过 value 访问 count 是会导致访问中重叠的.
三、补充: mutating 方法里使用逃逸闭包, 并在逃逸闭包里向 inout 参数写入(非独占访问问题)
图 3 报错原因:
- inout 参数实际上只把对象的地址传了进来.
- mutating 方法实际会把 self 以 inout 参数的形式传进来. 而匿名闭包会捕获外层函数的实参和局部变量, 所以 async 后的闭包捕获的其实是 self 的地址.
- Swift 无法保证闭包执行时这个 self 依然存在——因为方法返回后,这个实例可能已经被销毁了. 因为可以假设一种情况: 调用 performUpdate() 的变量其实也是另一个函数 (假设名字为 anotherFunc) 里的局部变量, 那么一旦执行完 anotherFunc, 变量销毁; 如果之后才执行 performUpdate() 里 async 闭包的代码, self 就会造成野指针访问了.
- 因为这里的 self 对应的类型是 struct (值类型)而不是 class, 所以不用对 self 考虑 ARC 问题.
图 4 为正确解决方案:
当我们把 self 声明在捕获列表里后, swift 实际上会把 self 拷贝出另一个只读的副本, 然后再让闭包捕获这个不可变的副本. 所以闭包读到的 self 和 mutating 捕获到的 self 就不是同一块内存了, 就保证了 mutating 函数的 self 的数据独占.
四、总结
所以综上所述, swift 对 inout 参数的规定就是: inout 参数只能在当前函数的作用域内访问, 同时不能访问重叠. 除此之外, mutating 函数、setter、didSet & willSet 实际上都会把 self 以 inout 的方式传进来, 因此不能在这些函数里建一个逃逸闭包, 然后再在这个逃逸闭包里向 self 写值.
Swift 5 Exclusivity Enforcement
https://www.swift.org/blog/swift-5-exclusivity/
更多推荐


所有评论(0)