スウィフトの初期化モデルは、Objective-Cのような言語で一般的な未定義の動作を排除するために設計されました。すべてのメモリが初期化される前にインスタンスメソッドやプロパティにアクセスすると、セグメンテーションフォールトやセキュリティの脆弱性が発生する可能性があります。根本的な問題はクラス階層にあります:サブクラスオブジェクトは自身の格納プロパティのメモリとすべての継承されたプロパティのメモリを含んでおり、コンパイラはすべてのバイトが有効になるまでこのメモリにアクセスするコードが存在しないことを保証する必要があります。これを解決するために、スウィフトは静的解析を通じて**確定初期化(DI)**の不変条件を強制し、オブジェクトがパーシャル構築の不安全な状態にとどまることを要求します。フェーズ1の2段階初期化が終了するまで、selfに安全にアクセスまたは逃避することはできません。フェーズ1中に、初期化子は現在のクラスによって導入されたすべてのプロパティに値を割り当て、スーパークラスの初期化子に委任する必要があります。このフェーズが完了した後にのみselfに安全にアクセスできます。
class Vehicle { let wheelCount: Int init(wheels: Int) { self.wheelCount = wheels // Vehicleのフェーズ1完了 } } class Bicycle: Vehicle { let hasBell: Bool init(bell: Bool) { // フェーズ1: 自身のプロパティをまず初期化する self.hasBell = bell // その後、スーパークラスに委任する super.init(wheels: 2) // フェーズ1完了: 完全な確定初期化が達成されました // フェーズ2: selfを使用するのは安全 self.checkSafety() } func checkSafety() { print("車輪数が \(wheelCount) の自転車は \(hasBell ? "ベルあり" : "ベルなし")です") } }
医療記録アプリケーションを開発しているとき、PatientRecordスーパークラスとICUPatientRecordサブクラスの複雑なシナリオに直面しました。初期化中に患者の年齢(スーパークラスのプロパティ)に基づいて重症度スコアを計算する必要がありました。最初の実装では、super.init(age:)を呼び出す前にself.ageにアクセスするヘルパーメソッドcalculateSeverity()を呼び出そうとしたため、コンパイラエラーが発生しました。なぜなら、サブクラスの初期化子が継承されたメモリの安全性を保証していなかったからです。この制約を解決するために、3つの異なるアーキテクチャアプローチを評価しました。
一つのアプローチは、重症度スコアを暗黙的にアンラップされたオプショナル(var severity: Int!)として宣言し、スーパークラスの初期化が完了した後に割り当てを遅延させることでした。この方法ではコンパイラの要件を満たしましたが、実行時のリスクが大幅に増加しました。プロパティが割り当て前にアクセスされ、クラッシュを引き起こす可能性があったため、レコードの整合性保証が損なわれ、イミュータブルのlet宣言を使用できなくなりました。
第二の戦略は、年齢を読み取るためだけに一時的なプレースホルダーオブジェクトをインスタンス化し、重症度をオフラインで計算した後、事前に計算された値で本物のインスタンスを構築する静的ファクトリーメソッドを使用することを検討しました。これによりメモリの安全性は保たれましたが、膨大なボイラープレートが追加され、初期化のフローが不明瞭になり、コードベースは他のチームメンバーにとってメンテナンスとデバッグが大幅に困難になりました。
選ばれた解決策は、初期化子を年齢をパラメーターとして受け入れるように再構築し、入力パラメーターに基づいて純粋な静的関数を使用して重症度を計算し、事前に計算された値を指定された初期化子に渡すことでした。このアプローチにより、severityをlet定数として保持することでイミュータビリティが維持され、2段階の初期化ルールを厳密に遵守し、コンパイラがビルド時に安全性を確認できるようになりました。その結果、初期化シーケンスはゼロクラッシュで、年齢と重症度のデータ依存関係を明確に表現し、スウィフトの静的解析を活用して回帰を防ぐことができました。
なぜコンパイラは、サブクラスで定義され、スーパークラスのプロパティとは無関係に見えるメソッドのselfへの呼び出しを防ぐのですか?
コンパイラはこの制限を強制します。というのも、オブジェクトは確保されたメモリとして存在しますが、スーパークラス部分は未初期化の生メモリのままになるためです。selfに対するメソッド呼び出しは、どこで定義されていても、完全なオブジェクトポインタを受け取り、間接的に未初期化のスーパークラスフィールドにアクセスする可能性があり、メモリの安全性に違反することになります。スウィフトは、フェーズ1の完了前のすべてのselfの使用を不安全と見なしており、現在のクラスの格納プロパティへの直接代入のみを許可しています。
確定初期化解析は、weak参照プロパティとunowned参照プロパティをどのように扱いますか?
確定初期化チェッカーは、weak変数を含むオプショナル型を暗黙的にOptionalとして扱い、コンパイラによって自動的にnilの有効な初期値が注入されます。そのため、weakプロパティは初期化子内で明示的に初期化する必要がありません。対照的に、unowned参照はオプショナルでなく、即時の非nilセマンティクスを仮定するため、初期化子が完了する前に値が割り当てられなければなりません。そうしないと、コンパイラは確定初期化エラーを出力します。
便利初期化子の委任ルールと指定初期化子における確定初期化の違いは何ですか?
便利初期化子は二次的なエントリポイントとして機能し、インスタンス固有の操作を行う前に指定初期化子(self.initを介して)に委任しなければなりません。彼らは格納プロパティを直接初期化することを厳しく禁止されています。これに対して、指定初期化子は、自己のクラスによって導入されたすべてのプロパティを初期化しなければならず、その後スーパークラスの初期化子に委任され、オブジェクトが階層の各レベルで有効であることを保証します。