THE RIGHT WAY TO WRITE A SINGLETON


Even though I've written about the woes of managing state in my previous post, sometimes there's just no way we can avoid it. One example of managing state is something we're all quite acquainted with - The Singleton. The problem we find in Swift is that there are SEVERAL ways of implementing them. But which way is the right way? In this post I'm going to show you the history of the singleton and then show you the right way to implement the singleton in Swift.

If you want to see the right way to implement the singleton pattern in Swift along with proof of it's "right-ness", you can scroll to the bottom of the post and see it there. :)


A TRIP DOWN MEMORY LANE

Swift is a natural evolution of Objective-C. In Objective-C, this is how we implemented the singleton:

@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

Now that we have that out of the way and we can see the basic structure of a singleton, let's lay out some rules so we understand what we're looking at:

DA RULES OF DA SINGLETON, MAHN

There are essentially three things to remember about the Singleton:

  • A singleton has to be unique. This is why it's called a singleton. There can only be one instance for the lifetime of the application it exists in. Singletons exist to give us singular global state. Such examples are NSNotificationCenter, UIApplication, and NSUserDefaults.
  • To maintain a singleton's unique-ness, the initializer of a singleton needs to be private. This helps to prevent other objects from creating instances of your singeton class themselves. Thank you to all who pointed that out to me :)
  • Because of rule #1, in order to have only one instance throughout the lifetime of the application, that means it needs to be thread-safe. Concurrency really sucks when you think about it, but simply put, if a singleton is built incorrectly in code, you can have two threads try to initialize a singleton at the same time which can potentially give you two separate instances of a singleton. This means that it's possible for it to not be unique unless we make it thread-safe. This means we want to wrap the initialization in a dispatch_once GCD block to make sure the initialization code only runs once at runtime.

Being unique and initializing in one place in an app is easy. The important thing to remember for the rest of this post is that a singleton fulfill the much-harder-to-see dispatch_once rule.

DA SWIFT SINGLETON

Since Swift 1.0, there have been several ways to create a singleton. These have been covered very extensively here, here, and here. But who likes clicking on links? SPOILER ALERT; There are four variations. Allow me to count the ways:

THE UGLIEST WAY (A.K.A. THE "WHY ARE YOU STILL CODING IN SWIFT IF YOU'RE JUST GOING TO DO THIS" WAY)

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!
    }
}

This way is a straight port of Objective-C's singleton implementation over to Swift. Ugly in my opinion because Swift was meant to be terse and expressive. Be better than the port guys. Be better. :P

THE STRUCT WAY (A.K.A. THE "OLD BUT STRANGELY STILL POPULAR" WAY)

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

This way was how we had to do it in Swift 1.0 since classes still didn't support static class variables back then. Structs, however, did support them! Because of these restrictions on static variables, we were forced into a model that looked like this. It's better than the straight Objective-C port but still not good enough. Funnily enough, I still see this method of writing singletons several months after the release of Swift 1.2. But more on that later.

THE GLOBAL VARIABLE WAY (A.K.A. THE "ONE LINE SINGLETON")

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

As of Swift 1.2, we gained access control specifiers and the ability to have static class members. This meant that we didn't have to have a global variable clutter the global namespace and we could now prevent namespace collisions. This version is a lot Swiftier in my opinion.

Now at this point, you may be asking why we don't see dispatch_once in our struct or global variable implementations. Well according to Apple, both of these methods fulfills the dispatch_once clause I outlined above. Here's a quote straight from their Swift Blog that proves that they are wrapped in dispatch_once blocks behind the scenes:

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

That's all Apple gave us as far as official documentation goes. But this meant that all we had proof for was global variables and static members of structs/enums! At that point, the only 100% safe bet backed by Apple docs was using a global variable to lazily wrap singleton initialization in a dispatch_once block. BUT WHAT ABOUT OUR STATIC CLASS VARIABLES?!?!?!?

This very question brings us to this next exciting section:

THE RIGHT WAY A.K.A. "THE ONE LINE SINGLETON (NOW WITH PROOF!")

class TheOneAndOnlyKraken {
    static let sharedInstance = TheOneAndOnlyKraken()
}

So I've done a fair amount of research for this post. In fact, this post was inspired by a conversation we had at Capital One today due to the review of a PR that aimed at achieving proper singleton consistency in Swift across our app. We knew about this "right" method of writing singletons, but we had no proof to back up our reasoning other than postulation. Trying to back this method up without sufficient documentation was useless. It was my word against a lack of information on the internet/blogosphere. And everyone knows that if it isn't on the Internet it isn't true. This made me sad.

I navigated to the far ends of the internets (AKA the 10th page of Google Search Results) and came up empty handed. Had no one posted proof of the one line singleton yet?! Maybe they have, but it was hard to find.

So I decided to do something about it and wrote up every way of initializing a singleton and inspected them at runtime using breakpoints. After analyzing each stack trace for any similarities I came across something interesting - PROOF!

Check it out, yo (Oh, and yay for class emojis!):

Using the Global Singleton
Using the One Line Singleton

The first image shows the stack trace of a global let instantiation. Outlined in red is the thing of interest here. Before the actual initialization of the Kraken singleton is a call trace labeled swift_once followed by a swift_once_block_invoke call. Since Apple said they lazily instantiate global variables in a dispatch_once block, we can safely assume this is what they meant.

Using this knowledge, I inspected the stack trace of our shiny & pretty one-line-singleton. As you can see with our second image, it's exactly the same! So there you have it! Proof that our one line singleton is proper. All is now right with the world. Also, now that this post is on the Internet, that MUST mean it's true!

wink wink

DON'T FORGET THE PRIVATE INIT!

As @davedelong, Frameworks Evangelist at Apple, graciously pointed out to me, you have to make sure that your inits are private. This makes sure your singletons are truly unique and prevents outside objects from creating their own instances of your class through virtue of access control. Since all objects come with a default public initializer in Swift, you need to override your init and make it private. This isn't too hard to do and still ensures our one line singleton is nice and pretty:

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

Doing this will makes sure that the compiler throws this error when any class attempts to initialize TheOneAndOnlyKraken using the () initializer:

And there you have it! The perfect, one-line singleton.

CONCLUSION

Echoing jtbandes' excellent comment on the top rated answer to swift singletons on Stack Overflow, I simply could not find documentation anywhere that proved thread-safety by "virtue of let". I actually remember something of the sort said when I was at WWDC last year, but you can't expect readers or quick Googlers to stumble across that when trying to make sure this is the right way to write a singleton in Swift. Hopefully, this post can help someone out there understand why the one-line singleton in Swift is the way to go.

转自:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • 在这个数据信息大幅度可视化的年代,在工作和生活中演讲要比文字报告更加通用和重要。而很多人,明明很辛苦地做了很多工作...
    火羽白628阅读 596评论 0 0
  • 昨晚和一群9394后唱k、聚餐、聊人生聊理想,当说到旅游的时候,他们说得头头是道:哪里哪里好玩,年纪轻轻去过哪里,...
    隔壁村的教主阅读 445评论 0 2
  • 每个奋斗者都感觉自己永远在路上,越过无数个山峰,前面是更高的山峰;经历了无数个驿站,前面还是路途遥遥。孤独地前行,...
    目送归鸿阅读 1,377评论 2 6