编写单例的正确方式

原文:The Right Way To Write a Singleton --- KrakenDev

尽管我在上一篇文章中写到了管理状态的苦恼,但有时我们无法避免它。管理状态的一个例子是我们都很熟悉的东西------单例(Singleton)。我们在 Swift 中发现的问题是,有很多方法来实现它。但哪种才是正确的方法?在这篇文章中,我将向你展示单例的历史,然后向你展示在 Swift 中实现单例的正确方式

如果你想查看在 Swift 中实现单例模式的正确方法,以及证明它的 "正确性",你可以滚动到文章的底部。:)

回忆之旅

Swift 是 Objective-C 的进化版本。在 Objective-C 中,我们就是这样实现单例的:

objectivec 复制代码
@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 编码" 的方式)

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

结构体方式(又称 "古老但奇怪的是仍然流行" 的方式)

swift 复制代码
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 发布几个月后,我仍然看到这种写单例的方法。但后面会有更多的内容。

全局变量方式(也就是所谓的 "单线单例")。

swift 复制代码
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

swift 复制代码
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 方法并使其私有化。这并不难做到,而且还能保证我们的一行代码实现单例是漂亮的:

swift 复制代码
class Singleton  {
    static let sharedInstance = Singleton()
    private init() {} // 这样可以防止其他对象使用这个类的默认 '()' 初始化器。
}

这样做将确保当任何类试图使用 () 初始化 Singleton 实例时,编译器会抛出这个错误:

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

总结

jtbandesStack Overflow 上对 swift singletons 的顶级答案的精彩评论相呼应,我根本无法在任何地方找到通过 "virtur of let" 来证明线程安全的文档。实际上,我记得去年参加 WWDC 时说过类似的话,但你不能指望读者或快速的 Googlers 在试图确定这是 Swift 中写单例的正确方法时偶然发现。希望这篇文章能帮助外面的人理解为什么 Swift 中的一行代码实现单例是个好方法。

编码快乐,书呆子们!

相关推荐
I烟雨云渊T1 小时前
iOS swiftUI的实用举例
ios·swiftui·swift
大熊猫侯佩2 小时前
SwiftUI 中为何 DisclosureGroup 视图在收缩时没有动画效果?
swiftui·swift·apple
大熊猫侯佩2 小时前
Swift 初学者交心:在 Array 和 Set 之间我们该如何抉择?
数据结构·性能优化·swift
大熊猫侯佩1 天前
Swift 中更现代化的调试日志系统趣谈(一)
debug·swift·apple
大熊猫侯佩1 天前
Swift 中更现代化的调试日志系统趣谈(二)
debug·swift·apple
Swift社区1 天前
Swift 解法详解:如何在二叉树中寻找最长连续序列
开发语言·ios·swift
nenchoumi31191 天前
Swift 6 学习笔记(二)The Basics
笔记·学习·swift
Fatbobman(东坡肘子)2 天前
WWDC 2025 开发者特辑 | 肘子的 Swift 周报 #088
开发语言·macos·ios·swiftui·ai编程·swift·wwdc