简介
默认情况下,Swift可以防止代码中发生不安全的行为.例如,Swift确保变量在使用之前进行初始化,在取消分配后不访问内存,并检查数组索引是否存在越界错误。
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中,有一些方法可以修改跨越多行代码的值,从而可以尝试在自己修改的过程中访问一个值。
如果您编写了并发或多线程代码,则对内存的冲突访问可能是一个熟悉的问题。但是,此处讨论的冲突访问可能发生在单个线程上,并且不涉及并发或多线程代码。
特征
-至少一个写操作
-访问内存中的相同位置
-他们的持续时间重叠
读写访问之间的区别:写访问权限会更改内存中的位置,但读访问权限则不会.内存中的位置指的是被访问的内容.例如,变量,常量或属性。存储器访问的持续时间是瞬时的或长期的。
如果在访问开始之后但在结束之前其他代码无法运行,则访问是即时的
函数具有对其所有输入输出参数的长期写访问权。
例如,下面代码清单中的所有读写访问都是即时的
func oneMore(than number: Int) -> Int {
return number + 1
}
var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// Prints "2"
但是,有几种方法可以访问内存,称为长期访问,跨越其他代码的执行.即时访问和长期访问之间的区别在于,其他代码可以在长期访问开始之后但在结束之前运行,这称为重叠。长期访问可以与其他长期访问和即时访问重叠。
重叠访问主要出现在使用函数和方法中的
in-out
修饰的参数或mutating
修饰的方法代码中。使用长期访问的特定Swift代码类型将在下面的部分中讨论。
in-out
in-out
是修饰函数参数类型,表示该参数在函数内修改后(即函数返回后),其值为修改后的值.
1,适用类型为变量
2,in-out
修饰后的参数,在传参时需&
修饰
in-out 访问冲突
var stepSize = 1
func increment(_ number: inout Int) {
number += stepSize
}
increment(&stepSize)
// Error: conflicting accesses to stepSize
解决
// Make an explicit copy.
var copyOfStepSize = stepSize
increment(©OfStepSize)
// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2
方法内访问冲突
struct Player {
var name: String
var health: Int
var energy: Int
static let maxHealth = 10
mutating func restoreHealth() {
health = Player.maxHealth
}
}
在上面的 restoreHealth()
方法中,对 self
的写访问权限从方法的开头开始,一直持续到方法返回为止.在这种情况下, restoreHealth()
中没有其他代码可以重叠访问Player实例的属性。下面的 shareHealth(with :)
方法将另一个 Player
实例作为 in-out
参数,从而创造了重叠访问的可能性。
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
的玩家调用shareHealth(with :)
方法与Maria
的玩家分享健康状况不会引起冲突.在方法调用期间有一个对oscar
的写访问权,因为oscar是变异方法中self
的值,并且在相同的持续时间内对maria进行写访问,因为maria作为in-out参数传递。
但是,如果您将
oscar
作为参数传递给shareHealth(with:)
,则存在冲突:
oscar.shareHealth(with: &oscar)
// Error: conflicting accesses to oscar
属性访问冲突
结构,元组和枚举等类型由单个组成值组成,例如结构的属性或元组的元素.因为这些是值类型,所以改变值的任何部分都会改变整个值,这意味着对其中一个属性的读或写访问需要对整个值的读或写访问权限
例如,重叠对元组元素的写访问会产生冲突:
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy) // Error
实际上,大多数对结构属性的访问都可以安全地重叠。
例如,如果上例中的变量holly更改为局部变量而不是全局变量,则编译器可以证明对结构的存储属性的重叠访问是安全的:
func someFunction() {
var oscar = Player(name: "Oscar", health: 10, energy: 10)
balance(&oscar.health, &oscar.energy) // OK
}
在上面的例子中,Oscar的health和energy 作为balance(: :)输入参数。编译器可以证明保留了内存安全性,因为两个存储的属性不会以任何方式进行交互。
为了保持存储器安全性,并不总是必须限制对结构属性的重叠访问.内存安全是理想的保证,但独占访问是比内存安全更严格的要求 - 这意味着一些代码可以保持内存安全,即使它违反了对内存的独占访问权限.如果编译器能够证明对内存的非独占访问仍然是安全的,那么Swift允许这种内存安全的代码。具体而言,如果满足以下条件,则可以证明对结构属性的重叠访问是安全的:
只访问实例的存储属性,而不是计算属性或类属性。
结构是局部变量的值,而不是全局变量。
该结构要么不被任何闭包捕获,要么仅由non-escaping
捕获。
参考资料