[Swift] Protocol の拡張について
はじめに
Protocol を拡張するにあたって extension
であったり、 where
の絞りこみがよくわかっていなかったので整理してみました。
(Xcode:12.5.1)
基本的な使い方
例:Protocol で鳥が 「息をすること」 = breathe()
と「飛ぶこと」 = fly()
を表現します。
protocol BirdProtocol {
func breathe()
func fly()
}
class Crow: BirdProtocol {
func breathe() {}
func fly() {}
}
Playground で BirdProtocol を継承した Class を宣言すると勝手に上のように関数の定義をサポートしてくれます。
拡張方法
例:カモのように泳げる鳥もいるので、「泳げること」 = swim()
も追加してあげます。
この場合に拡張する方法を 3 つ紹介したいと思います。
- 拡張方法その1
- 複数の Protocol を継承させる ← おすすめ
- 拡張方法その2
- 既存の Protocol を継承した Protocol を継承させる
- 拡張方法その3
- 既存の Protocol を extension する
拡張方法その1: 複数の Protocol を継承させる
Protocol の定義
protocol SwimAble {
func swim()
}
Class の定義
Class の定義の際に複数の Protocol を継承させることが可能です。
class KamoA: BirdProtocol, SwimAble {
func breathe() {}
func fly() {}
func swim() {}
}
下記のように extension で分けて表現するとこともできます。
class KamoA: BirdProtocol {
func breathe() {}
func fly() {}
}
extension KamoA: SwimAble {
func swim() {}
}
実行してみる
実際に swim()
を実行する際には、以下のように Classを格納する変数の型の表現を気を付けてあげる必要があります。
// 例1
let birdA1 = KamoA() // 型指定なし
birdA1.swim() // 勝手にSwimAbleを適応してくれる
// 例2
let birdA2: BirdProtocol = KamoA() // BirdProtocolの型指定する
// birdA2.swim() // BirdProtocolにはswim()がないのでコンパイルエラーとなる!!
// 例3
let birdA3: BirdProtocol & SwimAble = KamoA() // 複数のProtocolを指定する場合はこう書く
birdA3.swim() // これならswim()できる
使い所
一番一般的な拡張のさせ方の認識です。
迷ったらこれでいいと思います。
拡張方法その2: 既存の Protocol を継承した Protocol を継承させる
既存の Protocol を継承した Protocol を作成します。
Protocol の定義
protocol SwimAbleBird: BirdProtocol {
func swim()
}
ちなみに、以下のように書くこともできます。
ただし、Protocol の宣言では where
を使わず、extension 時に拡張したい範囲を絞るために where
を用いる認識です。
// こうはあまり書かないはず?
protocol SwimAbleBird where Self: BirdProtocol {
func swim()
}
Class の定義
SwimAbleBird
を継承させるだけで、BirdProtocol
の機能も保証してくれます。
class KamoB: SwimAbleBird {
func breathe() {}
func fly() {}
func swim() {}
}
実行してみる
複数 Protocol を 継承した場合に比べて、型宣言するときに BirdProtocol & SwimAble
のように表現しなくても、SwimAbleBird
だけでシンプルに swim()
を実行することができます。
// 例1
let birdB1 = KamoB()
birdB1.swim()
// 例2
let birdB2: SwimAbleBird = KamoB() // SwimAbleBirdの型を指定する
birdB2.swim() // SwimAbleBirdにはswim()がある
使い所
あきらかに特性が異なる Protocol を追加する場合に使う認識です。
なので、今回のような泳げる機能を追加するだけではケースでは使わない認識です。
例えば、今は鳥だけを扱っていますが、哺乳類などを扱う種類が増えていくと、どんどん Protocol を増やしていくことになってしまったりして、困ります。
拡張方法その3: 既存の Protocol を extension する
Protocol の定義
protocol SwimAble {}
extension BirdProtocol where Self: SwimAble {
// Protocol を extension する場合は {} の中身まで書かなければならない
func swim() {
fatalError("override required")
}
}
Class の定義
class KamoC: BirdProtocol, SwimAble {
func breathe() {}
func fly() {}
func swim() {
print("swim") // overrideする
}
}
実行してみる
型指定しない場合のみしか、KamoC
での override した swim()
しか使えず、BirdProtocol & SwimAble
と型指定した場合は KamoC
での override した swim()
は無視されるという謎の挙動になります。
// 例1
let birdC1 = KamoC()
birdC1.swim() // "swim"
// 例2
let birdC2: BirdProtocol = KamoC()
// birdC2.swim() // コンパイルエラー
// 例3
let birdC3: BirdProtocol & SwimAble = KamoC()
birdC3.swim() // 実行時エラー ( Fatal error: override required ) ← overrideできていない理由は不明
共通で swim()
処理を使うのであれば extension BirdProtocol where Self: SwimAble
時に実態の swim()
処理を書けば問題ないが、Class 側で個別に実装したい時は、このように思わぬ事故が発生しそう。。。
使い所
個別の Class では実装させないような場合に用いるイメージです。(以下の機能制限のような例。)
【応用】 機能制限
例:ダチョウやペンギンのように飛べない鳥がいるので、それを BirdProtocol
を extension させて表現します。
ポイントは where Self: FlyUnable
とすることで、他の BirdProtocol
を継承した Class には影響させないというところです。
protocol FlyUnable {}
extension BirdProtocol where Self: FlyUnable {
func fly() {
fatalError("Can't fly.")
}
}
class DachoA: BirdProtocol, FlyUnable {
func breathe() {}
}
let birdA = DachoA()
birdA.breathe()
birdA.fly() // 実行時エラーが起こる
下記のように BirdProtocol
を継承させた FlyUnableBird
を定義することでも表現可能です。
protocol FlyUnableBird: BirdProtocol {}
extension BirdProtocol where Self: FlyUnableBird {
func fly() {
fatalError("Can't fly.")
}
}
class DachoB: FlyUnableBird {
func breathe() {}
}
let birdB = DachoB()
birdB.breathe()
birdB.fly() // 実行時エラーが起こる
最後に
なるべく細かく Protocol で機能を分割していくのが Swift っぽいのかもしれません。
Discussion