這里的教程為Swift官方教程中文版。

內(nèi)存安全

本頁(yè)包含內(nèi)容:

默認(rèn)情況下,Swift 會(huì)阻止你代碼里不安全的行為。例如,Swift 會(huì)保證變量在使用之前就完成初始化,在內(nèi)存被回收之后就無(wú)法被訪(fǎng)問(wèn),并且數(shù)組的索引會(huì)做越界檢查。

Swift 也保證同時(shí)訪(fǎng)問(wèn)同一塊內(nèi)存時(shí)不會(huì)沖突,通過(guò)約束代碼里對(duì)于存儲(chǔ)地址的寫(xiě)操作,去獲取那一塊內(nèi)存的訪(fǎng)問(wèn)獨(dú)占權(quán)。因?yàn)?Swift 自動(dòng)管理內(nèi)存,所以大部分時(shí)候你完全不需要考慮內(nèi)存訪(fǎng)問(wèn)的事情。然而,理解潛在的沖突也是很重要的,可以避免你寫(xiě)出訪(fǎng)問(wèn)沖突的代碼。而如果你的代碼確實(shí)存在沖突,那在編譯時(shí)或者運(yùn)行時(shí)就會(huì)得到錯(cuò)誤。

理解內(nèi)存訪(fǎng)問(wèn)沖突

內(nèi)存的訪(fǎng)問(wèn),會(huì)發(fā)生在你給變量賦值,或者傳遞參數(shù)給函數(shù)時(shí)。例如,下面的代碼就包含了讀和寫(xiě)的訪(fǎng)問(wèn):

// 向 one 所在的內(nèi)存區(qū)域發(fā)起一次寫(xiě)操作
var one = 1

// 向 one 所在的內(nèi)存區(qū)域發(fā)起一次讀操作
print("We're number \(one)!")

內(nèi)存訪(fǎng)問(wèn)的沖突會(huì)發(fā)生在你的代碼嘗試同時(shí)訪(fǎng)問(wèn)同一個(gè)存儲(chǔ)地址的時(shí)侯。同一個(gè)存儲(chǔ)地址的多個(gè)訪(fǎng)問(wèn)同時(shí)發(fā)生會(huì)造成不可預(yù)計(jì)或不一致的行為。在 Swift 里,有很多修改值的行為都會(huì)持續(xù)好幾行代碼,在修改值的過(guò)程中進(jìn)行訪(fǎng)問(wèn)是有可能發(fā)生的。

你思考一下預(yù)算表更新的過(guò)程也可以看到同樣的問(wèn)題。更新預(yù)算表總共有兩步:首先你把預(yù)算項(xiàng)的名字和費(fèi)用加上,然后你再更新總數(shù)以體現(xiàn)預(yù)算表的現(xiàn)況。在更新之前和之后,你都可以從預(yù)算表里讀取任何信息并獲得正確的答案,就像下面展示的那樣。

而當(dāng)你添加預(yù)算項(xiàng)進(jìn)入表里的時(shí)候,它只是一個(gè)臨時(shí)的,錯(cuò)誤的狀態(tài),因?yàn)榭倲?shù)還沒(méi)有被更新。在添加預(yù)算項(xiàng)的過(guò)程中讀取總數(shù)就會(huì)讀取到錯(cuò)誤的信息。

這個(gè)例子也演示了你在修復(fù)內(nèi)存訪(fǎng)問(wèn)沖突時(shí)會(huì)遇到的問(wèn)題:有時(shí)修復(fù)的方式會(huì)有很多種,但哪一種是正確的就不總是那么明顯了。在這個(gè)例子里,根據(jù)你是否需要更新后的總數(shù),$5 和 $320 都可能是正確的值。在你修復(fù)訪(fǎng)問(wèn)沖突之前,你需要決定它的傾向。

注意

如果你寫(xiě)過(guò)并發(fā)和多線(xiàn)程的代碼,內(nèi)存訪(fǎng)問(wèn)沖突也許是同樣的問(wèn)題。然而,這里訪(fǎng)問(wèn)沖突的討論是在單線(xiàn)程的情境下討論的,并沒(méi)有使用并發(fā)或者多線(xiàn)程。

如果你曾經(jīng)在單線(xiàn)程代碼里有訪(fǎng)問(wèn)沖突,Swift 可以保證你在編譯或者運(yùn)行時(shí)會(huì)得到錯(cuò)誤。對(duì)于多線(xiàn)程的代碼,可以使用 Thread Sanitizer 去幫助檢測(cè)多線(xiàn)程的沖突。

內(nèi)存訪(fǎng)問(wèn)的典型狀況

內(nèi)存訪(fǎng)問(wèn)沖突有三種典型的狀況:訪(fǎng)問(wèn)是讀還是寫(xiě),訪(fǎng)問(wèn)的時(shí)長(zhǎng),以及被訪(fǎng)問(wèn)的存儲(chǔ)地址。特別是,當(dāng)你有兩個(gè)訪(fǎng)問(wèn)符合下列的情況:

  • 至少有一個(gè)是寫(xiě)訪(fǎng)問(wèn)
  • 它們?cè)L問(wèn)的是同一個(gè)存儲(chǔ)地址
  • 它們的訪(fǎng)問(wèn)在時(shí)間線(xiàn)上部分重疊

讀和寫(xiě)訪(fǎng)問(wèn)的區(qū)別很明顯:一個(gè)寫(xiě)訪(fǎng)問(wèn)會(huì)改變存儲(chǔ)地址,而讀操作不會(huì)。存儲(chǔ)地址會(huì)指向真正訪(fǎng)問(wèn)的位置 —— 例如,一個(gè)變量,常量或者屬性。內(nèi)存訪(fǎng)問(wèn)的時(shí)長(zhǎng)要么是瞬時(shí)的,要么是長(zhǎng)期的。

如果一個(gè)訪(fǎng)問(wèn)不可能在其訪(fǎng)問(wèn)期間被其它代碼訪(fǎng)問(wèn),那么就是一個(gè)瞬時(shí)訪(fǎng)問(wèn)?;谶@個(gè)特性,兩個(gè)瞬時(shí)訪(fǎng)問(wèn)是不可能同時(shí)發(fā)生。大多數(shù)內(nèi)存訪(fǎng)問(wèn)都是瞬時(shí)的。例如,下面列舉的所有讀和寫(xiě)訪(fǎng)問(wèn)都是瞬時(shí)的:

func oneMore(than number: Int) -> Int {
    return number + 1
}

var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// 打印 "2"

然而,有幾種被稱(chēng)為長(zhǎng)期訪(fǎng)問(wèn)的內(nèi)存訪(fǎng)問(wèn)方式,會(huì)在別的代碼執(zhí)行時(shí)持續(xù)進(jìn)行。瞬時(shí)訪(fǎng)問(wèn)和長(zhǎng)期訪(fǎng)問(wèn)的區(qū)別在于別的代碼有沒(méi)有可能在訪(fǎng)問(wèn)期間同時(shí)訪(fǎng)問(wèn),也就是在時(shí)間線(xiàn)上的重疊。一個(gè)長(zhǎng)期訪(fǎng)問(wèn)可以被別的長(zhǎng)期訪(fǎng)問(wèn)或瞬時(shí)訪(fǎng)問(wèn)重疊。

重疊的訪(fǎng)問(wèn)主要出現(xiàn)在使用 in-out 參數(shù)的函數(shù)和方法或者結(jié)構(gòu)體的 mutating 方法里。Swift 代碼里典型的長(zhǎng)期訪(fǎng)問(wèn)會(huì)在后面進(jìn)行討論。

In-Out 參數(shù)的訪(fǎng)問(wèn)沖突

一個(gè)函數(shù)會(huì)對(duì)它所有的 in-out 參數(shù)進(jìn)行長(zhǎng)期寫(xiě)訪(fǎng)問(wèn)。in-out 參數(shù)的寫(xiě)訪(fǎng)問(wèn)會(huì)在所有非 in-out 參數(shù)處理完之后開(kāi)始,直到函數(shù)執(zhí)行完畢為止。如果有多個(gè) in-out 參數(shù),則寫(xiě)訪(fǎng)問(wèn)開(kāi)始的順序與參數(shù)的順序一致。

長(zhǎng)期訪(fǎng)問(wèn)的存在會(huì)造成一個(gè)結(jié)果,你不能在原變量以 in-out 形式傳入后訪(fǎng)問(wèn)原變量,即使作用域原則和訪(fǎng)問(wèn)權(quán)限允許 —— 任何訪(fǎng)問(wèn)原變量的行為都會(huì)造成沖突。例如:

var stepSize = 1

func increment(_ number: inout Int) {
    number += stepSize
}

increment(&stepSize)
// 錯(cuò)誤:stepSize 訪(fǎng)問(wèn)沖突

在上面的代碼里,stepSize 是一個(gè)全局變量,并且它可以在 increment(_:) 里正常訪(fǎng)問(wèn)。然而,對(duì)于 stepSize 的讀訪(fǎng)問(wèn)與 number 的寫(xiě)訪(fǎng)問(wèn)重疊了。就像下面展示的那樣,numberstepSize 都指向了同一個(gè)存儲(chǔ)地址。同一塊內(nèi)存的讀和寫(xiě)訪(fǎng)問(wèn)重疊了,就此產(chǎn)生了沖突。

解決這個(gè)沖突的一種方式,是復(fù)制一份 stepSize 的副本:

// 復(fù)制一份副本
var copyOfStepSize = stepSize
increment(&copyOfStepSize)

// 更新原來(lái)的值
stepSize = copyOfStepSize
// stepSize 現(xiàn)在的值是 2

當(dāng)你在調(diào)用 increment(_:) 之前復(fù)制一份副本,顯然 copyOfStepSize 就會(huì)根據(jù)當(dāng)前的 stepSize 增加。讀訪(fǎng)問(wèn)在寫(xiě)操作之前就已經(jīng)結(jié)束了,所以不會(huì)有沖突。

長(zhǎng)期寫(xiě)訪(fǎng)問(wèn)的存在還會(huì)造成另一種結(jié)果,往同一個(gè)函數(shù)的多個(gè) in-out 參數(shù)里傳入同一個(gè)變量也會(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)  // 正常
balance(&playerOneScore, &playerOneScore)
// 錯(cuò)誤:playerOneScore 訪(fǎng)問(wèn)沖突

上面的 balance(_:_:) 函數(shù)會(huì)將傳入的兩個(gè)參數(shù)平均化。將 playerOneScoreplayerTwoScore 作為參數(shù)傳入不會(huì)產(chǎn)生錯(cuò)誤 —— 有兩個(gè)訪(fǎng)問(wèn)重疊了,但它們?cè)L問(wèn)的是不同的內(nèi)存位置。相反,將 playerOneScore 作為參數(shù)同時(shí)傳入就會(huì)產(chǎn)生沖突,因?yàn)樗鼤?huì)發(fā)起兩個(gè)寫(xiě)訪(fǎng)問(wèn),同時(shí)訪(fǎng)問(wèn)同一個(gè)的存儲(chǔ)地址。

注意 因?yàn)椴僮鞣彩呛瘮?shù),它們也會(huì)對(duì) in-out 參數(shù)進(jìn)行長(zhǎng)期訪(fǎng)問(wèn)。例如,假設(shè) balance(_:_:) 是一個(gè)名為 <^> 的操作符函數(shù),那么 playerOneScore <^> playerOneScore 也會(huì)造成像 balance(&playerOneScore, &playerOneScore) 一樣的沖突。

方法里 self 的訪(fǎng)問(wèn)沖突

一個(gè)結(jié)構(gòu)體的 mutating 方法會(huì)在調(diào)用期間對(duì) self 進(jìn)行寫(xiě)訪(fǎng)問(wèn)。例如,想象一下這么一個(gè)游戲,每一個(gè)玩家都有血量,受攻擊時(shí)血量會(huì)下降,并且有能量,使用特殊技能時(shí)會(huì)減少能量。

struct Player {
    var name: String
    var health: Int
    var energy: Int

    static let maxHealth = 10
    mutating func restoreHealth() {
        health = Player.maxHealth
    }
}

在上面的 restoreHealth() 方法里,一個(gè)對(duì)于 self 的寫(xiě)訪(fǎng)問(wèn)會(huì)從方法開(kāi)始直到方法 return。在這種情況下,restoreHealth() 里的其它代碼不可以對(duì) Player 實(shí)例的屬性發(fā)起重疊的訪(fǎng)問(wèn)。下面的 shareHealth(with:) 方法接受另一個(gè) Player 的實(shí)例作為 in-out 參數(shù),產(chǎn)生了訪(fǎ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)  // 正常

上面的例子里,調(diào)用 shareHealth(with:) 方法去把 oscar 玩家的血量分享給 maria 玩家并不會(huì)造成沖突。在方法調(diào)用期間會(huì)對(duì) oscar 發(fā)起寫(xiě)訪(fǎng)問(wèn),因?yàn)樵?mutating 方法里 self 就是 oscar,同時(shí)對(duì)于 maria 也會(huì)發(fā)起寫(xiě)訪(fǎng)問(wèn),因?yàn)?maria 作為 in-out 參數(shù)傳入。過(guò)程如下,它們會(huì)訪(fǎng)問(wèn)內(nèi)存的不同位置。即使兩個(gè)寫(xiě)訪(fǎng)問(wèn)重疊了,它們也不會(huì)沖突。

當(dāng)然,如果你將 oscar 作為參數(shù)傳入 shareHealth(with:) 里,就會(huì)產(chǎn)生沖突:

oscar.shareHealth(with: &oscar)
// 錯(cuò)誤:oscar 訪(fǎng)問(wèn)沖突

mutating 方法在調(diào)用期間需要對(duì) self 發(fā)起寫(xiě)訪(fǎng)問(wèn),而同時(shí) in-out 參數(shù)也需要寫(xiě)訪(fǎng)問(wèn)。在方法里,selfteammate 都指向了同一個(gè)存儲(chǔ)地址 —— 就像下面展示的那樣。對(duì)于同一塊內(nèi)存同時(shí)進(jìn)行兩個(gè)寫(xiě)訪(fǎng)問(wèn),并且它們重疊了,就此產(chǎn)生了沖突。

屬性的訪(fǎng)問(wèn)沖突

如結(jié)構(gòu)體,元組和枚舉的類(lèi)型都是由多個(gè)獨(dú)立的值組成的,例如結(jié)構(gòu)體的屬性或元組的元素。因?yàn)樗鼈兌际侵殿?lèi)型,修改值的任何一部分都是對(duì)于整個(gè)值的修改,意味著其中一個(gè)屬性的讀或?qū)懺L(fǎng)問(wèn)都需要訪(fǎng)問(wèn)整一個(gè)值。例如,元組元素的寫(xiě)訪(fǎng)問(wèn)重疊會(huì)產(chǎn)生沖突:

var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// 錯(cuò)誤:playerInformation 的屬性訪(fǎng)問(wèn)沖突

上面的例子里,傳入同一元組的元素對(duì) balance(_:_:) 進(jìn)行調(diào)用,產(chǎn)生了沖突,因?yàn)?playerInformation 的訪(fǎng)問(wèn)產(chǎn)生了寫(xiě)訪(fǎng)問(wèn)重疊。playerInformation.healthplayerInformation.energy 都被作為參數(shù)傳入,意味著 balance(_:_:) 需要在函數(shù)調(diào)用期間對(duì)它們發(fā)起寫(xiě)訪(fǎng)問(wèn)。任何情況下,對(duì)于元組元素的寫(xiě)訪(fǎng)問(wèn)都需要對(duì)整個(gè)元組發(fā)起寫(xiě)訪(fǎng)問(wèn)。這意味著對(duì)于 playerInfomation 發(fā)起的兩個(gè)寫(xiě)訪(fǎng)問(wèn)重疊了,造成沖突。

下面的代碼展示了一樣的錯(cuò)誤,對(duì)于一個(gè)存儲(chǔ)在全局變量里的結(jié)構(gòu)體屬性的寫(xiě)訪(fǎng)問(wèn)重疊了。

var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)  // 錯(cuò)誤

在實(shí)踐中,大多數(shù)對(duì)于結(jié)構(gòu)體屬性的訪(fǎng)問(wèn)都會(huì)安全的重疊。例如,將上面例子里的變量 holly 改為本地變量而非全局變量,編譯器就會(huì)可以保證這個(gè)重疊訪(fǎng)問(wèn)時(shí)安全的:

func someFunction() {
    var oscar = Player(name: "Oscar", health: 10, energy: 10)
    balance(&oscar.health, &oscar.energy)  // 正常
}

上面的例子里,oscarhealthenergy 都作為 in-out 參數(shù)傳入了 balance(_:_:) 里。編譯器可以保證內(nèi)存安全,因?yàn)閮蓚€(gè)存儲(chǔ)屬性任何情況下都不會(huì)相互影響。

限制結(jié)構(gòu)體屬性的重疊訪(fǎng)問(wèn)對(duì)于內(nèi)存安全并不總是必要的。內(nèi)存安全是必要的,但訪(fǎng)問(wèn)獨(dú)占權(quán)的要求比內(nèi)存安全還要更嚴(yán)格 —— 意味著即使有些代碼違反了訪(fǎng)問(wèn)獨(dú)占權(quán)的原則,也是內(nèi)存安全的。如果編譯器可以保證這種非專(zhuān)屬的訪(fǎng)問(wèn)是安全的,那 Swift 就會(huì)允許這種內(nèi)存安全的行為。特別是當(dāng)你遵循下面的原則時(shí),它可以保證結(jié)構(gòu)體屬性的重疊訪(fǎng)問(wèn)是安全的:

  • 你訪(fǎng)問(wèn)的是實(shí)例的存儲(chǔ)屬性,而不是計(jì)算屬性或類(lèi)的屬性
  • 結(jié)構(gòu)體是本地變量的值,而非全局變量
  • 結(jié)構(gòu)體要么沒(méi)有被閉包捕獲,要么只被非逃逸閉包捕獲了

如果編譯器無(wú)法保證訪(fǎng)問(wèn)的安全性,它就不會(huì)允許訪(fǎng)問(wèn)。

更新歷史

4.0 翻譯:kemchenj 2017-09-21

? 泛型 訪(fǎng)問(wèn)控制 ?
?