🕊

[Swift] Protocol の拡張について

2021/12/25に公開

はじめに

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 っぽいのかもしれません。

GitHubで編集を提案

Discussion