138
115

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Swiftのプロトコルエクステンションの罠

Last updated at Posted at 2016-03-03

try-swiftで知り合った @tarunon さんから面白い話を伺いました。

以下、Xcode 7.2.1 (7C1002)の環境での話です。

結論

どうしたらいいかわかりません。。

本題

下記のように、
エクステンションとしてhogeメソッドを持つHogeableプロトコルと、
それを実装したクラスCatを考えます。

protocol Hogeable {
}
extension Hogeable {
	func hoge() -> String {
		return "This is from Hogeable Extension"
	}
}
class Cat : Hogeable {
	func hoge() -> String {
		return "This is from Cat"
	}
}

さて、この場合Catのhogeを呼び出す際、
そのメソッドレシーバが 静的に 評価された型によって結果が代わります。

let cat = Cat()
cat.hoge()               // "This is from Cat"
(cat as Hogeable).hoge() // "This is from Hogeable Extension"

これもJavaやRubyのような言語の感覚では少し不思議ですが、
これは理解できる仕様です。

さて、ここでCatに親クラスAnimalを与えて、
hogeをCatでオーバライドするようにしてみます。

protocol Hogeable {
}
extension Hogeable {
    func hoge() -> String {
        return "This is from Hogeable Extension"
    }
}

class Animal : Hogeable {
    func hoge() -> String {
        return "This is from Animal"
    }
}

class Cat : Animal {
    override func hoge() -> String {
        return "This is from Cat"
    }
}

特に結果はかわりません。

let cat = Cat()
cat.hoge()               // "This is from Cat"
(cat as Hogeable).hoge() // "This is from Hogeable Extension"

さて、ここでHogeableのエクステンションであったhogeメソッドを、
プロトコル本体に宣言します。

protocol Hogeable {
    func hoge() -> String
}
extension Hogeable {
    func hoge() -> String {
        return "This is from Hogeable Extension"
    }
}

class Animal : Hogeable {
    func hoge() -> String {
        return "This is from Animal"
    }
}

class Cat : Animal {
    override func hoge() -> String {
        return "This is from Cat"
    }
}

するとあら不思議、
静的ディスパッチだったケースが動的ディスパッチに変わりました。

let cat = Cat()
cat.hoge()               // "This is from Cat"
(cat as Hogeable).hoge() // "This is from Cat"

これはちょっと不思議ですね。
メソッドレシーバがプロトコル型なら静的解決かとおもいきや、
プロトコル本体にメソッドがある場合は、
動的解決になるようです。

さて、ここで、
CatがHogeableをAnimal経由で実装するという構造は変えないまま、
Animalのhogeの実装を外してみます。

protocol Hogeable {
    func hoge() -> String
}
extension Hogeable {
    func hoge() -> String {
        return "This is from Hogeable Extension"
    }
}

class Animal : Hogeable {
}

class Cat : Animal {
    func hoge() -> String {
        return "This is from Cat"
    }
}

すると、また結果が変わります。

let cat = Cat()
cat.hoge()               // "This is from Cat"
(cat as Hogeable).hoge() // "This is from Hogeable Extension"

なんとまた静的ディスパッチに戻ってしまいました。
ここまで来るとよくわかりません。
どういうことなのでしょうか。

整理してみましょう。

プロトコル本体での宣言 親クラスでの実装 メソッド呼び出し
なし なし 静的
なし あり 静的
あり なし 静的
あり あり 動的

このとおり、プロトコル本体での宣言があり、
親クラスでも実装されている場合に動的ディスパッチになる、
という事です。

解釈

これを理解するための解釈を考えてみます。
私なりの理解なので、詳しい方はツッコミをください。

まず、cat as Hogeableのようにプロトコルの型として扱った時、
その式がどのような実体になっているかがポイントです。

そしてそれがどのような実体になるかは、
プロトコルを実装した場所が問題になります。
この場合、Animalが重要ということです。

Animalでhogeを実装しているからこそ、
プロトコルの実体がディスパッチテーブルを参照する何かになるのだと思います。

そしてここに次の規則が追加されます。

プロトコルの定義の場合、
本体に定義したものがプロトコルが持っているメソッドで、
エクステンションにあるものは、
プロトコルが持っているメソッドではないのです。
エクステンションだけがあるのか、本体にもあるのかが、重大な意味の違いを持ちます。
そのプロトコルがメソッドを持っていない場合に、
そのプロトコルに対して呼び出す事ができるメソッドを提供する、
という1段遅れた解決をするものなのです。

そう考えると、それぞれのケースは次のように理解できます。

プロトコル本体での宣言なしの場合は、
親クラスでの実装が有りでも無しでも、
プロトコル実体としてはメソッドを持っていないので、
インスタンスが持っているメソッドは呼ばれようがありません。
そして、プロトコルエクステンションが解決されるので、
エクステンションの実装が呼び出される事になります。

プロトコル本体での宣言があって、
親クラスでの実装がある場合はシンプルです。
プロトコルの本体定義としてはメソッドがあります。
そして、プロトコルの実体はAnimalですから、
Animalでメソッドを提供したので、
プロトコルの実体として動的ディスパッチなメソッドを持つことになります。

プロトコル本体での宣言があって、
親クラスでの実装がない場合がややこしいのです。
プロトコルの本体定義としてはメソッドがあります。
しかし、Animalではそれを実装していません。
だから結局のところ、Animalの段では、
hogeを持っていない実体になっているのです。

しかしコンパイルが通ります。
これはプロトコルエクステンションがあるからです。
Animalで、プロトコル本体に宣言されたメソッドを実装しない事を選択した時点で、
この実体はhogeメソッドを持っておらず、
しかしextensionで解決する、という状態になっています。
良くも悪くもプロトコルエクステンションがそういうものなのです。

Catでhogeを実装しようが、
これはAnimalが決めたHogeableの実体には影響を与えないため、
静的呼び出しになった、という事です。

実戦

さて、実際にこういうケースで困らないために、
何を意識していればよいでしょうか。

実践的に、プロトコルやクラスを別々の人が実装するケースで考えます。

まず、プロトコルを定義する人はその時点で、
それを本体にメソッドを書くかどうかが、
後にプロトコル型の式が生えた際に
ディスパッチに影響を与えるという事を意識しましょう。
そして動的なディスパッチがしたいのであれば、
本体に定義しておきます。

さてAnimalは誰かが定義したとします。

次にCatをあなたが実装する時ですが、
幸いな事に、親クラスでメソッドが定義されている場合、
func hoge()を定義すると、overrideをつけなさい、
とコンパイラに言われるわけです。
という事は親クラスでメソッドが生えているわけですから、
overrideを指定する時点で、
動的ディスパッチされる事がわかります。

逆に、overrideをつけずにコンパイルが通った場合、
そこで警戒しましょう。

一方、Animalを実装する人は、
プロトコルを実装する時に、
エクステンションに頼って自身で実装しない場合、
後にディスパッチに影響を与える事を理解する必要があります。

さて・・・、あなたが実装するクラスが、
overrideを付けても安心できないパターンがあります。

protocol Hogeable {
    func hoge() -> String
}

extension Hogeable {
    func hoge() -> String {
        return "This is from Hogeable Extension"
    }
}

class Animal : Hogeable {
}

class Cat : Animal {
    func hoge() -> String {
        return "This is from Cat"
    }
}

class MikeCat : Cat {
    override func hoge() -> String {
        return "This is from MikeCat"
    }
}

このようにCatを継承したさらなるサブクラスMikeCatを実装する事があったとします。
この場合、Catでhogeが実装されているので、
MikeCatを実装する人目線では、
MikeCatの実装時はoverrideが求められ、
これで動的ディスパッチされそうに思えます。
しかし、この場合は静的ディスパッチになってしまいます。
hogeがAnimalに実装されていないからです。

このケースでは、MikeCatの実装者にできることはもうありません。
動作がおかしくなったのを見てから、
親クラス達の実装を確認するしか無いです。

ただ、このケースの場合は、
Catの実装者がoverideが要らなかった時点で、
異変を感じてほしいですね。

そして一方Catの実装者にとっては、
Animalの実装者の判断が一番重要なのかなと思います。

現実性

とここまで書いたのですが、
ぶっちゃけ上記の考え方を、
各モジュールの実装者が理解していないと、
静的ディスパッチになってしまうこれ、
現実的にトラブル回避は不可能だと思います。

どうしたらいいんでしょうね・・・

138
115
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
138
115

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?