定稿注:原文沒有提供源碼,作為一個(gè)走心的翻譯組,我們已經(jīng)將本篇文章的最終版源碼作成 Playground,可以到這個(gè)地址進(jìn)行下載。
更新:
2015-12-17 提到 Swift evolution 郵件列表中一個(gè)關(guān)于行為屬性的新提案,如果這個(gè)提案被采取,則本篇文章中的大部分將成為過時(shí)的內(nèi)容
Swift 中的 lazy 關(guān)鍵字允許你定義這么一個(gè)屬性:它的初始值在當(dāng)它被首次訪問的時(shí)候才計(jì)算。舉個(gè)例子,試想通過一個(gè)結(jié)構(gòu)體來描述一副圖像。該圖像的元數(shù)據(jù)(metadata)字典創(chuàng)建操作所需要的開銷代價(jià)也許很大,因此我們更傾向于推遲這個(gè)行為直至需要該數(shù)據(jù)的時(shí)候。我們可以像這樣聲明一個(gè) lazy var
屬性:
struct Image {
lazy var metadata: [String:AnyObject] = {
// 加載圖片和解析 metadata,相當(dāng)占內(nèi)存
// ...
return ...
}()
}
注意我們必須使用 var
關(guān)鍵字聲明屬性。let
關(guān)鍵字聲明的常量必須在實(shí)例初始化完成前擁有一個(gè)值,顯然對于 lazy
變量無法保證初始化前有值。(譯者注:實(shí)例指結(jié)構(gòu)體實(shí)例)
訪問 lazy
屬性是一個(gè) mutating
操作,因?yàn)閷傩缘某跏贾抵辉诘谝淮卧L問時(shí)才計(jì)算確定。當(dāng)結(jié)構(gòu)體(屬于值類型)包含一個(gè) lazy
屬性時(shí),任何該結(jié)構(gòu)體的擁有者同樣必須聲明為一個(gè)變量(var
關(guān)鍵字聲明),因?yàn)樵L問該屬性意味著可能改變結(jié)構(gòu)體內(nèi)容。所以以下行為是不被允許的:
let image = Image()
print(image.metadata)
// error: Cannot use mutating getter on immutable value: 'image' is a 'let' constant.
你可以強(qiáng)制要求用戶使用 var
關(guān)鍵字來聲明 Image
類型,但是這可能有些不合適(比如,函數(shù)中作為變量的情況)或容易混淆(因?yàn)?getter 方法通常并不是可變的)。
使用Box包裝類型
另外一種選擇是將 lazy
值封裝到一個(gè)類中,有點(diǎn)類似經(jīng)常使用的 Box type
。由于類是引用類型,結(jié)構(gòu)體可以包含一個(gè) let
常量指向一個(gè)類實(shí)例,如此結(jié)構(gòu)體依舊是不可變的,但它指向的引用對象本身是可以改變的。
首先我們定義一個(gè)枚舉類型,命名為 LazyValue
,用于表示值類型 T
能夠被懶加載(有時(shí)又稱延遲加載)。它具有兩個(gè)可能狀態(tài):要么還未執(zhí)行計(jì)算操作,要么已經(jīng)執(zhí)行計(jì)算并得到結(jié)果值。在前一種情況中,它存儲(chǔ)用于執(zhí)行計(jì)算的函數(shù)(閉包)。后一種情況中,它存儲(chǔ)計(jì)算值。
private enum LazyValue<T> {
case NotYetComputed(() -> T)
case Computed(T)
}
現(xiàn)在我們將封裝該枚舉到一個(gè)類中,命名為 LazyBox
,這樣我們就能夠獨(dú)立地在它內(nèi)部做一些改變。而 LazyBox
實(shí)例的擁有者依舊能夠保持不可變狀態(tài)。實(shí)現(xiàn)如下:
final class LazyBox<T> {
init(computation: () -> T) {
_value = .NotYetComputed(computation)
}
private var _value: LazyValue<T>
var value: T {
switch self._value {
case .NotYetComputed(let computation):
let result = computation()
self._value = .Computed(result)
return result
case .Computed(let result):
return result
}
}
}
LazyBox
類構(gòu)造方法接收一個(gè)閉包作為變量,用于計(jì)算值。我們將該函數(shù)存儲(chǔ)到一個(gè)私有的 LazyValue
屬性中,等待需要時(shí)進(jìn)行訪問。該類的公有接口是一個(gè)只讀屬性 value
。在 value
的 getter
方法中我們檢查是否已經(jīng)擁有一個(gè)計(jì)算屬性,如果是則返回該值。否則我們執(zhí)行一次計(jì)算函數(shù)并緩存結(jié)果值到 _value
等待之后的讀取操作。
我們可以像這樣使用 LazyBox
,并且驗(yàn)證計(jì)算函數(shù)確實(shí)僅被執(zhí)行了一次:
var counter = 0
let box = LazyBox<Int> {
counter += 1;
return counter * 10
}
assert(box.value == 10)
assert(box.value == 10)
assert(counter == 1)// 倘若你把 1 改成 2,就會(huì)報(bào)錯(cuò)
上面這種方式的優(yōu)勢在于它能夠應(yīng)用到常量結(jié)構(gòu)體中。而在其他方面,它相比較 lazy
變量使用略遜一籌,因?yàn)橛脩粜枰ㄟ^ value
屬性才能訪問結(jié)果值。如果感覺這樣不直觀的話,我們可以隱藏具體的實(shí)現(xiàn),而向用戶提供另外一個(gè)計(jì)算屬性返回 LazyBox.value
,同時(shí)我們將 LazyBox
屬性設(shè)置為私有。
struct Image {
// 延遲存儲(chǔ)
private let _metadata = LazyBox<[String:AnyObject]> {
// 加載圖片和解析 metadata,相當(dāng)占內(nèi)存
// ...
return ...
}
var metadata: [String:AnyObject] {
return _metadata.value
}
}
let image = Image()
print(image.metadata) // 不報(bào)錯(cuò)
這樣該結(jié)構(gòu)體依舊保留了它的值語義。值類型內(nèi)部使用引用類型是慣用伎倆,以這種方式實(shí)現(xiàn)能夠保證值語義。標(biāo)準(zhǔn)庫中許多集合類型都是以這種方式實(shí)現(xiàn)的。
并發(fā)性
這種實(shí)現(xiàn)方式還存在最后一個(gè)潛在問題,也就是未考慮并發(fā)性。倘若 LazyBox.value
還未完成值計(jì)算操作,同時(shí)被多個(gè)線程訪問,那么計(jì)算函數(shù)就將被執(zhí)行多次。這些都是你需要避免的情況,萬一計(jì)算函數(shù)會(huì)產(chǎn)生副作用或相當(dāng)耗內(nèi)存呢,對吧?
我們可以將內(nèi)部 _value
屬性的所有讀寫操作都安排到一個(gè)私有串行隊(duì)列中,這樣就能夠保證函數(shù)僅被執(zhí)行一次。下面是新的實(shí)現(xiàn)方式:
final class LazyBox<T> {
init(computation: () -> T) {
_value = .NotYetComputed(computation)
}
private var _value: LazyValue<T>
/// 所有對于 `_value` 的讀寫都在這個(gè)線程隊(duì)列中。
private let queue = dispatch_queue_create(
"LazyBox._value", DISPATCH_QUEUE_SERIAL)
var value: T {
var returnValue: T? = nil
dispatch_sync(queue) {
switch self._value {
case .NotYetComputed(let computation):
let result = computation()
self._value = .Computed(result)
returnValue = result
case .Computed(let result):
returnValue = result
}
}
assert(returnValue != nil)
return returnValue!
}
}
使用這種方式的缺點(diǎn)是每次訪問 value
時(shí)都會(huì)造成一個(gè)小的性能影響,倘若多個(gè)線程同時(shí)讀取該值時(shí)可能引起爭用,畢竟它們都要經(jīng)過同一個(gè)串行隊(duì)列。鑒于每次緩存值被計(jì)算后隊(duì)列中要執(zhí)行的工作量相當(dāng)少,在大多數(shù)情況中都可以忽略不計(jì)。
值得注意的是 Swift 并沒有為 lazy
關(guān)鍵字提供上述保證。官方文檔里是這么說的:
注意:倘若一個(gè)標(biāo)記為 lazy 的屬性同時(shí)被多個(gè)線程修改時(shí),且該屬性還未進(jìn)行初始化操作,那么將無法保證該屬性僅被初始化一次。
最新更新:就在我發(fā)布該文章之后的一個(gè)小時(shí),Joe Groff 在 Swift evolution 中為屬性行為發(fā)布了一個(gè)影響深遠(yuǎn)的提議,郵件內(nèi)容請點(diǎn)擊這里,一旦被采納,將實(shí)現(xiàn)我在本文中討論的內(nèi)容(且遠(yuǎn)遠(yuǎn)不止),并以一個(gè)更自然的方式來實(shí)現(xiàn)。我強(qiáng)烈建議你看一下。
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請?jiān)L問 http://swift.gg。