こんにちは。 Swift Advent Calendar 11日目担当の @sato-shin です。
今日は、Objective-C で Protocol 宣言されたオプショナルなメソッドを呼び出すときの、Swift5 と Swift4系 の挙動の違いから、Access Controlがちょっと安全になったよというお話です。
SwiftでProtocolを宣言し、使ってみる
foo()
をもつFooProtocol
を定義します。なお、foo()
にはデフォルト実装が存在しています。
protocol FooProtocol {
func foo()
}
extension FooProtocol {
func foo() {}
}
このFooProtocol
に適合するBar
クラスを定義します。
class Bar: FooProtocol {
func foo() {}
}
この時、 Bar
に実装する foo()
の Access Control を private や fileprivate にするとコンパイルできなくなります。
class Bar: FooProtocol {
// ErrorMsg: Method 'foo()' must be declared internal because it matches a requirement in internal protocol 'FooProtocol'
private func foo() {}
}
Objective-CでProtocolを宣言し、使ってみる
では次に、 FooProtocol
と同じような使い方ができるObjcFooProtocol
を作ってみます。
FooProtocol
ではfoo()
のデフォルト実装が存在したため、適合側でfoo()
の実装は任意でした。
そこで、ObjcFooProtocol
でも同じ使い方ができるようにするため、foo()
はオプショナルであると宣言します。
@protocol ObjcFooProtocol <NSObject>
@optional
- (void)foo;
@end
ObjcFooProtocol
に適合する、Bar
クラスを swift で実装すると以下のようになります。
class Bar: NSObject, ObjcFooProtocol {
func foo() {}
}
Swift4とSwift5の挙動の違い
それでは、foo()
を private にし、Swift4 と Swift5 でコンパイルしてみましょう。
すると、Swift4ではコンパイルが成功し、Swift5ではコンパイルが失敗します。
class Bar: NSObject, ObjcFooProtocol {
// Swift4系ではコンパイルが成功する
//
// Swift5だとコンパイルが失敗する
// ErrorMsg: Method 'foo()' must be as accessible as its enclosing type because it matches a requirement in protocol 'ObjcFooProtocol'
private func foo() {}
}
このようにSwift5では、Swiftから見るObjective-CのProtocolに適合させるさいにprivateやfileprivateが許されなくなりました。
この変更により何がよくなったのか?
これによりDelegate実装のミスをなくすことができるようになりました。
どうミスが無くなるのか説明していきます。
プレゼントを届けることのできないサンタさん
以下のコードでは、ObjcSantaClaus
, Daddy
の二人が登場し、
サンタさんの代わりにパパさんがプレゼントを届けるということを表現しています。
ObjcSantaClaus
がdeliveryHappyItem()
をすると、
ObjcSantaClausDelegate
に適合するオブジェクトのputGift()
に動作が委譲されています。
Daddy
はObjcSantaClausDelegate
に適合しており、ObjcSantaClaus
の代わりにプレゼントを届けることができます。
#import <Foundation/Foundation.h>
@protocol ObjcSantaClausDelegate <NSObject>
@optional
- (void)putGift;
@end
@interface ObjcSantaClaus : NSObject
@property (weak, nonatomic) id <ObjcSantaClausDelegate> delegate;
- (void)deliveryHappyItem;
@end
#import "ObjcSantaClaus.h"
@interface ObjcSantaClaus ()
@end
@implementation ObjcSantaClaus
- (void)deliveryHappyItem {
if ([self.delegate respondsToSelector:@selector(putGift)]) {
[self.delegate putGift];
}
}
@end
class Daddy: NSObject, ObjcSantaClausDelegate {
func putGift() {
print("put a gift!")
}
}
let santa = ObjcSantaClaus()
let daddy = Daddy()
santa.delegate = daddy
santa.deliveryHappyItem() // put a gift! と表示される
これは正常に動作し、 put a gift! と表示され、無事にプレゼントが届きました。
問題
このとき、Daddy
クラスのputGift()
を privateに変更したらどうなるでしょうか?
class Daddy: NSObject, ObjcSantaClausDelegate {
private func putGift() {
print("put a gift!")
}
}
答え
- Swift4系ではコンパイルが成功するが、実行しても
Daddy
のputGift()
が呼ばれない。 - Swift5ではコンパイルが失敗する。
です。
なぜか
Daddy
がputGift()
を実装していても、private宣言をされていると、
SantaClausクラス側の [self.delegate respondsToSelector:@selector(putGift)]
でputGift()
へアクセスできないので、このようなことが起こります。
これで、Swift4ではObjcSantaClaus
がプレゼントを届けようとしてもプレゼントが届かないコードの完成です。
Swift5ではコンパイルが失敗し、上記のようなミスをできないので安全になりました。
まとめ
Swift5からは、Objective-Cによって定義されたprotocolに適合するよう実装するときにprivate, fileprivateを指定できなくなったため、
Swift4では気づきづらかった、 Delegate におけるの Access Control のミスをコンパイル時に見つけることができるようになりました。
蛇足
サンタさんはいます。25日にはサンタさんを追跡しましょう。
https://santatracker.google.com/