Swift の protocol における Interface と Method の違いを理解しよう

  • 9
    Like
  • 0
    Comment

はじめに

本記事は今月頭に開かれた Otemachi.swift x Kyobashi.swift 合同勉強会で発表した資料をベースに作成しております。

本題

常識かと思いますが、下記のような二つの protocol 宣言は全く別物だということを常に意識せねばなりません。

ProtocolA
protocol SomeProtocol {
    func printSelf()
}

extension SomeProtocol {
    func printSelf() {
        print("SomeProtocol")
    }
}
ProtocolB
protocol SomeProtocol {
    //func printSelf()
}

extension SomeProtocol {
    func printSelf() {
        print("SomeProtocol")
    }
}

一行だけの違いですが、protocol SomeProtocol の定義に Interface の宣言があるかどうかで、後の extension での実装の振る舞いは完全に別物になります。どう違うかというと、下記の例を読めばわかりやすいかと

ProtocolA
protocol SomeProtocol {
    func printSelf()
}

extension SomeProtocol {
    func printSelf() {
        print("SomeProtocol")
    }
}

class SomeClass: SomeProtocol {
    func printSelf() {
        print("SomeClass")
    }
}

let sc = SomeClass()

sc.printSelf() // 出力:SomeClass

(sc as SomeProtocol).printSelf() // 出力:SomeClass
ProtocolB
protocol SomeProtocol {

}

extension SomeProtocol {
    func printSelf() {
        print("SomeProtocol")
    }
}

class SomeClass: SomeProtocol {
    func printSelf() {
        print("SomeClass")
    }
}

let sc = SomeClass()

sc.printSelf() // 出力:SomeClass

(sc as SomeProtocol).printSelf() // 出力:SomeProtocol

ほとんど同じようなものですが、protocol SomeProtocol の宣言に func printSelf() を宣言した場合、(sc as SomeProtocol).printSelf() の出力結果は class SomeClass に実装したものになりますが、そうでない場合は extension SomeProtocol に実装したものになります。

なぜこのような違いがあるかというと、protocol の宣言時に宣言した関数は、この protocol の Interface(以下 PI と呼びます)となります。この protocol を準拠した class もしくは struct は、すべてこの PI があることが保証されています。ただし、extension ではこの PI のデフォルト実装があるので、もし準拠した class か struct にこの PI の実装を書いてなければ、extension で定義した実装を流用します。一方、宣言時にこの関数を宣言せず、extension でだけこの関数を実装した場合、これは PI としてのデフォルト実装ではなく、この protocol 固有の Method(以下 PM と呼びます)となります。

従って、(sc as SomeProtocol).printSelf() を呼び出す時、ProtocolA の場合は PI を呼び出しているので、extension のデフォルト実装関係なく、本来の実装である SomeClassprintSelf() を呼び出すが、ProtocolB の場合は PM を呼び出すことになります。

ちなみに PM の呼び出しはこちらのスライドを読めばわかる通り、静的 Dispatch となります。

発展

先ほど PI と PM の違いについて論じましたが、PM は静的 Dispatch なので何も難しいことはないですが、問題は PI の実装と class の継承が同時に入って来た場合どうなるのかというと、話がややこしくなります。

ちょっと複雑なコードになりますが、こちらをごらんください:

protocol SomeProtocol {
    func printSelf()
}

extension SomeProtocol {
    func printSelf() {
        print("SomeProtocol")
    }
}

class SomeClass: SomeProtocol {

}

class SomeSubClass: SomeClass {
    func printSelf() {
        print("SomeSubClass")
    }
}

class Parent {
    let someClass: SomeClass
    // init(someClass: SomeClass) { ... }
    func printSelf() {
        someClass.printSelf()
    }
}

let ssc = SomeSubClass()
let p = Parent(someClass: ssc)

ssc.printSelf() // 出力:SomeSubClass
p.printSelf() // 出力:?

/* 選択肢
A:SomeProtocol
B:SomeClass
C:SomeSubClass
*/

簡単に説明しますと、先ほどの ProtocolA の場合と同じように、protocol SomeProtoclfunc printSelf() という PI を宣言しました。そして extension SomeProtocol にこの printSelf() のデフォルト実装をしました。ここで class SomeClassSomeProtocol に準拠させました。ただし printSelf() の独自実装を行なっていないので当然ながら printSelf()SomeProtocol のデフォルト実装になります。ここでさらに SomeClass を継承した SomeSubClass を登場させます。継承なので当然 SomeSubClass もスーパークラスと同じように SomeProtocol に準拠しています。なのでこの SomeSubClassprintSelf() を独自実装させてみました。ここでさらに class Parent という新たなクラスを登場させます。このクラスには someClass という SomeClass 型のプロパティーがあり、ParentprintSelf() を呼び出すと、someClass.printSelf() を実行させます。ここで ParentsomeClass プロパティーが SomeSubClass のインスタンス ssc の場合、ParentprintSelf() の出力は果たしてどうなるのでしょうか?

実は発表のとき、この質問を当時現地にいた参加者に投げてみたところ、意外なことに正解者が一人もいませんでした(筆者の予想では多分 A と C で半々くらいになるんじゃないかと思ったのですが)…

ところでここまで読んだ読者の皆さんはどれが正解だと思いますか?

まあ正解を発表する前に、まず選択肢を見ていきましょう。A:SomeProtocol は PI のデフォルト実装、B:SomeClass は前のコードに書いた SomeClass の独自実装ですがここでそもそも登場してないのでまず可能性としてはありえないです。そして最後の C:SomeSubClass は今回のコードで書いた SomeSubClass の独自実装です。

ここで p.printSelf() を呼び出している際、実行されているのは P#someClass.printSelf() ですので、言い換えれば (ssc as SomeClass).printSelf() と同価です。

普通クラスを継承したとき、きちんと正しい振る舞いをさせるために、仮に継承した親クラスとして実行しても、実際行われているのは自分自身の実装になります。例えば

class Super {
    func printSelf() {
        print("I'm Super!")
    }
}

class Sub: Super {
    override func printSelf() {
        print("I'm Sub!")
    }
}

let sub = Sub()
(sub as Super).printSelf() // 出力:I'm Sub!

こちらのコードを実行し、subSuper として printSelf() を呼び出しても、呼び出されているのは Sub で実装した print("I'm Sub!") です。ですので逆にいうと、ssc.printSelf() であろうか、(ssc as SomeClass).printSelf() であろうか、出力結果は同じ、SomeSubClass が実装した print("SomeSubClass") になるはず。なので、正解は C:SomeSubClass。そう思いませんか?

そう思うみなさんは実際このコードを Playground で実行して見ましょう。驚きの結果がおまちしております。

そう、本当の正解は A:SomeProtocol です。

なぜそうなったのでしょうか。実は class SomeClassfunc printSelf() を独自実装しなかったため、SomeClassSomeProtocol としての printSelf() はすでに SomeProtocol のデフォルト実装で確定されました。SomeSubClass が実装した func printSelf() は、SomeProtocol の PI ではなく、SomeSubClass 特有の printSelf() です。つまり SomeSubClassprintSelf() は、SomeProtocolprintSelf() とは全く無関係です。

その証拠に、SomeSubClassfunc printSelf() 実装に、override キーワードがないことをご注意ください。SomeSubClass は親のメソッドをオーバーライドしていないのです。だから、(ssc as SomeClass).printSelf() を呼び出す場合、プログラムはそもそも ssc には独自の printSelf() 実装があるのは知らず、SomeClassprintSelf() を呼び出し、しかし SomeClass は独自の printSelf() がないため、準拠した SomeProtocolprintSelf() のデフォルト実装を呼び出すことになります。

ですので当然、(ssc as SomeProtocol).printSelf() を呼び出しても、結果は同じ print("SomeProtocol") になります。

だからなに

この protocol の振る舞いを知らないと、バグを生み出す可能性が出てきます。例えば何かのクラスを作ったとき、そのクラスが準拠したプロトコルのデフォルト実装で OK だとしても、自分を継承したサブクラスがそのデフォルト実装では足りない場合があります。ですので継承のことを考えて、仮に自分がデフォルト実装で OK でも、きちんと独自実装してデフォルト実装を呼び出しましょう。(もちろん継承させないように final をつけるのも一つの解決ですが

class SomeClass: SomeProtocol {
    func printSelf() {
        (self as SomeProtocol).printSelf()
    }
}

余談

実はこの仕様、以前にもすでに一度混乱を起こしたことがあります