编写单例的正确方式

原文: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 中的一行代码实现单例是个好方法。

编码快乐,书呆子们!

相关推荐
良技漫谈2 天前
Rust移动开发:Rust在iOS端集成使用介绍
后端·程序人生·ios·rust·objective-c·swift
KeithTsui3 天前
ZFC in LEAN 之 前集的等价关系(Equivalence on Pre-set)详解
开发语言·其他·算法·binder·swift
袁代码3 天前
Swift 开发教程系列 - 第4章:函数与闭包
ios·swift·ios开发
安泽13144 天前
高德地图美食
开发语言·swift·美食
袁代码4 天前
Swift 开发教程系列 - 第2章:Swift 基础语法
swift·ios开发·基础教程
袁代码4 天前
Swift 开发教程系列 - 第1章:Swift 简介与开发环境配置
swift·ios开发·基础教程
孚亭5 天前
一些swift问题
swift
莫问alicia5 天前
echarts 实现3D饼状图 加 label标签显示
前端·3d·echarts·swift
uiop_uiop_uiop7 天前
iOS Swift5算法恢复——HMAC
ios·iphone·swift
東三城8 天前
【ios】---SwiftUI开发从入门到放弃
ios·swiftui·swift·1024程序员节