TL;DR
下記のようなコードは落ちます:
import Foundation
class C: NSObject {
}
struct D {
unowned let x: C
}
let c = C()
let d = D(x: c) // Playground ではここで落ちる
print(d) // シミュレーターや実機ではここで落ちる
どういうことか
自作ライブラリー NotAutoLayout をメンテナンスして、とある修正を対応している時に、新しくできたものがどうしても Playground だけで落ちて、まったく同じコードでも Test でも普通に動くしシミュレーターで確認しても何の問題もありませんでした。
ちなみに実際それを再現するブランチをこちらから落とせます:
https://github.com/el-hoshino/NotAutoLayout/tree/Playground-Bug
上記のものを落として、Workspace を開いてシミュレーターを Target にビルドして Playground 開くと、確実に parent.nal.layout(child) { $0
のクロージャーの終了場所で EXC_BAD_ACCESS
で落ちるのです。しかし、Test にまったく同じコードがありますが、そちらは何の問題もなくテストが通ります。Playground は当然ながら lldb も使えないし Call Stack も表示されないので、落ちたとしても具体的になぜ落ちたかもわかりません。
この問題を Discord に投げてみたら、@kishikawakatsumi さんと @rintaro さんのおかげで、原因がようやく突き止められて、最終的には上の TL;DR で出したコードとなります。
具体的に言いますと、NSObject
を継承したオブジェクトを class や struct の unowned
プロパティーとして保持した際に、どうやら swift_ClassMirror_subscript()
のバグで、これを強参照として読み取ろうとしているのが原因だそうです。現在このバグはこちらにも上がっています:
https://bugs.swift.org/browse/SR-5289
このバグを再現するには 2 つの条件が必要です:
- プロパティーの型は
NSObject
を継承する必要があります(つまりそうではない通常のclass
は問題ありません) - プロパティーを
unowned
として保持する必要があります(つまり通常の強参照とweak
による弱参照の保持も問題ありません)
上記 2 つの条件を満足した場合、そのプロパティーを持つインスタンスを print
しようとすると落ちます。
また、Playground で動かす場合は、そもそも作られたものを全てとりあえず print
しようとしているので、作られた時点で落ちます。ツラい。
以上、改めまして、この現象の原因を究明するのにお世話になりました @kishikawakatsumi さんと @rintaro さんに、大変ありがとうございます。