zoukankan      html  css  js  c++  java
  • Swift5.3 语言指南(二十七) 内存安全

    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
    ➤微信公众号:山青咏芝(shanqingyongzhi)
    ➤博客园地址:山青咏芝(https://www.cnblogs.com/strengthen/
    ➤GitHub地址:https://github.com/strengthen/LeetCode
    ➤原文地址:https://www.cnblogs.com/strengthen/p/9739940.html 
    ➤如果链接不是山青咏芝的博客园地址,则可能是爬取作者的文章。
    ➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

    默认情况下,Swift防止代码中发生不安全行为。例如,Swift确保在使用变量之前先对其进行初始化,在释放变量后不访问内存,并检查数组索引是否存在越界错误。

    通过要求修改内存中位置的代码具有对该内存的独占访问权限,Swift还确保对同一内存区域的多次访问不冲突。由于Swift会自动管理内存,因此大多数时候您根本不需要考虑访问内存。但是,重要的是要了解在何处可能发生冲突,因此可以避免编写对内存的访问有冲突的代码。如果您的代码中确实包含冲突,则会出现编译时或运行时错误。

    了解对内存的访问冲突

    当您执行诸如设置变量的值或将参数传递给函数之类的操作时,就会在代码中访问内存。例如,以下代码包含读取访问权限和写入访问权限:

    1. // A write access to the memory where one is stored.
    2. var one = 1
    3. // A read access from the memory where one is stored.
    4. print("We're number (one)!")

    当代码的不同部分试图同时访问内存中的同一位置时,可能会发生对内存的冲突访问。同时访问内存中的某个位置可能会产生不可预测的或不一致的行为。在Swift中,有多种方法可以修改跨越几行代码的值,从而可以在修改自身的过程中尝试访问值。

    通过考虑如何更新写在纸上的预算,您可以看到类似的问题。更新预算分为两个步骤:首先添加项目的名称和价格,然后更改总金额以反映当前列表中的项目。在更新前后,您可以从预算中读取任何信息并获得正确的答案,如下图所示。

    ../_images/memory_shopping_2x.png

    在将项目添加到预算时,它处于临时无效状态,因为尚未更新总金额以反映新添加的项目。在添加项目的过程中读取总金额会给您错误的信息。

    此示例还演示了在解决冲突的内存访问时可能遇到的挑战:有时有多种方法可以解决冲突,从而产生不同的答案,并且哪个答案正确并不总是很明显。在此示例中,根据您是要原始总金额还是更新后的总金额,$ 5或$ 320可能是正确的答案。在解决冲突访问之前,您必须确定打算执行的操作。

    注意

    如果您编写了并发或多线程代码,则对内存的访问冲突可能是一个熟悉的问题。但是,这里讨论的冲突访问可以在单个线程上发生,并且涉及并发或多线程代码。

    如果您在单个线程中对内存的访问存在冲突,Swift保证您将在编译时或运行时收到错误。对于多线程代码,请使用Thread Sanitizer帮助检测跨线程的冲突访问。

    内存访问的特征

    在冲突的访问环境中要考虑内存访问的三个特征:访问是读还是写,访问的持续时间以及要访问的内存位置。具体来说,如果您具有两个满足以下所有条件的访问权限,则会发生冲突:

    • 至少一个是写访问权限。
    • 它们访问内存中的相同位置。
    • 它们的持续时间重叠。

    读和写访问之间的区别通常很明显:写访问会更改内存中的位置,但读访问不会。内存中的位置是指所访问的内容,例如,变量,常量或属性。内存访问的持续时间是瞬时的或长期的。

    如果在访问开始之后但结束之前不可能运行其他代码,则访问是瞬时的。从本质上讲,两个瞬时访问不能同时发生。大多数内存访问是瞬时的。例如,以下代码清单中的所有读取和写入访问都是瞬时的:

    1. func oneMore(than number: Int) -> Int {
    2. return number + 1
    3. }
    4. var myNumber = 1
    5. myNumber = oneMore(than: myNumber)
    6. print(myNumber)
    7. // Prints "2"

    但是,有几种访问内存的方法(称为长期访问)可以跨越其他代码的执行。瞬时访问和长期访问之间的区别在于,其他代码有可能在长期访问开始之后但在结束之前运行,这被称为overlay长期访问可以与其他长期访问和瞬时访问重叠。

    重叠访问主要出现在代码中,该代码在结构的函数和方法或变异方法中使用输入/输出参数。在以下各节中讨论了使用长期访问的特定类型的Swift代码。

    对In-Out参数的访问冲突

    函数可以对其所有输入输出参数进行长期写访问。在对所有非输入参数进行评估之后,将开始对输入输出参数进行写访问,并持续该函数调用的整个过程。如果有多个输入/输出参数,则写入访问的开始顺序与参数出现的顺序相同。

    这种长期写访问的结果是,即使作用域规则和访问控制允许这样做,您也无法访问以in-out形式传递的原始变量-对原始文件的任何访问都会产生冲突。例如:

    1. var stepSize = 1
    2. func increment(_ number: inout Int) {
    3. number += stepSize
    4. }
    5. increment(&stepSize)
    6. // Error: conflicting accesses to stepSize

    在上面的代码中,stepSize是一个全局变量,通常可以从内部访问它increment(_:)但是,对的读取访问stepSize与对的写入访问重叠number如下图所示,两者number和都stepSize指向内存中的同一位置。读取和写入访问引用相同的内存,并且它们重叠,从而产生冲突。

    ../_images/memory_increment_2x.png

    解决此冲突的一种方法是显式复制以下内容stepSize

    1. // Make an explicit copy.
    2. var copyOfStepSize = stepSize
    3. increment(&copyOfStepSize)
    4. // Update the original.
    5. stepSize = copyOfStepSize
    6. // stepSize is now 2

    stepSize在调用之前制作的副本时increment(_:),很明显,的值copyOfStepSize会增加当前步长。读访问在写访问开始之前结束,因此没有冲突。

    长期对in-out参数进行写访问的另一个结果是,将单个变量作为同一函数的多个in-out参数的参数传递会产生冲突。例如:

    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(_:_:)上面函数修改了它的两个参数,以在它们之间平均分配总值。playerOneScoreplayerTwoScore作为参数调用不会产生冲突-有两个时间重叠的写访问,但是它们访问内存中的不同位置。相反,传递playerOneScore两个参数的值会产生冲突,因为它试图同时对内存中的同一位置执行两次写访问。

    注意

    由于运算符是函数,因此他们也可以长期访问其输入输出参数。例如,如果balance(_:_:)是一个名为的运算符<^>,则写入将导致与相同的冲突playerOneScore <^> playerOneScorebalance(&playerOneScore, &playerOneScore)

    方法中的自我获取冲突

    self在方法调用期间,结构上的变异方法具有写权限例如,考虑一个游戏,其中每个玩家的健康值在受到伤害时都会减少,能量值在使用特殊能力时会减少。

    1. struct Player {
    2. var name: String
    3. var health: Int
    4. var energy: Int
    5. static let maxHealth = 10
    6. mutating func restoreHealth() {
    7. health = Player.maxHealth
    8. }
    9. }

    在上述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. var oscar = Player(name: "Oscar", health: 10, energy: 10)
    7. var maria = Player(name: "Maria", health: 5, energy: 10)
    8. oscar.shareHealth(with: &maria) // OK

    在上面的示例中,调用shareHealth(with:)Oscar的播放器与Maria的播放器共享健康方法不会引起冲突。oscar在方法调用过程中可以进行写访问,因为它oscarselfmutating方法中的值,而maria在相同的持续时间内可以进行写访问,因为它maria是作为in-out参数传递的。如下图所示,它们访问内存中的不同位置。即使两个写访问在时间上重叠,它们也不会冲突。

    ../_images/memory_share_health_maria_2x.png

    但是,如果将oscar用作参数,则会shareHealth(with:)发生冲突:

    1. oscar.shareHealth(with: &oscar)
    2. // Error: conflicting accesses to oscar

    mutation方法需要self在该方法的持续时间内进行写访问,而in-out参数需要teammate在相同的持续时间内进行写访问在该方法中,两个self和都teammate指向内存中的同一位置-如下图所示。这两个写访问引用相同的内存,并且它们重叠,从而产生冲突。

    ../_images/memory_share_health_oscar_2x.png

    访问属性冲突

    诸如结构,元组和枚举之类的类型由各个组成值组成,例如结构的属性或元组的元素。因为这些是值类型,所以对值的任何部分进行更改都会对整个值进行更改,这意味着对属性之一的读或写访问要求对整个值的读或写访问。例如,对元组元素的重叠写访问会产生冲突:

    1. var playerInformation = (health: 10, energy: 20)
    2. balance(&playerInformation.health, &playerInformation.energy)
    3. // Error: conflicting access to properties of playerInformation

    在上面的示例中,调用balance(_:_:)元组的元素会产生冲突,因为对的写入访问重叠playerInformation双方playerInformation.healthplayerInformation.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允许使用此内存安全代码。具体来说,如果满足以下条件,则可以证明重叠访问结构的属性是安全的:

    • 您仅访问实例的存储属性,而不访问计算的属性或类属性。
    • 结构是局部变量的值,而不是全局变量的值。
    • 该结构要么没有被任何闭包捕获,要么仅被不冒号的闭包捕获。

    如果编译器无法证明访问是安全的,则它不允许访问。

  • 相关阅读:
    Java实现 LeetCode 833 字符串中的查找与替换(暴力模拟)
    Java实现 LeetCode 833 字符串中的查找与替换(暴力模拟)
    Java实现 LeetCode 833 字符串中的查找与替换(暴力模拟)
    Java实现 LeetCode 832 翻转图像(位运算)
    Java实现 LeetCode 832 翻转图像(位运算)
    Java实现 LeetCode 832 翻转图像(位运算)
    Java实现 LeetCode 831 隐藏个人信息(暴力)
    Java实现 LeetCode 831 隐藏个人信息(暴力)
    Java实现 LeetCode 831 隐藏个人信息(暴力)
    how to use automapper in c#, from cf~
  • 原文地址:https://www.cnblogs.com/strengthen/p/9739940.html
Copyright © 2011-2022 走看看