Avatar
しばらく潜って考えて整理してみた。 slavaが指摘しているバグ この gist と合わせて解説 https://gist.github.com/omochi/ac7bfec6f0b3f92b74502bbfaf0563a7 あるクラスがが convenience init から、別の init メソッド を呼んでいるときは、 静的な Selfではなく、 convenience init 実行中の動的な Self とともに呼ばねばならない。 例えば、Animal.init(init0:)はそのようなパターンで、Cat.init(init0:)を呼び出すと、 Animalのconvenience initから、オーバライドされたCat.init()が呼び出されることがわかる。 (edited)
12:06 PM
しかし、 protocol の extension メソッドで定義された init を呼び出している場合には、 動的な Self ではなく、静的な Self に対して呼び出してしまうバグがあった。 これを検証しているのが Animal.init(init1:) である。 この中から呼ばれる self.init(facotry:) は、 Self が Animal に固定されて型検査されているし、 コンパイル後もそのように動作する。 その結果として、 Cat.init(init1:) の呼び出しは、 Cat の初期化をなんら呼び出していない。 実際に cat1.b を表示してみると、謎の値65が表示された。 (at IBM Swift sandbox) 本来は、 protocol から来ていたとしても、 convenience init から init を呼び出すところは、 動的な Self として扱わねばならない。
12:07 PM
このような「バグでコンパイルできてしまう」パターンは、 既存の NSNumber 実装が踏み抜いている。 https://github.com/apple/swift-corelibs-foundation/blob/87466c45462cba5a085d5cd41d785d681016542d/Foundation/NSNumber.swift#L266 この行は、 NSNumber の親、 NSValue が conformance する protocol _Factory によって定義された init(factory:) を呼び出している。 ここでは unsafeBitCast で NSNumber にキャストしているので、 init(factory:) に対しては、 () -> NSNumber なクロージャを渡している。
swift-corelibs-foundation - The Foundation Project, providing core utilities, internationalization, and OS independence
12:07 PM
slava の修正によって、 このような場合でも動的な Self として型検査されるようになった。 そうすると、 gist の例の場合では、 init(factory:) に渡しているクロージャの式が、 () -> Animal であるゆえに、 コンパイルエラーとなる。 ここは、 self の式の型は Animal ではなく Self なので、 () -> Self 型のクロージャを渡さねばならないからだ。
12:07 PM
同様のコンパイルエラーを、 NSNumber が実際に踏み抜いた。 これを回避するために、 slava は3つの選択肢をだしている。 1. swift5までとりあえず放置する 2. NSNumberをfinalにする 3. ジェネリクスを使う 1は無いとして、2については、 もしこの問題になっているクラス (AnimalやNSNumber) が final class であれば、 convenience init をサブクラスから呼ぶ事自体が無いので、 解決するというもの。(修正した型チェッカはそれも考慮している)
12:08 PM
実際には、この選択肢は取れない。 NSDecimalNumber のような、 NSNumber のサブクラスがすでに存在しているから。 https://github.com/apple/swift-corelibs-foundation/blob/856b8bcd9cae659f1ca48545b2ddb7f63da77171/Foundation/NSDecimalNumber.swift しかもこいつは下記フィールドを保持している。 fileprivate let decimal: Decimal ということは、現状でも convenience init 経由の NSDecimalNumber のコンストラクトで、 メモリがぶっ壊れる可能性がある・・・?
swift-corelibs-foundation - The Foundation Project, providing core utilities, internationalization, and OS independence
12:08 PM
3の選択肢は、 現状の unsafeBitCast にジェネリクスを組み合わせて、 NSNumber固定ではなく、 Self へのキャストにすることで動かす、というもの。 slavaの実装例は下記で、返り値型推論を活用している。 func castCFNumber<T>() -> T { return unsafeBitCast(cfnumber, to: T.self) } slavaはこれを試してみるとこのこと。
I'll try the generics hack.
12:08 PM
これでとりあえず動くようになるけど、 NSDecimalNumber の decimal プロパティが初期化されない可能性は解決しないのでは・・・???