Skip to content

Instantly share code, notes, and snippets.

@omochi
Created August 26, 2017 11:44
Show Gist options
  • Save omochi/ac7bfec6f0b3f92b74502bbfaf0563a7 to your computer and use it in GitHub Desktop.
Save omochi/ac7bfec6f0b3f92b74502bbfaf0563a7 to your computer and use it in GitHub Desktop.
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
===
*/
@omochi
Copy link
Author

omochi commented Aug 26, 2017

この 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. swift5までとりあえず放置する
  2. NSNumberをfinalにする
  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の実装例は下記で、返り値型推論を活用している。

func castCFNumber<T>() -> T {
  return unsafeBitCast(cfnumber, to: T.self)
}

slavaはこれを試してみるとこのこと。

I'll try the generics hack.


これでとりあえず動くようになるけど、
NSDecimalNumber の decimal プロパティが初期化されない可能性は解決しないのでは・・・???

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment