原文:The Right Way To Write a Singleton — KrakenDev
尽管我在上一篇文章中写到了办理状况的苦恼,但有时咱们无法避免它。办理状况的一个比如是咱们都很了解的东西——单例(Singleton)。咱们在 Swift 中发现的问题是,有许多办法来完成它。但哪种才是正确的办法?在这篇文章中,我将向你展现单例的历史,然后向你展现在 Swift 中完成单例的正确办法。
如果你想查看在 Swift 中完成单例形式的正确办法,以及证明它的 “正确性”,你能够滚动到文章的底部。:)
回忆之旅
Swift 是 Objective-C 的进化版本。在 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
现在咱们已经解决了这个问题,咱们能够看到一个单例的基本结构,让咱们制定一些规矩,以便咱们了解咱们正在看的东西。
DA SINGLETON, MAHN 的规矩
关于单例,基本上有三件事要记住:
- 一个单例必须是仅有(unique)的。这便是为什么它被称为单例。在它在运用程序中存活的生命周期内,只能有一个实例。单例的存在是为了给咱们供给单一的大局状况。这样的比如有
NSNotificationCenter
、UIApplication
和NSUserDefaults
。 - 为了坚持单例的仅有性,单例的初始化构造器需求是私有(private)的。这有助于避免其他目标自己创立你的单例类的实例。感谢一切向我指出这一点的人 :)
- 由于规矩#1,为了在运用程序的整个生命周期中只要一个实例,这意味着它需求是**线程安全(thread-safe)的。当你想到这一点时,并发性真的很糟糕,但简单来说,如果单例在代码中构建不正确时,你能够让两个线程同时尝试初始化一个单例,这有或许给你返回一个单例的两个独立实例。这意味着它有或许不是仅有的,除非咱们让它是线程安全(thread-safe)**的。这意味着咱们要把初始化封装在一个
dispatch_once
GCD 块中,以确保初始化代码在运转时只运转一次。
在运用程序中,在一个当地做仅有的初始化很容易。在这篇文章的其余部分中,需求记住的是,单例要满意更难看的 dispatch_once
规矩。
Swift 单例
从 Swift 1.0 开端,就有几种办法来创立一个单例。这些办法在这儿、这儿和这儿都有十分广泛的介绍。但谁会喜欢点击链接呢?剧透警报;有四种改变。请答应我细数一下这些办法。
最丑恶的办法(又称 “如果你仅仅要这样做,为什么还要用 Swift 编码” 的办法)
class Singleton {
class var sharedInstance: Singleton {
struct Static {
static var onceToken: dispatch_once_t = 0
static var instance: Singleton? = nil
}
dispatch_once(&Static.onceToken) {
Static.instance = Singleton()
}
return Static.instance!
}
}
这种办法是将 Objective-C 的单例完成直接移植到 Swift 上。在我看来是很难看的,由于 Swift 本来便是要简洁明了,赋有表现力。比移植的人要好。要更好。 :P
结构体办法(又称 “陈旧但奇怪的是依然流行” 的办法)
class Singleton {
class var sharedInstance: Singleton {
struct Static {
static let instance: Singleton = Singleton()
}
return Static.instance
}
}
这便是咱们在 Swift 1.0 中不得不采纳的办法,由于那时的类还不支撑 static
类型的变量。然而,结构体却支撑静态变量。由于这些对静态变量的限制,咱们被迫采用了这样的形式,看起来就像上面这样。这比直接移植 Objective-C 好,但仍是不够好。风趣的是,在 Swift 1.2 发布几个月后,我依然看到这种写单例的办法。但后边会有更多的内容。
大局变量办法(也便是所谓的 “单线单例”)。
private let sharedInstance = SomeObject()
class SomeObject {
class var sharedInstance: SomeObject {
return sharedInstance
}
}
从 Swift 1.2 开端,咱们有了 **拜访操控说明符(access control specifiers)和静态类成员(static class members)**的能力。这意味着咱们不必让大局变量扰乱大局命名空间,咱们现在能够避免命名空间冲突。在我看来,这个版本的 Swiftier 许多。
现在,你或许会问为什么咱们在结构体或大局变量的完成中没有看到 dispatch_once
。依据 Apple 的说法,这些办法都满意了我上面概述的 dispatch_once
条款。以下是直接从他们的 Swift 博客中引用的一段话,证明了他们在底层被封装在 dispatch_once
块中:
“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’s Swift Blog
大局变量的慵懒初始化器(也适用于结构体和枚举的静态成员)会在第一次拜访该大局变量时运转,并以
dispatch_once
办法发动,以确保初始化是原子性的。这使得在你的代码中运用dispatch_once
成为一种很酷的办法:只要用初始化器声明一个大局变量并将其标记为private
。
就官方文档而言,这便是 Apple 给咱们的全部。但这意味着咱们所能证明的仅仅大局变量和结构体/枚举的静态成员。在这一点上,仅有有 Apple 文档支撑的 100% 安全的赌注是运用大局变量来慵懒地在dispatch_once
块中包裹单例初始化。但是咱们的静态类变量怎么办?
这个问题把咱们带到了下一个令人兴奋的部分:
正确的办法,又称 “一行代码完成单例(现在有证明了!”)。
class Singleton {
static let sharedInstance = Singleton()
}
所以我为这篇文章做了相当多的研讨。事实上,这篇文章的灵感来自于咱们今天在 Capital One 的一次说话,由于咱们审查了一个 PR,意图是在咱们的运用中适当的完成 Swift 单例一致性。咱们知道这种编写单例的 “正确” 办法,但除了估测,咱们没有任何证据来支撑咱们的推理。在没有满足文档的情况下,试图支撑这种办法是没有用的。这是我对互联网/博客圈中缺乏信息的说法。每个人都知道,如果它不在互联网上,就不是真的。这让我很难过。
我浏览了互联网的末端(也便是谷歌搜索结果的第10页),却一无所得。莫非还没有人公布一行代码完成单列的证据吗?也许他们有,但很难找到。
所以我决定做点什么,写出了每一种初始化单例的办法,并在运转时运用断点来查看它们。在分析了每个仓库盯梢的任何相似之处之后,我发现了一些风趣的东西–证明!这是我的一个想法。
看看吧,哟(哦,为班级的表情符号喝彩吧!)。
Using the Global Singleton
Using the One Line Singleton
第一张图片显现了一个大局 let
实例的仓库盯梢。红色的部分是咱们感兴趣的东西。在 Kraken 单例的实际初始化执行之前,有一个标记为 swift_once
的调用盯梢,后边是swift_once_block_invoke
调用。已然苹果公司说他们在 dispatch_once
块中慵懒地实例化了大局变量,咱们能够有把握地假定这便是他们的意思。
利用这些知识,我查看了咱们闪亮而美丽的一行代码完成单例的仓库盯梢。正如你在第二张图片中所看到的,它是完全相同的! 所以你有了它! 证明了咱们的一行代码完成单例是正确的。现在世界一切都好了。另外,已然这个帖子在互联网上呈现了,那就一定意味着它是真的!
不要忘了私有的初始化办法!
正如 Apple 的框架布道者 @davedelong 大方地指出的那样,你必须确保你的 inits 是 private
的。这能够确保你的单例是真实仅有的,并避免外部目标通过拜访操控来创立自己的类的实例。由于在 Swift 中,一切目标都有一个默许的 public
初始化器,你需求覆写你的 init
办法并使其私有化。这并不难做到,而且还能确保咱们的一行代码完成单例是美丽的:
class Singleton {
static let sharedInstance = Singleton()
private init() {} // 这样能够避免其他目标运用这个类的默许 '()' 初始化器。
}
这样做将确保当任何类试图运用 ()
初始化 Singleton
实例时,编译器会抛出这个错误:
就这样,你具有了它! 完美的、一行代码完成的单例。
总结
与 jtbandes 在 Stack Overflow 上对 swift singletons 的顶级答案的精彩谈论相照应,我底子无法在任何当地找到通过 “virtur of let” 来证明线程安全的文档。实际上,我记住上一年参与 WWDC 时说过相似的话,但你不能盼望读者或快速的 Googlers 在试图确认这是 Swift 中写单例的正确办法时偶尔发现。期望这篇文章能协助外面的人了解为什么 Swift 中的一行代码完成单例是个好办法。
编码高兴,书呆子们!