原文: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)的。这便是为什么它被称为单例。在它在运用程序中存活的生命周期内,只能有一个实例。单例的存在是为了给咱们供给单一的大局状况。这样的比如有NSNotificationCenterUIApplicationNSUserDefaults
  • 为了坚持单例的仅有性,单例的初始化构造器需求是私有(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 块中包裹单例初始化。但是咱们的静态类变量怎么办?

这个问题把咱们带到了下一个令人兴奋的部分:

正确的办法,又称 “一行代码完成单例(现在有证明了!”)。

官方文档:Managing a Shared Resource Using a Singleton

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 实例时,编译器会抛出这个错误:

编写单例的正确办法

就这样,你具有了它! 完美的、一行代码完成的单例。

总结

jtbandesStack Overflow 上对 swift singletons 的顶级答案的精彩谈论相照应,我底子无法在任何当地找到通过 “virtur of let” 来证明线程安全的文档。实际上,我记住上一年参与 WWDC 时说过相似的话,但你不能盼望读者或快速的 Googlers 在试图确认这是 Swift 中写单例的正确办法时偶尔发现。期望这篇文章能协助外面的人了解为什么 Swift 中的一行代码完成单例是个好办法。

编码高兴,书呆子们!

编写单例的正确办法