はじめに
あるObjective-C
のライブラリをSwift
で使っているとき(たぶんObjective-Cで使っても同じ)に、ライブラリ内のシングルトンがよくありがちな原因でクラッシュするという現象に遭遇しました。
原因は難しいことではないのですが、意外と見落としやすいので備忘録のような形でメモしておきます。
Objective-C
でのシングルトン
最近は下記のように書くことが多いかと思います。
@interface SampleClass: NSObject
@end
@implementation SampleClass
static SampleClass *_sharedInstance = nil;
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[self alloc] init];
});
return _sharedInstance;
}
@end
そして、上記のシングルトンを持ったクラスを継承したクラスを以下のものとします。
@interface SampleSubClass: SampleClass
- (NSString *)hogeString;
@end
@implementation SampleSubClass
- (NSString *)hogeString {
return @"hogeString";
}
@end
SampleClass
とSampleSubClass
をBridge-Header.h
を通して、Swift
で使用するとなると
SampleClass.sharedInstance()
とSampleSubClass.sharedInstance()
になり,
それぞれのsharedInstance()
メソッドで返却される型は、SampleClass!
とSampleSubClass!
になります。
実行してみる
print(SampleClass.sharedInstance()) //"SampleClass: 0x7fd58d8030c0"
print(SampleSubClass.sharedInstance()) //"SampleClass: 0x7fd58d8030c0"
結果はSampleClass
の同じインスタンスとなります。
順番を逆に呼び出してみると
print(SampleSubClass.sharedInstance()) //"SampleSubClass: 0x7f972a70fc80"
print(SampleClass.sharedInstance()) //"SampleSubClass: 0x7f972a70fc80"
結果はSampleSubClass
の同じインスタンスとなります。
はじめにsharedInstance()
にアクセスしたクラスのインスタンスがstatic SampleClass *_sharedInstance
に
1度だけ代入されるため、2回目以降にsharedInstance()
にアクセスした場合は上記のような結果になります。
Sample①
の順序で以下のような呼び出し方をすると、sharedInstance()
はSampelClass
のインスタンスを
返してくるためクラッシュします。
print(SampleClass.sharedInstance()) //"SampleClass: 0x7fd58d8030c0"
print(SampleSubClass.sharedInstance().hogeString()) //"unrecognized selector sent to instance 0x7fd58d8030c0"
Sample②
の順序では問題なく動きます。
print(SampleSubClass.sharedInstance().hogeString()) //"hoge"
print(SampleClass.sharedInstance()) //"SampleSubClass: 0x7f972a70fc80"
あえてSwift
で
Objective-C
と似たようなことをしようとするとこうなります。
class SampleClass: NSObject {
private static var _sharedInstance: SampleClass!
class func sharedInstance() -> Self {
struct Static {
static var onceToken: dispatch_once_t = 0
}
return sharedInstanceHelper(&Static.onceToken)
}
private class func sharedInstanceHelper<T>(onceToken: UnsafeMutablePointer<dispatch_once_t>) -> T {
dispatch_once(onceToken) {
_sharedInstance = self.init()
}
return _sharedInstance as! T
}
required override init() {
super.init()
}
}
class SampleSubClass: SampleClass {
func hogeString() -> String {
return "hoge"
}
}
print(SampleClass.sharedInstance()) //"SampleClass: 0x7fd58d8030c0"
print(SampleSubClass.sharedInstance().hogeString()) //"Could not cast value of type 'SampleClass' to 'SampleSubClass'."
SampleSubClass
に強制的にキャストしようとしてるので、return _sharedInstance as! T
で落ちます。
最後に
上記の結果を踏まえるとシングルトンとして扱われるクラスは、やはりfinal class
にすべきだと考えています。
final class SampleClass {
let sharedInstance = SampleClass()
}