Swift で Singleton Pattern
AppleのSwiftで Singleton Pattern を実装する方法について調べてみました。あちこちで言及されているみたいなので、今更感が強いですが。だから実装方法だけではなんなので、スレッドセーフなことを検証する XCTest も最後に書いてみました。
作ったコードは GitHub においておきます。Xcode 6 Beta4 で確認しています。
→ https://github.com/tsntsumi/SingletonPatternInSwift
(git@github.com:tsntsumi/SingletonPatternInSwift.git)
遅延初期化してスレッドセーフな実装方法には 3 つあります。
- グローバル定数を使用する方法
- ネストした構造体を使用する方法
- dispatch_once を使用する方法
グローバル定数を使用する方法
グローバル定数を使用する方法が最もシンプルですが、あまりグローバルスコープは汚したくないなと思います。Xcode 6 Beta4 からはアクセスコントロールがサポートされたので、private をつければよいとはいえ、それでも同じファイル内でアクセスできてしまうところが難点です。
グローバル定数を使用するのは、Swift ではクラス定数がまだサポートされていないためです。この方法では遅延初期化とスレッドセーフをサポートします。
private let globalConstantSharedInstance = GlobalConstantSingleton() public final class GlobalConstantSingleton { public class var sharedInstance: GlobalConstantSingleton { return globalConstantSharedInstance } private init() { NSLog("init GlobalConstantSingleton") sleep(3) // 初期化に時間をかけることで、スレッドセーフかどうかを検証できるようにする。 } }
ネストした構造体を使用する方法
ネストした構造体というのは、クラスの中で構造体を宣言する方法です。クラスと違って構造体はフィールドに static constant を定義できますので、ネストすることでその static constant をクラス定数として使用することができます。
この方法でも遅延初期化とスレッドセーフをサポートします。
public final class NestedStructSingleton { public class var sharedInstance: NestedStructSingleton { struct Static { static let instance: NestedStructSingleton = NestedStructSingleton() } return Static.instance } private init() { NSLog("init NestedStructSingleton") sleep(3) // 初期化に時間をかけることで、スレッドセーフかどうかを検証できるようにする。 } }
dispatch_once を使用する方法
dispatch_once を使用する方法は、従来 Objective-C で使われてきた方法です。ネストした構造体と同様の方法を使いますが、コードが冗長になってしまうようです。
この方法でももちろん遅延初期化とスレッドセーフをサポートします。
public final class DispatchOnceSingleton { public class var sharedInstance: DispatchOnceSingleton { struct Static { static var instance: DispatchOnceSingleton? static var onceToken: dispatch_once_t = 0 } dispatch_once(&Static.onceToken){ Static.instance = DispatchOnceSingleton() } return Static.instance! } private init() { NSLog("init DispatchOnceSingleton") sleep(3) // 初期化に時間をかけることで、スレッドセーフかどうかを検証できるようにする。 } }
スレッドセーフでない方法
最後に対照的な実装方法として、ネストした構造体を使用しますが定数を使用しない方法です。
遅延初期化をサポートしていますが、スレッドセーフではない方法でインスタンスを生成しているため、タイミングによっては複数のインスタンスが生成されてしまいます。
public final class ThreadUnsafeSingleton { public class var sharedInstance: ThreadUnsafeSingleton { struct Static { static var instance: ThreadUnsafeSingleton? } if !Static.instance { Static.instance = ThreadUnsafeSingleton() } return Static.instance! } private init() { NSLog("init ThreadUnsafeSingleton") sleep(3) // 初期化に時間をかけることで、スレッドセーフかどうかを検証できるようにする。 } }
テストケース
このクラスを、次のようなテストケースで検証してみます。
class ThreadUnsafeSingletonTests: XCTestCase { func testSharedInstance_ThreadUnsafety() { var instance1: ThreadUnsafeSingleton? var instance2: ThreadUnsafeSingleton? let expectation1 = expectationWithDescription("Instance 1") dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { instance1 = ThreadUnsafeSingleton.sharedInstance expectation1.fulfill() } let expectation2 = expectationWithDescription("Instance 2") dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { instance2 = ThreadUnsafeSingleton.sharedInstance expectation2.fulfill() } waitForExpectationsWithTimeout(6.0) { (_) in // 二つのインスタンスが等しくないことをテスト。 XCTAssertTrue(instance1 !== instance2, "") } } }
二つのスレッドからシングルトンインスタンスにアクセスしています。上で紹介したThreadUnsafeSingletonクラスでは、初期化で sleep() しているため時間がかかり、インスタンスが複数生成されてしまいます。
このテストケースでは最後の方で二つのインスタンスが等しくないことをテストしていますが、シングルトンであるにも関わらずテストが成功してしまいます。