Swift 5.0で導入された**@frozen**属性は、APIの拡張性とバイナリの安定性との間の緊張を解消するために設計されました。このメカニズムが導入される前は、復元性ライブラリ内のすべての公開列挙型は暗黙的に凍結されていないと見なされ、コンパイラは将来のバージョンで未知のケースが追加される可能性を考慮する必要がありました。この仮定によって、固定サイズのコンパクトなレイアウトの生成が妨げられ、クライアントコードにおいて防御的なプログラミングパターンが強制されました。この属性は、列挙型のケースの在庫が永遠に不変であることを正式に保証し、積極的な最適化を可能にします。
ライブラリがこの属性なしで列挙型を公開した場合に問題が生じます。Swiftはその列挙型を復元性として扱わざるを得ず、将来のケース識別子や関連付けられた値のレイアウトを収容するためにメモリ表現において変数空間を予約します。これにより、クライアントがスイッチ文に@unknown defaultケースを含めることを強制され、すべての論理状態が処理されていることのコンパイル時検証が無効化されます。このようなデフォルトなしでライブラリにケースを追加すると、新しい識別子値を処理するコードを持たない事前コンパイル済みクライアントバイナリで未定義の動作を引き起こし、クラッシュやメモリの破損につながります。
解決策は、@frozen注釈によって恒久的な契約が確立されることです。列挙型を凍結としてマークすることで、ライブラリの著者はケースのセットが決して変更されないことを約束し、コンパイラは固定整数タグを割り当てて安定したコンパクトなメモリレイアウトを使用できるようになります。これにより、デフォルトケースなしでの網羅的スイッチ文が可能になり、コンパイラは識別子のすべての可能なビットパターンが既知のケースに対応していることを証明できます。これにより得られるABI安定性は、列挙型のサイズとアライメントがライブラリのバージョンを跨いで一定であることを保証し、クライアントコードはジャンプテーブル最適化とすべての状態の必須処理の恩恵を受けることができます。
// -enable-library-evolutionでコンパイルされたライブラリ内 @frozen public enum LoadState { case idle case loading case loaded(Data) } // 別のモジュール内のクライアントコード func updateUI(for state: LoadState) { switch state { case .idle: print("待機中") case .loading: print("スピナー") case .loaded: print("コンテンツ") // コンパイラが網羅性を確認します; デフォルトは必要ありません } }
物流会社のプラットフォームチームは、TransportMode列挙型を公開するSwiftパッケージをルート最適化のために配布していました。この列挙型には.truck、.air、および.shipのケースがありました。彼らは後のリリースで.droneと.railを追加することを期待していたため、最初は**@frozen**属性なしでライブラリを配布しました。クライアントチームはすぐに、Xcodeが@unknown default句なしでスイッチをコンパイルすることを拒否し、運賃計算で.shipを処理し忘れたロジックエラーを隠していることを報告しました。
チームはこの問題を解決するための3つのアーキテクチャアプローチを検討しました。
まず、凍っていない状態を維持し、クライアントが警告を記録する@unknown defaultハンドラーを記述するように重いリンティングに投資することです。これにより、主要なバージョンの切り上げなしで輸送モードを追加する柔軟性が保たれますが、コンパイル時の網羅性チェックが永久に無効化されます。また、各列挙型インスタンスに復元性メタデータが含まれているため、バイナリサイズのオーバーヘッドにも対処できませんでした。
第二に、列挙型を整数定数に基づくRawRepresentable構造体に置き換えることです。これにより、固定メモリレイアウトが提供され、新しいモードを追加してもバイナリ互換性が壊れないことが可能になりますが、Swiftのパターンマッチング機能が完全に失われます。開発者は冗長なif-elseチェーンを強いられ、コンパイラは重要な経路探索アルゴリズムで処理されるすべての輸送状態が処理されていると信じることができなくなります。
第三に、列挙型に**@frozen**を適用し、既存の3つのケースにコミットし、将来の拡張のために別のExtendedTransportModeラッパーを作成することです。これにより、復元性のオーバーヘッドが排除され、網羅的なスイッチコンパイルが可能になり、すべてのクライアントがすべての現在のモードを明示的に処理することが保証されます。妥協点は、元の列挙型を変更することが永久に制限され、基本的な追加のためのバージョン管理が必要になることです。
彼らは第三の解決策を選びました。TransportModeを凍結した後、コンパイル中に自分たちの分析ダッシュボードで2つの未処理のスイッチケースをすぐに発見しました。復元性メタデータの削除により、送信されたルートオブジェクトのサイズが18%減少し、明示的なアーキテクチャ境界により、コアの輸送ロジックと実験的モードとの間のクリーンな分離が強制されました。
凍結されていない公開列挙型にケースを追加すると、クライアントソースコードが正常にコンパイルされても、なぜバイナリ互換性が壊れるのでしょうか?
Swiftが復元性モジュールをコンパイルするとき、非凍結の列挙型は将来のケース識別子のためにスペースを予約する可変幅表現を使用します。ライブラリがその後ケースを追加すると、列挙型のランタイムレイアウトが変更されます—たとえば、識別子整数が新しいタグを収容するために8ビットから16ビットに拡張される可能性があります。事前コンパイルされたクライアントバイナリは古いレイアウトを期待しており、元のタグ範囲のみを考慮したジャンプテーブルや条件分岐を含んでいます。これらのバイナリが新しい識別子値に遭遇すると、無効なコードパスを実行したり、予期されるペイロード境界を超えてメモリを読み取ったりして、クラッシュが発生し、ソースレベルの@unknown default句では防止できません。
@frozenは、間接ケースや復元性型の関連値を含む列挙型とどのように相互作用しますか?
@frozenはケースの同一性と数が一定であることを保証しますが、関連付けられた値のサイズを凍結するわけではありません。ケースが非凍結の構造体やクラス参照のペイロードを持つ場合、列挙型のABI安定性は固定の識別子タグを指しますが、ペイロードストレージはまだポインタや値ウィットネステーブルを介して動的サイズを使用する可能性があります。候補者はしばしば**@frozen**がペイロードサイズを含むメモリフットプリント全体を固定すると誤解しますが、実際には最適化は主にタグに適用され、関連付けられた値はそれ自体が復元性であったり未知のサイズを含む型の場合、ランタイムレイアウト計算を必要とする可能性があります。
凍結された列挙型は非復元モジュール内で宣言できますか?それを行うことの長期的な影響は何ですか?
はい、@frozenはライブラリエボリューションが無効な通常のアプリケーションターゲット内の列挙型に適用できます。この文脈では、すべての列挙型が実質的に凍結されているため、この属性は意図の文書として機能します。ただし、候補者は考慮しがちですが、@frozenは恒久的なABI契約を構成します。モジュールが後に復元性ライブラリフレームワークに抽出された場合、既存のクライアントとのバイナリ互換性を破壊することなく列挙型を凍結解除または拡張することはできません。初期の開発中に列挙型を凍結として明示的にマークすることで、プロジェクトのアーキテクチャが進化する際の偶発的なABI違反に対してコードベースを将来にわたって保護します。