对于 in-out 参数,在整个函数调用周期内,不可再以其它方式访问 in-out 参数(只能以形参名称访问),所有的访问都必须通过 in-out 形参直接或是间接进行。
默认下 Swift 会阻止代码出现不安全行为,如 Swift 会保证变量在使用前被初始化、内存在回收后不会被访问、会检测数组越界错误等。Swift 保证同一内存区域的多个访问不会冲突,会自动管理内存,码农大多数时候不需要考虑内存访问。然而,理解冲突可能出现在哪里是必要的,尽早避免,尽少依靠编译器提示和运行时错误。
在 OC 中通过锁手动管理数据的多线程安全,在 Swift 中也是一样。这里讨论的 Swift 提供的内存访问安全机制是在单线程情况下发生的内存安全问题,会有编译时或运行时错误出现
理解内存访问冲突
内存读写和参数传递都会访问内存。
// A write access to the memory where one is stored.
var one = 1// A read access from the memory where one is stored.
print("We're number \(one)!")
在 Swift 中,有多种方式修改一个跨越多行代码的值,所以很容易出现,在修改还没有结束时,再次访问该值得情况。
In Swift, there are ways to modify a value that span several lines of code, making it possible to attempt to access a value in the middle of its own modification.
如下面账单操作过程,从加入新的产品之前、添加中到添加后,是一个过程的,中间存在多种对总价访问的可能性。单个产品的加入和总价格的改变不是同步进行的。
内存访问特性
发生访问冲突的必要条件是:
- 访问中存在写操作
- 访问相同的内存位置
- 访问时间有重叠
瞬时访问
内存访问的持续时间分为瞬时和时段。如果当前内存访问开始后和结束前的时段内,其它访问不能操作相同的内存区域,则当前内存访问就是瞬时访问。因此,本质上两个瞬时访问是无法同时发生的,大多数内存访问都是瞬时访问。
func oneMore(than number: Int) -> Int {
return number + 1
}
var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// Prints "2"
时段访问
与瞬时访问不同的是,在时段访问中,即在访问开始后结束前,其它访问可以访问相同的内存区域,即发生了访问重叠。访问重叠主要发生在使用输入输出参数的函数和方法中、**结构体 **mutating 方法中。
输入输出In-Out参数访问冲突
函数对输入输出参数进行**时段访问。**在评估完所有的非输入输出参数后,开始写访问,并持续整个函数调用周期内(defer也包括在内)。如果有多个 in-out参数,写顺序与参数顺序一样。
var stepSize = 1func increment(_ number: inout Int) {
number += stepSize
}increment(&stepSize)
// Error: conflicting accesses to stepSize
stepSize 作为一个全局变量,以 in-out参数传递给 increment,而 increment 内部又通过 += stepSize 访问了该变量,引起反问冲突。
stepSize作为 in-out参数,在 increment 内部不可以访问 stepSize 变量,只能访问形参number。number 和 stepSize 指向同一个位置的内存,而 number 具有写权限。
可以通过拷贝 stepSize 解决上面冲突:
// Make an explicit copy.
var copyOfStepSize = stepSize
increment(©OfStepSize)
// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2
单个变量不能作为两个 inout 参数,同时传递给函数:
func balance(_ x: inout Int, _ y: inout Int) {
let sum = x + y
x = sum / 2
y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore)
balance(&playerOneScore, &playerOneScore) //把playerOneScore 作为不同的inout同时传入
// Error: conflicting accesses to playerOneScore
把 playerOneScore 作为不同的 inout 同时传入到 balance ,引起冲突。
在函数的调用中,同一个变量只能作为一个 in-out参数。
操作符也是函数,也会出现访问冲突。
在方法中访问 self 冲突
结构体中的 mutating 方法,在方法调用中,具有对 self 的写访问权。
struct Player {
var name: String
var health: Int
var energy: Int
static let maxHealth = 10
mutating func restoreHealth() {
health = Player.maxHealth
}
}
restoreHealth 对 health 的写不会发生冲突。
extension Player {
mutating func shareHealth(with teammate: inout Player) {
balance(&teammate.health, &health)
}
}
var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria) // OK
oscar 和 maria 实例各自有自己的 health,在 shareHealth 中,health 是 oscar 的属性,teammate 是 maria,属性访问不冲突;shareHealth 是 mutating 函数,改变的是 oscar,而传入的是 maria,self 访问不冲突。
oscar.shareHealth(with: &oscar)
// Error: conflicting accesses to oscar
上面代码对 self 冲突,因为 shareHealth 是 mutating 函数。
属性访问冲突
类型,如结构体、元组、枚举,是有各个独立值组成,如结构体的属性和元组的元素。因为它们都是值类型,改变值的任何部分就会改变整个值,意味着读写单个属性都会要求对整个值的读写访问控制。
var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// Error: conflicting access to properties of playerInformation
元组 playerInformation,作为一个整体,对其任何属性的访问都会要求对整体结构体体的读写控制权限,所以 balance(&playerInformation.health, &playerInformation.energy) 会产生冲突。同理,对 Player 结构体的属性的访问也会导致访问冲突:
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy) // Error
值类型的本地变量的属性访问冲突推断
使用本地变量后,编译器会推断出,两个存储属性不会以任何方式发生交互,访问 Player 的属性不会发生冲突。
func someFunction() {
var oscar = Player(name: "Oscar", health: 10, energy: 10)
balance(&oscar.health, &oscar.energy) // OK
}
如满足以下几个条件,访问值类型变量的属性不会发生冲突:
- 只访问存储属性,不访问计数属性
- 是本地值变量
- 值类型变量至多有一个闭包捕获了它