[Swift] Protocol と ProtocolExtension

Swift

WWDC2015 の Protocol-Oriented Programming in Swift を見ての覚書です。

Protocol に定義するかどうかで振る舞いが変わるケース

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.2.1
  • Xcode 12.4
  • Swift5.3.2

protocol 定義により変わる振る舞い

先のビデオでも Swift は、protocol-oriented な言語だと何度も説明されていますが、実際に、protocol の重要性がわかる例です。

protocol に定義されているかどうかで振る舞いが変わるケースがあります。その意味を考察してみます。

Protocol に定義するかどうかで変わる振る舞い

Concrete な class/struct/enum と protocol extension とで同名のメソッド等が定義されているときに、そのメソッド等が protocol に定義されているかどうかで、実際の動作が変わります。

protocol で定義されていないケース

以下のようなコードです。

example

protocol General {
    // (1)
    //func func1()
}

func callFunc1(_ obj:General) {
    obj.func1()
}

extension General {
    func func1() {
        print("general func1")
    }
}

struct sConcrete: General {
    func func1() {
        print("sConcrete func1")
    }
}

let sc = sConcrete()

// (2)
sc.func1()        // -> "sConcrete func1"
// (3)
callFunc1(sc)     // -> "general func1"

コード解説
  1. protocol には、func1 は定義されていません
  2. sConcrete に定義した func1 が呼ばれます
  3. General の extension で定義された func1 が呼ばれます。

protocol で定義されているケース

以下のようなコードです。

example

protocol General {
    // (1)
    func func1()
}

func callFunc1(_ obj:General) {
    obj.func1()
}

extension General {
    func func1() {
        print("general func1")
    }
}

struct sConcrete: General {
    func func1() {
        print("sConcrete func1")
    }
}

let sc = sConcrete()

// (2)
sc.func1()        // -> "sConcrete func1"
// (3)
callFunc1(sc)     // -> "sConcrete func1"

コード解説
  1. protocol に、func1 が定義されています
  2. sConcrete に定義した func1 が呼ばれます
  3. sConcrete で定義した func1 が呼ばれ、protocol extension の func1 は呼ばれません

protocol 定義により異なる振る舞いについての考察

振る舞いが変わるのは、型情報として抽象的な情報しか持っていないケースです。

先の例では、直接 sc.func1() という呼び方は、sConcrete という型であることをわかっているケースなので、呼び出されるメソッドは常に一定です。

振る舞いが変わるのは、General という型情報しか持っていない callFunc1 という関数内から、メソッドを呼び出すときです。

コンパイラの気持ちになるとその違いが理解できるような気がします。

protocol で定義されていないとき

コンパイラー的には General という 型情報しかわかっていません。実際の型がなんであるかは、ランタイム時にしかわかりません。すくなくとも General を conform しているはずですが、func1 が存在するかどうかには役立つ情報ではありません。

ですので、protocol General とその extension をチェックし、存在することがわかるので、extension General で定義されている func1 を呼ぶことにします。

<確認>
Extension General の func1 をコメントアウトしてみると コンパイルエラーになります。ですので、コンパイル時に、extension General をチェックしていることがわかります。
sConcrete に func1 が定義されていても、コンパイラーは採用してくれません。

protocol で定義されているとき

コンパイラー的には、General という型情報しかわかっていないことは同じなのですが、General という型は、func1 というメソッドを持つことを保証してくれています。ですので、実際の型の func1 を呼ぶようにします。

<確認>
extension General の func1 をコメントアウトしても、コンパイルエラーになりません。sConcrete の func1 が使用されます。

(一旦、extension General の func1 のコメントアウトを解除して)
sConcrete の func1 をコメントアウトしても、コンパイルエラーになりません。extension General の func1 が使用されます。

extension General の func1 と sConcrete の func1 の両方をコメントアウトすると コンパイルエラーになります。

このことから、コンパイル時に、使用できる func1 を探してくれていることがわかります。どちらか片方に定義されているのであれば、存在する唯一のメソッドを使用します。

(複数存在するときは、実際の型に違いメソッドが使用されることはその動作からわかります)

protocol 定義により異なる振る舞いになることの意味

protocol に定義されていないという意味

protocol に定義しないということは、その protocol の性質を表しているものではないという意味と解釈されるのが妥当です。
ですので、その protocol に conform している型であっても、その性質を持っているかどうかはわかりません。つまり、conform した継承型にその性質は期待できず、コンパイラとしては、protocol extension に実装されたものを使用するということになります。

protocol に定義されているという意味

protocol に定義されているということは、その型の性質を表すものという解釈になります。

ですので、その protocol に conform している型であれば、その性質を持っているハズなので、コンパイラとしては、その具体的な型に実装されたものを使うことになります。

ただし、protocol レベルでその性質を提供しているケースが想定できるので、protocol extension での定義を採用することもできます。

具体的な型でオーバーライドしたいケースもあるので、具体的な型で、実装しているのであればそちらを採用します。

見方を変えると、protocol extension では、その protocol の性質の デフォルト実装を提供することができ、具体型によっては、より効率的な実装も採用することができるということになります。

まとめ:protocol と protocol extension

protocol と protocol extension
  • protocol には、そのプロトコルの性質を表すものを定義する
  • protocol での定義の有無により同名のメソッド等の振る舞いが変わるケースがあることを意識する
  • 「プロトコルの性質を表す」ということであっても、protocol extension でデフォルトの定義を提供すると具体的な型での実装を省略できる
  • 必要に応じて、具体的な型で、プロトコルで定義された性質を上書きすることができる
  • 上書きするときに、override のようなキーワード付与を強制することはできない

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です