Swiftはバージョン2.0で構造化されたエラーハンドリングを導入し、Objective-Cのエラーポインターパターンをネイティブのthrowおよびcatchセマンティクスに置き換えました。rethrowsキーワードは、呼び出し側がリトライを強制される場合に、非スロークロージャを渡すときに発生する特定の摩擦を解決するために登場しました。このため、不要なエラーハンドリングの儀式が生じました。
この問題は、関数効果の多態性とサブタイピングに関連しています。Swiftの型システムにおいて、非スロークロージャはスロークロージャのサブタイプです。なぜなら、決してスローしないことで「スローする可能性がある」という契約を満たしているからです。rethrowsがなければ、スロークロージャを受け入れる関数は無条件にスローを伝播しなければならず、すべての呼び出しサイトが引数の実際の動作に関わらずエラーを処理しなければなりません。
この解決策は、rethrows注釈であり、条件付き契約を確立します:関数は、クロージャの引数がスローするときのみスローします。Swiftコンパイラは、コンパイル時にクロージャ引数のスロー性を追跡することによってこれを実装します。非スローのクロージャが渡されると、関数は呼び出しサイトで非スローとして扱われ、tryの必要がなくなります;スロークロージャが渡されると、関数はスロー効果を継承します。
我々は、ユーザーがJSONパース、画像リサイズ、暗号化ハッシュ作成のような操作を連鎖させることができるiOSアプリケーションのためのモジュラーデータ変換パイプラインを構築していました。コアのpipeline関数は、(Data) throws -> Dataとして定義された変換の配列を受け入れていました。最初は、pipelineに標準のthrows注釈を使用していましたが、これにより、単純な変換でさえもすべての呼び出しサイトがdo-catchブロックにラップしなければならなくなり、多くの操作が失敗モードのない純粋関数であったにもかかわらず、無駄の多いエラーハンドリングが生じました。
私たちの最初のアプローチは、すべての関数を複製することを必要としました:非スロー変換用のpipelineと、スロー変換用のpipelineThrowingという2つのバージョンです。この分離により、きれいな呼び出しサイトが可能になりましたが、すべてのバグ修正には2箇所を編集する必要が生じ、それによってAPIサーフェスが新しい構成オプションごとに倍増しました。さらに、ユーザーは正しいメソッドを選択するために実装の詳細を知る必要があり、カプセル化の原則に違反しました。
2番目のアプローチは、単一のthrowsシグネチャを保持しましたが、警告を抑えるためにtry?の使用を奨励しました。これにより、エラー情報が効果的に捨てられ、実際のエラーが発生した際のデバッグが不可能になりました。これは安全性の保証を侵害し、コードを脆弱にし、開発者が安全でない操作を含む混合パイプライン内の真のエラーケースを処理するのを忘れがちにしました。
最終的に、rethrows解決策を採用し、func pipeline(_ transforms: [(Data) throws -> Data]) rethrows -> Dataと宣言しました。これにより、コンパイラは、クロージャ配列にスロー操作が含まれている場合にのみtryを強制し、純粋な計算のための直接呼び出しを許可しました。その結果、ボイラープレートコードが40%削減され、重複する関数シグネチャが排除され、型システムが特定の使用ケースの実際のエラー範囲を正確に反映するように、APIの使いやすさが向上しました。
なぜSwiftはrethrows関数本体の中で直接エラーをスローすることを禁じているのか、それともクロージャの引数を介してのみか?
rethrowsキーワードは、関数が引数によって生成されたエラーのみを伝播することを明確にする厳格な透明性契約を作成します。関数本体内でthrow CustomError()を試みると、Swiftコンパイラはこれを拒否します。これは無条件のスローを表すため、「クロージャがスローする場合のみ」という保証に違反するからです。関数は、そのエラーを内部で処理するか、戻り値に変換するか、無条件のthrowsに契約を引き上げる必要があり、呼び出し側は関数自体から新しいエラー領域が発生しないと安全に仮定できるようにします。
rethrowsは複数のクロージャ引数とどのように相互作用し、効果伝播にどのような影響を与えますか?
関数にスローとしてマークされた複数のクロージャ引数がある場合、その関数自体がrethrowsとしてマークされていると、関数は任意のクロージャがスローする場合にスローし、効果の和を作成します。Swiftのコンパイラは、呼び出しチェーンを通じてこれらの効果を個別に追跡するため、rethrows関数を合成しても手動で介入せずに条件付き特性が保持されます。ただし、引数を渡す前にクロージャを変換またはラップする場合、ラッパー内でスローシグネチャを保持しなければなりません。さもなければ、コンパイラは引数を非スローとして扱い、外部関数が条件付きスロー能力を失うことになります。
rethrowsと@autoclosureの関係は何であり、なぜこのパターンはアサーションAPIに現れますか?
@autoclosureとrethrowsの組み合わせは、条件付きエラー伝播を伴う遅延評価を可能にします。ここでは、オートクロージャは必要になるまで評価を遅らせ、関数はその遅延評価がスローするときのみスローします。このパターンは、Swiftのassertおよびprecondition関数を駆動し、スローする式をアサーションに渡すことを可能にしますが、アサーション呼び出しをtryでマークする必要はありません。候補者はしばしば、オートクロージャが() throws -> Tとして明示的に宣言される必要があることを見逃します。このメカニズムは、評価のタイミング(遅延)とエラー伝播セマンティクス(条件付き)を分離します。これは、リリースビルドでアサーションが無効化されているパフォーマンスクリティカルなコードパスにとって重要です。