Created
August 26, 2017 11:44
-
-
Save omochi/ac7bfec6f0b3f92b74502bbfaf0563a7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
protocol Factory {} | |
extension Factory { | |
init(factory: () -> Self) { | |
self = factory() | |
} | |
} | |
class Animal : Factory { | |
let a: Int | |
init() { | |
print("Animal init") | |
self.a = 3 | |
} | |
convenience init(init0: Int) { | |
print("Animal convenience init0") | |
self.init() | |
} | |
convenience init(init1: Int) { | |
print("Animal convenience init1") | |
self.init(factory: { Animal() }) | |
} | |
} | |
class Cat : Animal { | |
let b: Int | |
override init() { | |
print("Cat init") | |
self.b = 4 | |
super.init() | |
} | |
} | |
print("===") | |
let cat0 = Cat(init0: 0) | |
print(cat0.b) | |
print("===") | |
let cat1 = Cat(init1: 1) | |
print(cat1.b) | |
print("===") | |
/* output | |
=== | |
Animal convenience init0 | |
Cat init | |
Animal init | |
4 | |
=== | |
Animal convenience init1 | |
Animal init | |
65 | |
=== | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
この gist は以下のPRの読解。
apple/swift#11635
しばらく潜って考えて整理してみた。
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()が呼び出されることがわかる。
--
しかし、 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 として扱わねばならない。
このような「バグでコンパイルできてしまう」パターンは、
既存の 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
なクロージャを渡している。slava の修正によって、 このような場合でも動的な Self として型検査されるようになった。
そうすると、 gist の例の場合では、
init(factory:)
に渡しているクロージャの式が、() -> Animal
であるゆえに、コンパイルエラーとなる。
ここは、 self の式の型は Animal ではなく Self なので、
() -> Self
型のクロージャを渡さねばならないからだ。同様のコンパイルエラーを、 NSNumber が実際に踏み抜いた。
これを回避するために、 slava は3つの選択肢をだしている。
1は無いとして、2については、
もしこの問題になっているクラス (AnimalやNSNumber) が final class であれば、
convenience init をサブクラスから呼ぶ事自体が無いので、
解決するというもの。(修正した型チェッカはそれも考慮している)
実際には、この選択肢は取れない。
NSDecimalNumber のような、 NSNumber のサブクラスがすでに存在しているから。
https://github.com/apple/swift-corelibs-foundation/blob/856b8bcd9cae659f1ca48545b2ddb7f63da77171/Foundation/NSDecimalNumber.swift
しかもこいつは下記フィールドを保持している。
fileprivate let decimal: Decimal
ということは、現状でも convenience init 経由の NSDecimalNumber のコンストラクトで、
メモリがぶっ壊れる可能性がある・・・?
3の選択肢は、 現状の unsafeBitCast にジェネリクスを組み合わせて、
NSNumber固定ではなく、 Self へのキャストにすることで動かす、というもの。
slavaの実装例は下記で、返り値型推論を活用している。
slavaはこれを試してみるとこのこと。
これでとりあえず動くようになるけど、
NSDecimalNumber の decimal プロパティが初期化されない可能性は解決しないのでは・・・???