第24章:内存安全
默认情况下,Swift可以防止代码中发生不安全行为。例如,Swift确保变量在使用之前进行初始化,在取消分配后不访问内存,并检查数组索引是否存在越界错误。
Swift还确保对同一内存区域的多次访问不会发生冲突,因为需要修改内存中某个位置的代码才能对该内存进行独占访问。因为Swift自动管理内存,所以大多数时候你根本不需要考虑访问内存。但是,了解潜在冲突可能发生的位置非常重要,这样您就可以避免编写对内存具有冲突访问权限的代码。如果您的代码确实包含冲突,则会出现编译时或运行时错误。
23.1 了解对内存的冲突访问
当你执行诸如设置变量的值或将参数传递给函数之类的操作时,会在代码中访问内存。例如,以下代码包含读访问和写访问:
// 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中,有一些方法可以修改跨越多行代码的值,从而可以尝试在自己修改的过程中访问一个值。
通过考虑如何更新写在纸上的预算,您可以看到类似的问题。更新预算的过程分为两步:首先添加项目的名称和价格,然后更改总金额以反映列表中当前的项目。在更新之前和之后,您可以从预算中读取任何信息并获得正确答案,如下图所示。
当您在预算中添加项目时,它处于临时无效状态,因为总金额尚未更新以反映新添加的项目。在添加项目的过程中读取总金额会给您不正确的信息。
此示例还演示了在修复对内存的冲突访问时可能遇到的挑战:有时有多种方法可以解决产生不同答案的冲突,并且并不总是很明显哪个答案是正确的。在此示例中,根据您是否需要原始总金额或更新的总金额, 内存访问冲突
解决这种冲突的一种方法是制作一份明确的副本stepSize
:
1. // Make an explicit copy.
2. var copyOfStepSize = stepSize
3. increment(©OfStepSize)
5. // Update the original.
6. stepSize = copyOfStepSize
7. // stepSize is now 2
当您在调用之前复制stepSize
,很明显,copyOfStepSize
当前步长会增加值。读访问在写访问开始之前结束,因此不存在冲突。
对输入输出参数进行长期写访问的另一个后果是,将单个变量作为同一函数的多个输入输出参数的参数传递会产生冲突。例如:
1. func balance(_ x: inout Int, _ y: inout Int) {
2. let sum = x + y
3. x = sum / 2
4. y = sum - x
5. }
6. var playerOneScore = 42
7. var playerTwoScore = 30
8. balance(&playerOneScore, &playerTwoScore) // OK
9. balance(&playerOneScore, &playerOneScore)
10. // Error: conflicting accesses to playerOneScore
上面的balance(_:_:)
函数修改了它的两个参数,以便在它们之间平均分配总值。使用playerOneScore
和playerTwoScore
作为参数调用它不会产生冲突 - 有两个写访问在时间上重叠,但它们访问内存中的不同位置。相反,playerOneScore
作为两个参数的值传递会产生冲突,因为它试图同时对内存中的同一位置执行两次写访问。
注意
由于运算符是函数,因此它们也可以长期访问其输入输出参数。例如,如果balance(_:_:)
是一个名为的运算符函数<^>
,则写入将导致与之相同的冲突。playerOneScore <^> playerOneScore``balance(&playerOneScore, &playerOneScore)
23.4 方法中的self冲突
结构上的变异方法在self
方法调用期间具有写访问权。例如,考虑一种游戏,其中每个玩家具有健康量,其在受到伤害时减少,并且能量量在使用特殊能力时减少。
1. struct Player {
2. var name: String
3. var health: Int
4. var energy: Int
6. static let maxHealth = 10
7. mutating func restoreHealth() {
8. health = Player.maxHealth
9. }
10. }
在上面的restoreHealth()
方法中,写入访问self
从方法的开头开始并持续到方法返回。在这种情况下,内部没有其他代码restoreHealth()
可以重叠访问Player
实例的属性。shareHealth(with:)
下面的方法将另一个Player
实例作为输入输出参数,从而创建重叠访问的可能性。
1. extension Player {
2. mutating func shareHealth(with teammate: inout Player) {
3. balance(&teammate.health, &health)
4. }
5. }
7. var oscar = Player(name: "Oscar", health: 10, energy: 10)
8. var maria = Player(name: "Maria", health: 5, energy: 10)
9. oscar.shareHealth(with: &maria) // OK
在上面的示例中,调用shareHealth(with:)
Oscar的播放器与Maria的播放器共享健康状态的方法不会导致冲突。oscar
在方法调用期间存在写访问权,因为它oscar
是self
变异方法中的值,并且maria
在相同的持续时间内存在写访问权,因为它maria
是作为输入输出参数传递的。如下图所示,它们访问内存中的不同位置。即使两个写访问在时间上重叠,它们也不会发生冲突。
但是,如果您oscar
作为参数传递shareHealth(with:)
,则存在冲突:
1. oscar.shareHealth(with: &oscar)
2. // Error: conflicting accesses to oscar
变换方法需要self
在方法的持续时间内进行写访问,并且输入输出参数需要teammate
对相同持续时间的写访问权。在该方法中,无论是self
和teammate
指的是相同的位置在内存如示于下图中。两次写访问指的是相同的内存,它们重叠,产生冲突。
23.4 属性的访问冲突
结构,元组和枚举等类型由单个组成值组成,例如结构的属性或元组的元素。因为这些是值类型,所以改变值的任何部分都会改变整个值,这意味着对其中一个属性的读取或写入访问需要对整个值进行读取或写入访问。例如,重叠对元组元素的写访问会产生冲突:
var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// Error: conflicting access to properties of playerInformation
在上面的示例中,调用balance(::)元组的元素会产生冲突,因为存在重叠的写访问playerInformation。双方playerInformation.health并playerInformation.energy都在出参数,这意味着通过balance(::)需要写访问他们的函数调用的持续时间。在这两种情况下,对元组元素的写访问都需要对整个元组进行写访问。这意味着有两次写访问playerInformation,持续时间重叠,导致冲突。
下面的代码显示,对于存储在全局变量中的结构属性的重写写访问,会出现相同的错误。
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
}
在上面的例子中,奥斯卡的健康和能量作为两个输入参数传递给balance(::)。编译器可以证明保留了内存安全性,因为两个存储的属性不会以任何方式进行交互。
为了保持存储器安全性,并不总是必须限制对结构属性的重叠访问。内存安全是理想的保证,但独占访问是比内存安全更严格的要求 - 这意味着一些代码可以保持内存安全,即使它违反了对内存的独占访问权限。如果编译器可以证明对内存的非独占访问仍然是安全的,那么Swift允许这种内存安全的代码。具体而言,如果满足以下条件,则可以证明对结构属性的重叠访问是安全的:
- 您只访问实例的存储属性,而不是计算属性或类属性。
- 结构是局部变量的值,而不是全局变量。
- 该结构要么不被任何闭包捕获,要么仅由非脱节闭包捕获。
如果编译器无法证明访问是安全的,则不允许访问。