Protocol に定義するかどうかで振る舞いが変わるケース
Sponsor Link
環境&対象
- 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 で定義されていないケース
以下のようなコードです。
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"
- protocol には、func1 は定義されていません
- sConcrete に定義した func1 が呼ばれます
- General の extension で定義された func1 が呼ばれます。
protocol で定義されているケース
以下のようなコードです。
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"
- protocol に、func1 が定義されています
- sConcrete に定義した func1 が呼ばれます
- 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 での定義の有無により同名のメソッド等の振る舞いが変わるケースがあることを意識する
- 「プロトコルの性質を表す」ということであっても、protocol extension でデフォルトの定義を提供すると具体的な型での実装を省略できる
- 必要に応じて、具体的な型で、プロトコルで定義された性質を上書きすることができる
- 上書きするときに、override のようなキーワード付与を強制することはできない
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link