默認(rèn)情況下,Swift可以防止代碼中發(fā)生不安全行為曙旭。例如别垮,Swift確保變量在使用前被初始化便监,內(nèi)存被釋放后不會(huì)被訪問(wèn),并且檢查數(shù)組索引是否有越界錯(cuò)誤宰闰。
Swift還要求修改內(nèi)存中某個(gè)位置的代碼具有對(duì)該內(nèi)存的獨(dú)占訪問(wèn)權(quán)茬贵,從而確保對(duì)同一內(nèi)存區(qū)域的多次訪問(wèn)不會(huì)發(fā)生沖突。因?yàn)镾wift是自動(dòng)管理內(nèi)存的移袍,所以大多數(shù)時(shí)候你根本不用考慮訪問(wèn)內(nèi)存解藻。但是,了解可能發(fā)生沖突的地方很重要葡盗,這樣您就可以避免編寫訪問(wèn)內(nèi)存沖突的代碼螟左。如果您的代碼確實(shí)包含沖突,則會(huì)得到編譯時(shí)或運(yùn)行時(shí)錯(cuò)誤觅够。
Understanding Conflicting Access to Memory
當(dāng)您設(shè)置變量的值或?qū)?shù)傳遞給函數(shù)時(shí)胶背,對(duì)內(nèi)存的訪問(wèn)發(fā)生在代碼中。例如喘先,下面的代碼包含讀訪問(wèn)和寫訪問(wèn):
// 對(duì)存儲(chǔ)1的內(nèi)存進(jìn)行寫訪問(wèn)
var one = 1
//對(duì)存儲(chǔ)1的內(nèi)存進(jìn)行讀訪問(wèn)
print("We're number \(one)!")
當(dāng)代碼的不同部分試圖同時(shí)訪問(wèn)內(nèi)存中的相同位置時(shí)钳吟,可能會(huì)發(fā)生對(duì)內(nèi)存的沖突訪問(wèn)。同時(shí)多次訪問(wèn)內(nèi)存中的某個(gè)位置可能會(huì)產(chǎn)生不可預(yù)測(cè)或不一致的行為窘拯。在Swift中红且,有幾種方法可以修改跨越幾行代碼的值,從而使在修改過(guò)程中訪問(wèn)值成為可能涤姊。
當(dāng)您向預(yù)算添加項(xiàng)目時(shí)暇番,它處于臨時(shí)的無(wú)效狀態(tài),因?yàn)榭偨痤~沒(méi)有更新以反映新添加的項(xiàng)目思喊。在添加項(xiàng)目的過(guò)程中讀取總金額會(huì)給出不正確的信息壁酬。
這個(gè)示例還演示了在修復(fù)對(duì)內(nèi)存的沖突訪問(wèn)時(shí)可能遇到的一個(gè)挑戰(zhàn):有時(shí)有多種方法可以修復(fù)產(chǎn)生不同答案的沖突,而且并不總是很明顯哪個(gè)答案是正確的恨课。在本例中舆乔,根據(jù)需要原始總額還是更新后的總額,正確的答案可能是320庄呈。在修復(fù)沖突訪問(wèn)之前蜕煌,您必須確定它的目的是什么。
如果您已經(jīng)編寫了并發(fā)或多線程代碼诬留,那么對(duì)內(nèi)存的沖突訪問(wèn)可能是一個(gè)常見(jiàn)的問(wèn)題斜纪。然而贫母,這里討論的沖突訪問(wèn)可能發(fā)生在單個(gè)線程上,并且不涉及并發(fā)或多線程代碼盒刚。
如果你在一個(gè)線程中有沖突的內(nèi)存訪問(wèn)腺劣,Swift保證你會(huì)在編譯時(shí)或運(yùn)行時(shí)得到一個(gè)錯(cuò)誤。對(duì)于多線程代碼因块,使用線程殺毒器來(lái)幫助檢測(cè)線程之間的沖突訪問(wèn)橘原。
Characteristics of Memory Access 內(nèi)存訪問(wèn)特性
在訪問(wèn)沖突的上下文中,需要考慮內(nèi)存訪問(wèn)的三個(gè)特征:訪問(wèn)是讀還是寫涡上、訪問(wèn)的持續(xù)時(shí)間和正在訪問(wèn)的內(nèi)存中的位置趾断。具體地說(shuō),如果您有兩個(gè)符合以下所有條件的訪問(wèn)吩愧,則會(huì)發(fā)生沖突:
- 至少有一個(gè)是寫訪問(wèn)芋酌。
- 它們?cè)L問(wèn)內(nèi)存中的相同位置。
- 他們的時(shí)間重疊雁佳。
讀訪問(wèn)和寫訪問(wèn)之間的區(qū)別通常很明顯:寫訪問(wèn)會(huì)更改內(nèi)存中的位置脐帝,而讀訪問(wèn)不會(huì)。內(nèi)存中的位置指的是正在訪問(wèn)的內(nèi)容——例如糖权,變量堵腹、常量或?qū)傩浴?nèi)存訪問(wèn)的持續(xù)時(shí)間可以是瞬時(shí)的星澳,也可以是長(zhǎng)期的疚顷。
如果其他代碼無(wú)法在訪問(wèn)開始之后而在訪問(wèn)結(jié)束之前運(yùn)行,則訪問(wèn)是瞬時(shí)的禁偎。從本質(zhì)上講荡含,兩個(gè)瞬時(shí)訪問(wèn)不可能同時(shí)發(fā)生。大多數(shù)內(nèi)存訪問(wèn)都是瞬時(shí)的届垫。例如,下面代碼清單中的所有讀寫訪問(wèn)都是瞬時(shí)的:
func oneMore(than number: Int) -> Int {
return number + 1
}
var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// Prints "2"
然而全释,有幾種訪問(wèn)內(nèi)存的方法装处,稱為長(zhǎng)期訪問(wèn),它們跨越了其他代碼的執(zhí)行浸船。瞬時(shí)訪問(wèn)和長(zhǎng)期訪問(wèn)的區(qū)別在于妄迁,其他代碼可能在長(zhǎng)期訪問(wèn)開始后運(yùn)行,但在長(zhǎng)期訪問(wèn)結(jié)束之前運(yùn)行李命,這稱為重疊登淘。長(zhǎng)期訪問(wèn)可以與其他長(zhǎng)期訪問(wèn)和瞬時(shí)訪問(wèn)重疊。
重疊訪問(wèn)主要出現(xiàn)在在函數(shù)和方法中使用in-out參數(shù)的代碼中封字,或者在結(jié)構(gòu)的方法中進(jìn)行修改的代碼中黔州。下面將討論使用長(zhǎng)期訪問(wèn)的特定類型的Swift代碼耍鬓。
Conflicting Access to In-Out Parameters 輸入輸出參數(shù)的訪問(wèn)沖突
函數(shù)具有對(duì)其所有in-out參數(shù)的長(zhǎng)期寫訪問(wèn)權(quán)。in-out參數(shù)的寫訪問(wèn)在所有非in-out參數(shù)被評(píng)估之后開始流妻,并持續(xù)到函數(shù)調(diào)用的整個(gè)期間牲蜀。如果有多個(gè)in-out參數(shù),那么寫入訪問(wèn)將按照參數(shù)出現(xiàn)的順序開始绅这。
這種長(zhǎng)期寫訪問(wèn)的一個(gè)結(jié)果是涣达,您不能訪問(wèn)作為in-out傳遞的原始變量,即使范圍規(guī)則和訪問(wèn)控制允許這樣做—對(duì)原始變量的任何訪問(wèn)都會(huì)產(chǎn)生沖突证薇。例如:
var stepSize = 1
func increment(_ number: inout Int) {
number += stepSize
}
increment(&stepSize)
// Error: conflicting accesses to stepSize
在上面的代碼中度苔,stepSize是一個(gè)全局變量,通郴攵龋可以從increment(_:)中訪問(wèn)它寇窑。但是,對(duì)stepSize的讀訪問(wèn)與對(duì)number的寫訪問(wèn)重疊俺泣。如下圖所示疗认,number和stepSize都指向內(nèi)存中的相同位置。讀和寫訪問(wèn)指的是相同的內(nèi)存伏钠,它們重疊横漏,產(chǎn)生沖突。
解決這個(gè)沖突的一個(gè)方法是顯式復(fù)制stepSize:
// Make an explicit copy.
var copyOfStepSize = stepSize
increment(©OfStepSize)
// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2
當(dāng)您在調(diào)用increment(_:)之前復(fù)制stepSize時(shí)熟掂,很明顯copyOfStepSize的值是由當(dāng)前的步驟大小遞增的缎浇。讀訪問(wèn)在寫訪問(wèn)開始之前結(jié)束,因此不存在沖突赴肚。
對(duì)in-out參數(shù)的長(zhǎng)期寫訪問(wèn)的另一個(gè)結(jié)果是素跺,將單個(gè)變量作為同一函數(shù)的多個(gè)in-out參數(shù)的參數(shù)傳遞會(huì)產(chǎn)生沖突。例如:
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) // OK
balance(&playerOneScore, &playerOneScore)
// Error: conflicting accesses to playerOneScore
上面的balance(::)函數(shù)修改了它的兩個(gè)參數(shù)誉券,以便在它們之間平均分配總價(jià)值指厌。用playerOneScore和playerTwoScore作為參數(shù)調(diào)用它不會(huì)產(chǎn)生沖突——有兩個(gè)寫訪問(wèn)在時(shí)間上重疊,但是它們?cè)L問(wèn)內(nèi)存中的不同位置踊跟。相反踩验,將playerOneScore作為兩個(gè)參數(shù)的值傳遞會(huì)產(chǎn)生沖突,因?yàn)樗噲D同時(shí)執(zhí)行對(duì)內(nèi)存中相同位置的兩次寫訪問(wèn)商玫。
因?yàn)椴僮鞣呛瘮?shù)箕憾,所以它們也可以長(zhǎng)期訪問(wèn)它們的in-out參數(shù)。例如拳昌,如果balance(::)是一個(gè)名為<^>的操作符函數(shù)袭异,那么編寫playerOneScore <^> playerOneScore將導(dǎo)致與balance(&playerOneScore, &playerOneScore)相同的沖突炬藤。
Conflicting Access to self in Methods 方法中對(duì)self的沖突訪問(wèn)
結(jié)構(gòu)上的修改方法在方法調(diào)用期間具有對(duì)self的寫訪問(wèn)權(quán)御铃。例如碴里,考慮一個(gè)游戲,每個(gè)玩家都有一個(gè)生命值(在受到傷害時(shí)降低)和一個(gè)能量值(在使用特殊技能時(shí)降低)畅买。
struct Player {
var name: String
var health: Int
var energy: Int
static let maxHealth = 10
mutating func restoreHealth() {
health = Player.maxHealth
}
}
在上面的restoreHealth()方法中并闲,對(duì)self的寫訪問(wèn)從方法的開頭開始,一直持續(xù)到方法返回谷羞。在本例中帝火,restoreHealth()中沒(méi)有其他代碼可以對(duì)Player實(shí)例的屬性進(jìn)行重疊訪問(wèn)。下面的shareHealth(with:)方法將另一個(gè)Player實(shí)例作為in-out參數(shù)湃缎,從而創(chuàng)建了重疊訪問(wèn)的可能性犀填。
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的玩家調(diào)用shareHealth(with:)方法來(lái)與Maria的玩家共享health不會(huì)引起沖突嗓违。在方法調(diào)用期間有對(duì)oscar的寫訪問(wèn)九巡,因?yàn)閛scar是在一個(gè)可變方法中self的值,在相同的時(shí)間內(nèi)有對(duì)maria的寫訪問(wèn)蹂季,因?yàn)閙aria是作為in-out參數(shù)傳遞的冕广。如下圖所示,它們?cè)L問(wèn)內(nèi)存中的不同位置偿洁。即使這兩個(gè)寫訪問(wèn)在時(shí)間上重疊撒汉,它們也不會(huì)沖突。
但是涕滋,如果將oscar作為參數(shù)傳遞給shareHealth(with:)睬辐,就會(huì)產(chǎn)生沖突:
oscar.shareHealth(with: &oscar)
// Error: conflicting accesses to oscar
在方法的持續(xù)時(shí)間內(nèi),修改方法需要對(duì)self的寫訪問(wèn)權(quán)宾肺,而in-out參數(shù)在相同的持續(xù)時(shí)間內(nèi)需要對(duì)teamate的寫訪問(wèn)權(quán)溯饵。在方法中,self和team都引用內(nèi)存中的相同位置锨用,如下圖所示丰刊。這兩個(gè)寫訪問(wèn)指的是相同的內(nèi)存,它們重疊增拥,產(chǎn)生沖突藻三。
Conflicting Access to Properties 屬性訪問(wèn)沖突
結(jié)構(gòu)、元組和枚舉等類型由單獨(dú)的組成值組成跪者,例如結(jié)構(gòu)的屬性或元組的元素。因?yàn)檫@些是值類型熄求,所以對(duì)值的任何部分進(jìn)行修改都會(huì)對(duì)整個(gè)值進(jìn)行修改渣玲,這意味著對(duì)其中一個(gè)屬性的讀或?qū)懺L問(wèn)需要對(duì)整個(gè)值進(jìn)行讀或?qū)懺L問(wèn)。例如弟晚,對(duì)元組元素的重疊寫訪問(wèn)會(huì)產(chǎn)生沖突:
var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// Error: conflicting access to properties of playerInformation
在上面的例子中忘衍,對(duì)元組的元素調(diào)用balance(::)會(huì)產(chǎn)生沖突逾苫,因?yàn)閷?duì)playerInformation有重疊的寫訪問(wèn)。playerInformation.health 和 playerInformation.energy作為in-out參數(shù)傳遞枚钓,這意味著balance(::)在函數(shù)調(diào)用期間需要對(duì)它們進(jìn)行寫訪問(wèn)铅搓。在這兩種情況下,對(duì)元組元素的寫訪問(wèn)都需要對(duì)整個(gè)元組的寫訪問(wèn)搀捷。這意味著有兩種對(duì)playerInformation的寫訪問(wèn)星掰,它們的持續(xù)時(shí)間重疊,從而導(dǎo)致沖突嫩舟。
下面的代碼顯示氢烘,對(duì)存儲(chǔ)在全局變量中的結(jié)構(gòu)的屬性的重疊寫訪問(wèn)也會(huì)出現(xiàn)相同的錯(cuò)誤。
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy) // Error
實(shí)際上家厌,對(duì)結(jié)構(gòu)屬性的大多數(shù)訪問(wèn)都可以安全地重疊播玖。例如,如果將上面例子中的變量holly改為局部變量而不是全局變量饭于,編譯器可以證明對(duì)結(jié)構(gòu)存儲(chǔ)屬性的重疊訪問(wèn)是安全的:
func someFunction() {
var oscar = Player(name: "Oscar", health: 10, energy: 10)
balance(&oscar.health, &oscar.energy) // OK
}
在上面的例子中蜀踏,Oscar的健康和精力被傳遞為兩個(gè)In -out參數(shù)來(lái)平衡(::)。編譯器可以證明內(nèi)存安全得到了保護(hù)掰吕,因?yàn)檫@兩個(gè)存儲(chǔ)的屬性沒(méi)有以任何方式交互果覆。
限制對(duì)結(jié)構(gòu)屬性的重疊訪問(wèn)并不總是保持內(nèi)存安全所必需的。內(nèi)存安全是需要的保證畴栖,但是獨(dú)占訪問(wèn)比內(nèi)存安全要求更嚴(yán)格——這意味著一些代碼保留內(nèi)存安全随静,即使它違反了對(duì)內(nèi)存的獨(dú)占訪問(wèn)。如果編譯器能夠證明對(duì)內(nèi)存的非排他性訪問(wèn)仍然是安全的吗讶,Swift就允許使用這種內(nèi)存安全代碼燎猛。具體來(lái)說(shuō),如果符合以下條件照皆,則可以證明對(duì)結(jié)構(gòu)屬性的重疊訪問(wèn)是安全的:
- 您只訪問(wèn)實(shí)例的存儲(chǔ)屬性重绷,而不訪問(wèn)計(jì)算屬性或類屬性。
- 結(jié)構(gòu)是局部變量的值膜毁,而不是全局變量昭卓。
- 結(jié)構(gòu)要么不被任何閉包捕獲,要么只被非轉(zhuǎn)義閉包捕獲瘟滨。
如果編譯器不能證明訪問(wèn)是安全的候醒,它就不允許訪問(wèn)。