合成されたCodableの実装は、コンパイル時に利用可能な静的型情報にのみ依存します。基底クラス参照を介して異種コレクションのクラスインスタンスをエンコードする際、コンパイラは基底クラス型に対して可視なプロパティのみをシリアライズするencode(to:)コードを生成します。その結果、サブクラス固有のプロパティはJSON出力から省略され、デコード時には正しいサブクラスをインスタンス化するために必要なメタデータがランタイムに存在せず、基底クラスにデフォルト化されて型特有のデータが失われます。
私たちは、ポートフォリオ管理のためにさまざまな取引タイプを処理する金融分析ダッシュボードを構築していました。ドメインモデルは、基底クラスであるTransactionを使用し、StockTrade、DividendPayment、FeeChargeのようなサブクラスがtickerSymbolやdividendRateなどの特定のプロパティを追加するクラス階層になっていました。バックエンドAPIは、各取引にtransactionTypeの識別子フィールドを含む、これらの取引の混合JSON配列を返しました。
最初は、Swiftの自動Codable合成に依存し、多態的配列**[Transaction]を処理できると仮定していました。しかし、統合テスト中に[StockTrade]配列を[Transaction]にキャストしてエンコードすると、JSONにはidやamountのような基底クラスのフィールドしか含まれず、tickerSymbolが完全に省略されていることが判明しました。逆に、このJSONをデコードすると、基底のTransaction**インスタンスのみが再作成され、期待されるサブクラス固有のプロパティにアクセスしようとするとアプリがクラッシュしました。
この制限を解決するために、私たちは3つの異なるアプローチを検討しました。最初のアプローチは手動のCodable実装で、エンコーディングコンテナにtransactionTypeフィールドを明示的に追加し、この識別子に基づいて正しいサブクラスをインスタンス化するカスタム**init(from:)**を実装しました。このアプローチは完全な型安全性を提供し、既存のオブジェクトグラフを保持しましたが、新しい取引タイプごとに大規模なボイラープレートコードを記述および維持する必要があり、機能を追加する際に開発者エラーのリスクが増加しました。
2つ目の解決策は、型消去されたAnyCodableラッパーや存在論的型(any TransactionProtocol)を使用するプロトコル指向のアプローチを探索するものでした。これにより、継承なしで異種型を配列に格納できましたが、コンパイル時の型安全性が失われ、存在論的ボクシングや動的ディスパッチによるランタイムのオーバーヘッドが導入されました。また、消費者が型消去のアーティファクトやキャストを処理する必要があるため、API契約が複雑になり、コードの明確性が低下しました。
3つ目のオプションは、関連値を持つ単一エヌムにクラス階層をリファクタリングすることでした。例えば、enum Transaction { case stock(StockData), case dividend(DividendData) }のような形です。エヌムは、合成されたCodableを通じて多態的シリアル化を自然にサポートします。コンパイラは自動的に識別子フィールドを生成します。しかし、これには既存のCore Dataモデルやアプリケーション全体のビジネスロジックの大規模なリファクタリングが必要で、運用システムに対する許容できない回帰リスクを伴います。
私たちは、識別子フィールドを持つ手動のCodable実装という最初の解決策を選択しました。これにより、シリアル化層に局所的な変更を加えつつ、既存のアーキテクチャやデータベーススキーマを妨げることなく、適切なサブクラス初期化子に基づいて文字列値に応じてデコードされたタイプ識別子を最初にデコードします。
その結果、多態的APIレスポンスを完全な型の忠実性で正しく処理できる堅牢なシリアル化パイプラインが実現しました。手動の解析コードは約200行を必要としましたが、既存の機能との後方互換性を維持し、新しい取引タイプを追加する際にデコードロジックを忘れた場合には明確なコンパイル時エラーを提供し、ランタイムエラーを防ぎました。
なぜ、JSONEncoderでエンコードする前に[Subclass]を[BaseClass]にキャストすると、サブクラス固有のプロパティのデータが失われるのですか?
合成されたencode(to:)メソッドは、コレクション内の値のコンパイル時の型に基づいて静的にディスパッチされます。[BaseClass]にキャストすると、コンパイラはBaseClassの合成実装を選択し、基底クラスで宣言されたプロパティのみを反復します。サブクラスプロパティは、この実装に対しては見えないため、静的なディスパッチメカニズムは合成メソッドの動的型のメタデータを参照しません。すべてのプロパティを保持するためには、具体的な型を使用してエンコードするか、識別子フィールドを通じて手動で動的型の解決を実装する必要があります。
必須初期化子の要件は、クラス階層におけるDecodableの適合性とどのように相互作用し、なぜ自動的なサブクラスのインスタンス化を妨げるのですか?
Decodableはinit(from: Decoder)初期化子を要求します。クラスの場合、これを基底クラスでrequiredとしてマークする必要があり、サブクラスが適合性を継承できるようになります。しかし、基底クラスの合成実装は、識別子フィールドのような外部データに基づいて動的にどのサブクラスをインスタンス化するかを決定できません。デコーダーがサブクラスを示すデータに遭遇すると、基底クラスの**init(from:)を呼び出しますが、基底クラス部分を初期化する方法しか知らないのです。多態的デコードをサポートするためには、開発者がすべてのサブクラスでinit(from:)**をオーバーライドし、インスタンス化前にデコーダーのコンテナを調査して具体的な型を決定するファクトリメソッドを実装する必要があります。
Swiftの合成されたCodableが関連値を持つエヌムとクラス継承をどのように扱うかの根本的な違いは何であり、なぜこれがエヌムを多態的シリアル化に適したものにするのですか?
Swiftは、関連値を持つエヌムのためにCodableを合成する際に識別子キーを生成します。エンコーディングにはケース名が文字列キーとして含まれ、デコーディング実装はこのキーに基づいて正しいケースとその関連ペイロードを再構築します。これは、エヌムがコンパイル時に完全に知られているクローズドで封じられた型階層を形成するためです。これにより、コンパイラは完全なスイッチ文を生成できます。対照的に、クラスは異なるモジュールに新しいサブクラスを追加できるオープンな階層を形成します。基底クラスのCodable適合性を合成する際に、コンパイラはすべての可能なサブクラスに対して網羅的なスイッチを生成できないため、手動の介入なしに多態性を自動的に処理することは不可能です。