鍍金池/ 教程/ iOS/ 內(nèi)存安全
特性(Attributes)
Access Control 權(quán)限控制的黑與白
基本運(yùn)算符(Basic Operators)
基礎(chǔ)部分(The Basics)
閉包(Closures)
擴(kuò)展
泛型參數(shù)(Generic Parameters and Arguments)
訪問控制和 protected
語(yǔ)句(Statements)
模式(Patterns)
WWDC 里面的那個(gè)“大炮打氣球”
關(guān)于語(yǔ)言參考(About the Language Reference)
語(yǔ)法總結(jié)(Summary of the Grammar)
嵌套類型
類型(Types)
Swift 初見(A Swift Tour)
泛型
枚舉(Enumerations)
高級(jí)運(yùn)算符
繼承
析構(gòu)過程
關(guān)于 Swift(About Swift)
訪問控制
類和結(jié)構(gòu)體
內(nèi)存安全
Swift 與 C 語(yǔ)言指針友好合作
協(xié)議
屬性(Properties)
可選類型完美解決占位問題
錯(cuò)誤處理
字符串和字符(Strings and Characters)
聲明(Declarations)
自動(dòng)引用計(jì)數(shù)
Swift 里的值類型與引用類型
表達(dá)式(Expressions)
Swift 文檔修訂歷史
造個(gè)類型不是夢(mèng)-白話 Swift 類型創(chuàng)建
歡迎使用 Swift
詞法結(jié)構(gòu)(Lexical Structure)
集合類型(Collection Types)
下標(biāo)
方法(Methods)
可選鏈?zhǔn)秸{(diào)用
版本兼容性
類型轉(zhuǎn)換
構(gòu)造過程
The Swift Programming Language 中文版
函數(shù)(Functions)
Swift 教程
控制流(Control Flow)

內(nèi)存安全


4.0 翻譯:kemchenj 2017-09-21

4.1 翻譯+校對(duì):mylittleswift

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

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

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

理解內(nèi)存訪問沖突

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

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

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

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

你思考一下預(yù)算表更新的過程也可以看到同樣的問題。更新預(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ù)還沒有唄更新。在添加預(yù)算項(xiàng)的過程中讀取總數(shù)就會(huì)讀取到錯(cuò)誤的信息。

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

注意

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

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

內(nèi)存訪問的典型狀況

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

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

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

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

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

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

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

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

In-Out 參數(shù)的訪問沖突

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

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

var stepSize = 1

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

increment(&stepSize)
// 錯(cuò)誤:stepSize 訪問沖突

在上面的代碼里,stepSize 是一個(gè)全局變量,并且它可以在 increment(_:) 里正常訪問。然而,對(duì)于 stepSize 的讀訪問與 number 的寫訪問重疊了。就像下面展示的那樣,numberstepSize 都指向了同一個(gè)存儲(chǔ)地址。同一塊內(nèi)存的讀和寫訪問重疊了,就此產(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 增加。讀訪問在寫操作之前就已經(jīng)結(jié)束了,所以不會(huì)有沖突。

長(zhǎng)期寫訪問的存在還會(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 訪問沖突

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

注意

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

方法里 self 的訪問沖突

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

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 的寫訪問會(huì)從方法開始直到方法 return。在這種情況下,restoreHealth() 里的其它代碼不可以對(duì) Player 實(shí)例的屬性發(fā)起重疊的訪問。下面的 shareHealth(with:) 方法接受另一個(gè) Player 的實(shí)例作為 in-out 參數(shù),產(chǎ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ā)起寫訪問,因?yàn)樵?mutating 方法里 self 就是 oscar,同時(shí)對(duì)于 maria 也會(huì)發(fā)起寫訪問,因?yàn)?maria 作為 in-out 參數(shù)傳入。過程如下,它們會(huì)訪問內(nèi)存的不同位置。即使兩個(gè)寫訪問重疊了,它們也不會(huì)沖突。

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

oscar.shareHealth(with: &oscar)
// 錯(cuò)誤:oscar 訪問沖突

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

屬性的訪問沖突

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

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

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

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

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

在實(shí)踐中,大多數(shù)對(duì)于結(jié)構(gòu)體屬性的訪問都會(huì)安全的重疊。例如,將上面例子里的變量 holly 改為本地變量而非全局變量,編譯器就會(huì)可以保證這個(gè)重疊訪問時(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)體屬性的重疊訪問對(duì)于內(nèi)存安全并不總是必要的。內(nèi)存安全是必要的,但訪問獨(dú)占權(quán)的要求比內(nèi)存安全還要更嚴(yán)格 —— 意味著即使有些代碼違反了訪問獨(dú)占權(quán)的原則,也是內(nèi)存安全的。如果編譯器可以保證這種非專屬的訪問是安全的,那 Swift 就會(huì)允許這種內(nèi)存安全的行為。特別是當(dāng)你遵循下面的原則時(shí),它可以保證結(jié)構(gòu)體屬性的重疊訪問是安全的:

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

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