Swift4.2语言规范(二十六) 内存安全
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了Swift4.2语言规范(二十六) 内存安全,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含8441字,纯文字阅读大概需要13分钟。
内容图文
默认情况下,Swift可以防止代码中发生不安全行为。例如,Swift确保变量在使用之前进行初始化,在取消分配后不访问内存,并检查数组索引是否存在越界错误。
Swift还确保对同一内存区域的多次访问不会发生冲突,因为需要修改内存中某个位置的代码才能对该内存进行独占访问。因为Swift自动管理内存,所以大多数时候你根本不需要考虑访问内存。但是,了解潜在冲突可能发生的位置非常重要,这样您就可以避免编写对内存具有冲突访问权限的代码。如果您的代码确实包含冲突,那么您将收到编译时或运行时错误。
了解对内存的冲突访问
当您执行诸如设置变量的值或将参数传递给函数之类的操作时,会在代码中访问内存。例如,以下代码包含读访问和写访问:
1 // A write access to the memory where one is stored. 2 var one = 1 3 4 // A read access from the memory where one is stored. 5 print("We're number \(one)!")
当代码的不同部分试图同时访问内存中的相同位置时,可能会发生冲突的内存访问。同时多次访问内存中的某个位置会产生不可预测或不一致的行为。在Swift中,有一些方法可以修改跨越多行代码的值,从而可以尝试在自己的修改过程中访问一个值。
通过考虑如何更新写在纸上的预算,您可以看到类似的问题。更新预算的过程分为两步:首先添加项目的名称和价格,然后更改总金额以反映列表中当前的项目。在更新之前和之后,您可以阅读预算中的任何信息并获得正确答案,如下图所示。
当您将项目添加到预算时,它处于临时无效状态,因为总金额尚未更新以反映新添加的项目。在添加项目的过程中读取总金额会给您提供不正确的信息。
此示例还演示了在修复对内存的冲突访问时可能遇到的挑战:有时有多种方法可以解决产生不同答案的冲突,并且并不总是很明显哪个答案是正确的。在此示例中,根据您是否需要原始总金额或更新的总金额,$ 5或$ 320可能是正确的答案。在修复冲突访问之前,您必须确定要执行的操作。
注意
如果您编写了并发或多线程代码,则对内存的冲突访问可能是一个熟悉的问题。但是,此处讨论的冲突访问可能发生在单个线程上,并且不涉及并发或多线程代码。
如果您在单个线程内存在对内存的冲突访问,Swift会保证您在编译时或运行时都会收到错误。对于多线程代码,请使用Thread Sanitizer帮助检测跨线程的冲突访问。
内存访问的特征
在冲突访问的上下文中,要考虑存储器访问的三个特征:访问是读取还是写入,访问的持续时间以及访问的内存中的位置。具体而言,如果您有两次访问满足以下所有条件,则会发生冲突:
- 至少一个是写访问。
- 他们访问内存中的相同位置。
- 他们的持续时间重叠。
读取和写入访问之间的区别通常很明显:写入访问会更改内存中的位置,但读取访问权限则不会。内存中的位置是指正在访问的内容 - 例如,变量,常量或属性。存储器访问的持续时间是瞬时的或长期的。
如果在访问开始之后但在结束之前其他代码无法运行,则访问是即时的。就其本质而言,两次即时访问不可能同时发生。大多数内存访问都是即时的。例如,下面代码清单中的所有读写访问都是即时的:
1 func oneMore(than number: Int) -> Int { 2 return number + 1 3 } 4 5 var myNumber = 1 6 myNumber = oneMore(than: myNumber) 7 print(myNumber) 8 // Prints "2"
但是,有几种方法可以访问内存,称为长期访问,跨越其他代码的执行。即时访问和长期访问之间的区别在于,其他代码可以在长期访问开始之后但在结束之前运行,这称为重叠。长期访问可以与其他长期访问和即时访问重叠。
重叠访问主要出现在使用函数和方法中的输入输出参数或结构的变异方法的代码中。使用长期访问的特定Swift代码类型将在下面的部分中讨论。
对输入输出参数的访问冲突
函数具有对其所有输入输出参数的长期写访问权。在对所有非输入参数进行评估并且持续该函数调用的整个持续时间之后,对输入输出参数的写访问开始。如果有多个输入输出参数,则写访问的开始顺序与参数显示的顺序相同。
这种长期写入访问的一个后果是您无法访问作为输入输出传递的原始变量,即使范围规则和访问控制允许它 - 任何对原始数据的访问都会产生冲突。例如:
1 var stepSize = 1 2 3 func increment(_ number: inout Int) { 4 number += stepSize 5 } 6 7 increment(&stepSize) 8 // Error: conflicting accesses to stepSize
在上面的代码中,stepSize
是一个全局变量,它通常可以从内部访问increment(_:)
。但是,读访问stepSize
与写访问重叠number
。如在下文中的图所示,两者number
并stepSize
指代相同的位置在存储器中。读和写访问指的是相同的内存,它们重叠,产生冲突。
解决这种冲突的一种方法是制作一份明确的副本stepSize
:
1 // Make an explicit copy. 2 var copyOfStepSize = stepSize 3 increment(©OfStepSize) 4 5 // Update the original. 6 stepSize = copyOfStepSize 7 // stepSize is now 2
当你stepSize
在调用之前复制一个副本时increment(_:)
,很明显,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)
self
方法的冲突
结构上的变异方法在self
方法调用期间具有写访问权。例如,考虑一个游戏,其中每个玩家的健康量在受到伤害时减少,而能量值在使用特殊能力时减少。
1 struct Player { 2 var name: String 3 var health: Int 4 var energy: Int 5 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 } 6 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:)
,则存在冲突:
- oscar.shareHealth(with: &oscar)
- // Error: conflicting accesses to oscar
变换方法需要self
在方法持续时间内进行写访问,并且输入输出参数需要teammate
对相同持续时间的写访问权。在该方法中,无论是self
和teammate
指的是相同的位置在内存如示于下图中。两次写访问指的是相同的内存,它们重叠,产生冲突。
对属性的访问冲突
结构,元组和枚举等类型由单个组成值组成,例如结构的属性或元组的元素。因为这些是值类型,所以改变值的任何部分都会改变整个值,这意味着对其中一个属性的读或写访问需要对整个值进行读或写访问。例如,重叠对元组元素的写访问会产生冲突:
1 var playerInformation = (health: 10, energy: 20) 2 balance(&playerInformation.health, &playerInformation.energy) 3 // Error: conflicting access to properties of playerInformation
在上面的示例中,调用balance(_:_:)
元组的元素会产生冲突,因为存在重叠的写访问playerInformation
。双方playerInformation.health
并playerInformation.energy
都在出参数,这意味着通过balance(_:_:)
需要写访问他们的函数调用的持续时间。在这两种情况下,对元组元素的写访问都需要对整个元组进行写访问。这意味着有两次写访问playerInformation
,持续时间重叠,导致冲突。
下面的代码显示,对于存储在全局变量中的结构属性的重叠写访问,会出现相同的错误。
1 var holly = Player(name: "Holly", health: 10, energy: 10) 2 balance(&holly.health, &holly.energy) // Error
实际上,大多数对结构属性的访问都可以安全地重叠。例如,如果上例中的变量holly
更改为局部变量而不是全局变量,则编译器可以证明对结构的存储属性的重叠访问是安全的:
1 func someFunction() { 2 var oscar = Player(name: "Oscar", health: 10, energy: 10) 3 balance(&oscar.health, &oscar.energy) // OK 4 }
在上面的例子中,奥斯卡的健康和能量作为两个输入参数传递给balance(_:_:)
。编译器可以证明保留了内存安全性,因为两个存储的属性不以任何方式交互。
对于保持存储器安全性并不总是必须限制对结构属性的重叠访问。内存安全是理想的保证,但独占访问是比内存安全更严格的要求 - 这意味着一些代码可以保持内存安全,即使它违反了对内存的独占访问权限。如果编译器可以证明对内存的非独占访问仍然是安全的,那么Swift允许这种内存安全的代码。具体而言,如果满足以下条件,则可以证明对结构属性的重叠访问是安全的:
- 您只访问实例的存储属性,而不是计算属性或类属性。
- 结构是局部变量的值,而不是全局变量。
- 该结构要么不被任何闭包捕获,要么仅由非加密闭包捕获。
如果编译器无法证明访问是安全的,则不允许访问。
内容总结
以上是互联网集市为您收集整理的Swift4.2语言规范(二十六) 内存安全全部内容,希望文章能够帮你解决Swift4.2语言规范(二十六) 内存安全所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。