Xcode
Swift

【バグ】NSObject を継承したオブジェクトを unowned プロパティーとして保持すると、それを保持しているインスタンスを print すると落ちる

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 つの条件が必要です:

  1. プロパティーの型は NSObject を継承する必要があります(つまりそうではない通常の class は問題ありません)
  2. プロパティーを unowned として保持する必要があります(つまり通常の強参照と weak による弱参照の保持も問題ありません)

上記 2 つの条件を満足した場合、そのプロパティーを持つインスタンスを print しようとすると落ちます。

また、Playground で動かす場合は、そもそも作られたものを全てとりあえず print しようとしているので、作られた時点で落ちます。ツラい。

以上、改めまして、この現象の原因を究明するのにお世話になりました @kishikawakatsumi さんと @rintaro さんに、大変ありがとうございます。