Swift の関数の override で引数のデフォルト値を変更すると何が起こるか
Xcode engineer の人がこんなツイートをしていた
Overriding functions with default arguments is fun in Swift! Guess what this prints. pic.twitter.com/CBtZ6am6R9
— Louis D'hauwe (@LouisDhauwe) 2021年7月13日
早速 Playground で確認
まずクラスとメソッドを定義*1
import Foundation class Base { func test(i: Int = 1) { print("Base class i: \(i)") } } class Sub: Base { override func test(i: Int = 2) { print("Sub class i: \(i)") } }
通常のインスタンス化と呼び出し
let base = Base() base.test() // Base class i: 1 let sub = Sub() sub.test() // Sub class i: 2
ここまでは普通
問題は次
サブクラスでインスタンス化しつつ親クラスを型として宣言
そしてメソッドを呼ぶと...
let a: Base = Sub() a.test() // Sub class i: 1
インスタンスはサブクラスの Sub
でメソッドもサブクラスの Sub.test()
が呼ばれるけど、デフォルト引数は親クラス Base
で指定されている値が使われる!
以下を試してみる
let b = Sub() b.test() // Sub class i: 2 let c: Base = b c.test() // Sub class i: 1 print(b === c) // true
同一インスタンスの参照であるため ===
で比較すると true
になる。
つまり実体は1つ。
でも宣言型によってメソッドを呼んだ時に適用されるデフォルト引数が変化している。
個人的には直感に反した挙動でいつか落とし穴になってしまいそう。*2
ちなみに Kotlin だと親クラスでデフォルト引数が指定されていようがいまいが override された関数ではデフォルト引数を指定できない仕様になっているらしく、不意の事故が起こる心配が無い。
This is a really curious effect!
— rolgalan (@rolgalan_) 2021年7月13日
I was intrigued about what would happen in #Kotlin and it turns out that this is solved by disallowing default parameters in any overridden function, even if the base doesn't have any default param. https://t.co/aeYBhYqTmB pic.twitter.com/2y2RcC1Tor
ドキュメントでの記述が見当たらなかったので Swift Forum を当たってみたところ、2018年3月に話題に上がっていた模様。*3
https://forums.swift.org/t/pitch-allow-default-parameter-overrides/10673
*1:シンプルにするため super.test() は省いた
*2:ドキュメントを探したけどこの挙動の仕様について明記されている場所は見つからず
https://docs.swift.org/swift-book/LanguageGuide/Functions.html#ID169
https://docs.swift.org/swift-book/LanguageGuide/Inheritance.html#ID196
*3:内容は確認できていないけど C++ と同じ挙動だそう