Swift中編寫單例的正確方式

本文由CocoaChina譯者leon(社區ID)翻譯自krakendev
原文:THE RIGHT WAY TO WRITE A SINGLETON
轉載請保持全部內容和連接的完整性。php


在以前的帖子裏聊過狀態管理有多痛苦,有時這是不可避免的。一個狀態管理的例子你們都很熟悉,那就是單例。使用Swift時,有許多方法實現單例,這是個麻煩事,由於咱們不知道哪一個最合適。這裏咱們來回顧一下單例的歷史,看一看在Swift中如何正確地實現單例。html

若是你想直接看看Swift中單例的正確實現方式,直接跳到帖子最後便可。git

791010.png

往事回憶之ObjC單例github

Swift是Objective-C的一種天然演變,它用以下的方式實現單例:編程

@interface Kraken : NSObject
@end
 
@implementation Kraken
 
+ (instancetype)sharedInstance {
    static Kraken *sharedInstance = nil;
    static dispatch_once_t onceToken;
     
    dispatch_once(&onceToken, ^{
        sharedInstance = [[Kraken alloc] init];
    });
    return sharedInstance;
}
 
@end

在這個現成方案中,咱們能夠看到單例的基本結構。讓咱們來約定一些規則,這樣便於更好的理解。swift

單例規則api

關於單例,有三個重要的準則須要牢記:安全

1. 單例必須是惟一的(要不怎麼叫單例?) 在程序生命週期中只能存在一個這樣的實例。單例的存在使咱們能夠全局訪問狀態。例如:併發

NSNotificationCenter, UIApplication和NSUserDefaults。app

2. 爲保證單例的惟一性,單例類的初始化方法必須是私有的。這樣就能夠避免其餘對象經過單例類建立額外的實例。

3. 考慮到規則1,爲保證在整個程序的生命週期中值有一個實例被建立,單例必須是線程安全的。併發有時候確實挺複雜,簡單說來,若是單例的代碼不正確,若是有兩個線程同時實例化一個單例對象,就可能會建立出兩個單例對象。也就是說,必須保證單例的線程安全性,才能夠保證其惟一性。經過調用dispatch_once,便可保證明例化代碼只運行一次。

在程序中保持單例的惟一性,只初始化一次,這樣並不難。帖子的餘下部分中,須要記住:單例實現要知足隱藏的dispatch_once規則。

Swift單例

自Swift 1.0開始,建立單例有不少種方法。這些連接中已經有很詳盡的描述,好比

https://github.com/hpique/SwiftSingletonhttp://stackoverflow.com/questions/24024549/dispatch-once-singleton-model-in-swift

https://developer.apple.com/swift/blog/?id=7可是誰喜歡點連接呢?先劇透一下吧:總共有4個版本。咱們來清點一下:

1. 最醜陋方法(Swift皮,Objective-C心)

class TheOneAndOnlyKraken {
    class var sharedInstance: TheOneAndOnlyKraken {
        struct Static {
            static var onceToken: dispatch_once_t = 0
            static var instance: TheOneAndOnlyKraken? = nil
        }
        dispatch_once(&Static.onceToken) {
            Static.instance = TheOneAndOnlyKraken()
        }
        return Static.instance!
    }
}

這個版本是Objective-C的直接移植版。我認爲它很差看是由於Swift本該更簡潔、更有描述力。不要作個搬運工,要作就作的更好。

下載.jpg

2. 結構體方法(「新瓶裝老酒)

class TheOneAndOnlyKraken {
    class var sharedInstance: TheOneAndOnlyKraken {
        struct Static {
            static let instance = TheOneAndOnlyKraken()
        }
        return Static.instance
    }
}

Swift 1.0時,不支持靜態類變量,那時這個方法是不得已而爲之。但使用結構體,就能夠支持這個功能。由於靜態變量的限制,咱們被約束在這樣的一個模型中。這比Objective-C移植版本好一些,但還不夠好。有趣的是,在Swift 1.2發佈幾個月後,我還能夠看到這種寫法。在那以後,反而更多了。

3.全局變量方法(「單行單例」方法)

private let sharedKraken = TheOneAndOnlyKraken()
class TheOneAndOnlyKraken {
    class var sharedInstance: TheOneAndOnlyKraken {
        return sharedKraken
    }
}

在Swift 1.2之後,咱們有了訪問權限設置(access control specifiers) 的功能和靜態類成員(static class members)。這意味着咱們終於能夠擺脫混亂的全局變量、全局命名空間,也不會發生命名空間衝突了。這個版本看起來更Swiftier一點。

如今,你可能會有疑問:爲什麼看不到dispatch_once?根據Apple Swift博客中的說法,以上方法都自動知足dispatch_once規則。這裏有個帖子能夠證實dispatch_once規則一直在起做用。

「全局變量(還有結構體和枚舉體的靜態成員)的Lazy初始化方法會在其被訪問的時候調用一次。相似於調用'dispatch_once'以保證其初始化的原子性。這樣就有了一種很酷的'單次調用'方式:只聲明一個全局變量和私有的初始化方法便可。」--來自Apple's Swift Blog

「The lazy initializer for a global variable (also for static members of structs and enums) is run the first time that global is accessed, and is launched as `dispatch_once` to make sure that the initialization is atomic. This enables a cool way to use `dispatch_once` in your code: just declare a global variable with an initializer and mark it private.」)
這就是Apple官方文檔給咱們的全部信息,但這些已經足夠證實全局變量和結構體/枚舉體的靜態成員是支持」dispatch_once」特性的。如今,咱們相信使用全局變量來「懶包裝」單例的初始化方法到dispatch_once代碼塊中是100%安全的。可是對於靜態類變量來講,狀況又如何?

這個問題帶咱們到更激動人心的思考中去:

正確的方法(也便是「單行單例法」)如今已經被證實正確。

class TheOneAndOnlyKraken {
    static let sharedInstance = TheOneAndOnlyKraken()
}

下載 (1).jpg

到此爲止,咱們已經作了許多研究工做。這個帖子的靈感來源於咱們在Capital One的一次對話:結對編程review代碼的過程當中,咱們試圖找到在App中使用Swift編寫正確、一致的單例方法。咱們知道編寫單例的正確方法,可是沒法用理論來證實。沒有足夠的文檔支持,想證實方法的正確是徒勞的。在網上或博客圈中沒有足夠多的信息的話,這隻能是一家之言,你們都知道若是網上查不到信息,就不會相信。這點讓我很難過。

下載 (2).jpg

我搜索了許多信息,甚至翻到了google搜索結果的10多頁,仍是一無所得。難道沒有人發帖證實單行單利方法的正確性?可能有人發過,可是太難被發現了。

所以我決定將各類單例都寫一變,而後在運行時加入斷點來觀測。

分析了每一個stack trace的記錄後,我發現了有趣的東西——證據!

來看看截圖:

Screen+Shot+2015-07-16+at+4.56.38+PM.png

使用全局單例方法

1449215873883297.png

使用單行單例方法

第一張圖片展現了使用全局實例時的stack trace。標紅的地方須要注意。在調用Kraken單例以前,先調用了swift_once,接下來是swift_once_block_invoke。Apple以前在文檔中已經說過,「懶實例化」的全局變量會被自動放在dispatch_once塊中,咱們能夠假定說的就是這個東西。

瞭解了這些知識,咱們來看看漂亮的單行單例方法。如圖所示,調用徹底同樣。這樣,咱們就有了證據證實單行單例方法是正確的。

不要忘記設置初始化方法爲私有

@davedelong,Apple的Framework傳道者,善意地提醒我:必須保證init方法的私有性,只有這樣,才能保證單例是真正惟一的,避免外部對象經過訪問init方法建立單例類的其餘實例。因爲Swift中的全部對象都是由公共的初始化方法建立的,咱們須要重寫本身的init方法,並設置其爲私有的。這很簡單,並且不會破壞到咱們優雅的單行單例方法。

class TheOneAndOnlyKraken {
    static let sharedInstance = TheOneAndOnlyKraken()
    private init() {} //This prevents others from using the default '()' initializer for this class.
}

這樣作就能夠保證編譯器在某個類嘗試使用()來初始化TheOneAndOnlyKraken時,拋出錯誤:

下載.png

就是這樣,咱們的單行單例,很是完美!

結論

這裏回覆一下jtbandes在「top rated answer to swift singletons on Stack Overflow這個帖子中的問題:我也找不到哪裏有文檔證實let語句能夠帶來線程安全性的好處。我記得在去年參加WWDC的時候有相似的說法,沒辦法保證讀者或各位Googler也偶遇到這個說法。但願這個帖子能幫助你們理解爲何單行單例在Swift中是正確的方法。

相關文章
相關標籤/搜索