Swift5でちょっと安全になったAccess Control

こんにちは。 Swift Advent Calendar 11日目担当の @sato-shin です。

今日は、Objective-C で Protocol 宣言されたオプショナルなメソッドを呼び出すときの、Swift5 と Swift4系 の挙動の違いから、Access Controlがちょっと安全になったよというお話です。

# SwiftでProtocolを宣言し、使ってみる
`foo()`をもつ`FooProtocol`を定義します。なお、`foo()`にはデフォルト実装が存在しています。

```swift
protocol FooProtocol {
    func foo()
}
extension FooProtocol {
    func foo() {}
}
```

この`FooProtocol`に適合する`Bar`クラスを定義します。

```swift
class Bar: FooProtocol {
   func foo() {}
}
```

この時、 `Bar` に実装する `foo()` の Access Control を private や fileprivate にするとコンパイルできなくなります。

```swift
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()`はオプショナルであると宣言します。

```objc:ObjcFooProtocol.h
@protocol ObjcFooProtocol <NSObject>

@optional
- (void)foo;

@end
```

`ObjcFooProtocol` に適合する、`Bar` クラスを swift で実装すると以下のようになります。

```swift
class Bar: NSObject, ObjcFooProtocol {
   func foo() {}
}
```

# Swift4とSwift5の挙動の違い

それでは、`foo()`を private にし、Swift4 と Swift5 でコンパイルしてみましょう。
すると、Swift4ではコンパイルが成功し、Swift5ではコンパイルが失敗します。

```swift:Bar.swift
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`の代わりにプレゼントを届けることができます。

```objc:ObjcSantaClaus.h
#import <Foundation/Foundation.h>

@protocol ObjcSantaClausDelegate <NSObject>

@optional
- (void)putGift;

@end


@interface ObjcSantaClaus : NSObject

@property (weak, nonatomic) id <ObjcSantaClausDelegate> delegate;

- (void)deliveryHappyItem;

@end
```

```objc:ObjcSantaClaus.m
#import "ObjcSantaClaus.h"

@interface ObjcSantaClaus ()
@end

@implementation ObjcSantaClaus

- (void)deliveryHappyItem {
    if ([self.delegate respondsToSelector:@selector(putGift)]) {
        [self.delegate putGift];
    }
}

@end
```

```swift:Daddy
class Daddy: NSObject, ObjcSantaClausDelegate {
    func putGift() {
        print("put a gift!")
    }
}
```

```swift:プレゼントを届ける
let santa = ObjcSantaClaus()
let daddy = Daddy()
santa.delegate = daddy
santa.deliveryHappyItem() // put a gift! と表示される
```

これは正常に動作し、 put a gift! と表示され、無事にプレゼントが届きました。

### 問題

このとき、`Daddy`クラスの`putGift()`を privateに変更したらどうなるでしょうか？

```swift
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/