Rust 2018での非レキシカルライフタイム(NLL)の安定化以前、コンパイラーは借用のために厳格なレキシカルスコープを施行しており、vec.push(vec.len())のような式は不正でした。なぜなら、pushに必要な可変借用がlenによる不変借用と競合しているように見えたからです。コミュニティは、この制限が過度に保守的であると認識しました。というのも、可変アクセスはメソッド本体が実行されるまで実際には使用されないため、不変検査が安全である理論的なウィンドウが存在するからです。これにより、可変借用の予約とその実際のアクティベーションを区別する、借用チェッカーの洗練である2フェーズ借用が導入されました。
根本的な課題は、RustのエイリアスXORミューテーション保証とエルゴノミックAPI設計を調和させることにあります。特に、メソッド呼び出しが&mut selfを要求するが、その引数が同じオブジェクトの&selfを必要とする場合、問題が発生します。特別な処理がなければ、借用チェッカーはこれを2番目の可変借用ルールの違反としてフラグ付けし、開発者は一時変数を使用して手動で操作を系列化する必要があります。この問題は、実際の変化のポイントまで可変排他性の施行を遅らせるメカニズムを必要とし、同時に中間の不変アクセスが過渡的状態を生き延びたり、ダングリング参照を作成したりできないことを保証する必要があります。
2フェーズ借用は、メソッド呼び出し内の可変借用を引数の評価中の「予約」として扱うことによって機能し、評価が完了し制御がメソッド本体に入ると完全な可変借用に「アクティベート」されます。予約フェーズ中、コンパイラーは(受信者からのautorefに由来する)限定的な不変借用を許可し、可変アクティベーションが保留中であることを追跡します。これは**MIR(中間表現)**の借用チェッキング内で実装されており、コンパイラーは予約ポイントとアクティベーションポイントの間に矛盾する使用が存在しないことを検証し、安全性を静的分析によって確保します。
パケットを送信前に集約するネットワークバッファマネージャを考えてみてください。システムは、現在のバッファの長さに依存するヘッダーを追加する必要があります:buffer.append_header(buffer.current_len())。ここで、append_headerはバッファを拡張するために可変アクセスを必要としますが、current_lenは不変の検査のみを必要とします。
開発者は、変化の前に長さを別のバインディングに抽出することができます:let len = buffer.current_len(); buffer.append_header(len);。このアプローチはすべてのRustエディションで機能し、複雑な借用チェッカーの規則を完全に回避します。ただし、冗長性が生じ、コードが競合性を含むように改変されると長さが理論上古くなる可能性がありますが、単一スレッドのコンテキストではこれは純粋にスタイルの懸念です。主な欠点は、エルゴノミクスが低下することと、一時変数がその必要性を超えて生き延びる可能性があることです。これがスコープを混雑させます。
バッファをRefCellでラップすることで、ランタイムでborrow()およびborrow_mut()メソッドを通して不変および可変の借用を可能にします。これは、ランタイムにチェックを遅延させることによってコンパイル時の競合を排除しますが、違反が発生するとパニックを引き起こす可能性があります。柔軟性がありますが、参照カウントやランタイム検証のオーバーヘッドを導入し、高スループットネットワークコードにとって重要なゼロコスト抽象の原則に違反します。さらに、カスタムデストラクタを持つタイプに制限を移すことにより、コンパイル時の保証から潜在的なランタイムの失敗にシフトし、信頼性を低下させます。
チームはappend_headerを&mut selfを受け取るメソッドとして構成し、NLLの借用チェッカーが自動的に予約を処理することを信頼して、2フェーズ借用を利用しました。これにより、一時変数やランタイムのオーバーヘッドなしで論理の自然な表現が可能になりました。コンパイラーはcurrent_lenが可変借用がアクティブになる前に完了することを確認し、安全性を保証しました。この解決策が選ばれた理由は、ゼロコスト抽象を維持しつつ、意図したデータフローを正確に反映するクリーンで保守可能な構文を提供したからです。
実装は**Rust 1.63+**でエラーなしにコンパイルされ、手動系列化コードと同じ最適なパフォーマンスを達成しました。バッファマネージャは、割り当てのオーバーヘッドなしに10Gbpsのトラフィックを成功裏に処理し、2フェーズ借用がエルゴノミクスの問題を解決し、Rustの安全性保証を損なうことなく機能することを示しました。コードベースは内部可変性の複雑さから解放され、メモリ安全性の将来の監査を簡素化しました。
2フェーズ借用は明示的な逆参照操作や演算子のオーバーロードとどのように相互作用しますか?
多くの候補者は、2フェーズ借用がすべての可変参照に普遍的に適用されると考えていますが、これはメソッド呼び出しの受信者に特有のautorefの状況に限定されています。*vecや演算子トレイトのIndexMutを通じて明示的に逆参照すると、借用チェッカーは2フェーズのロジックを適用せず、可変借用を直ちにアクティブにします。この制限は、メソッドのautorefが明確な予約ポイント(メソッド呼び出しサイト)を提供し、コンパイラーが状態遷移を追跡できるのに対し、任意の逆参照操作はこの意味的境界を欠くためです。この違いを理解することは、似たようなコードがコンパイルに失敗することによる混乱を防ぐのに役立ちます。
なぜコンパイラーは受信者がDropを実装する場合に2フェーズ借用を禁止するのですか?
候補者はしばしば、Dropを実装するタイプが予約フェーズを複雑にするデストラクター意味を持っていることを見落としています。デストラクタが実行される際(例えば、パニックや複雑な制御フローによって)可変予約が存在する場合、部分的に初期化された状態がDropの有効な自己に対する期待を侵害する可能性があります。したがって、コンパイラーは、カスタムデストラクタを持つタイプに対して2フェーズ借用を制限します。これにより、可変借用のアクティベーションが破棄の実行を妨げることができないことが保証されます。これにより、スタックの巻き戻し中に部分的に移動されたり無効化された状態を観察する予約フェーズにおける微妙なバグを防ぎます。
許可された操作に関して、"予約"フェーズと"アクティベーション"フェーズを区別するものは何ですか?
予約フェーズ中、コンパイラーはメソッド呼び出しのautorefから派生した受信者の不変使用のみを許可し、特に引数の評価を許可します。しかし、候補者はしばしば、引数の評価中に受信者への追加の名前付き参照を作成したり、他の関数に渡したりすることは禁止されていることを見落とします。アクティベーションフェーズは、制御がメソッド本体に入る瞬間に始まり、この時点で引数の評価からのすべての不変借用が終了している必要があります。これにより、厳格な線形タイムラインが作成されます:予約 → 不変引数の評価 → アクティベーション → メソッド実行。参照をアクティベーションポイントより長く生き延びさせるなど、このシーケンスを違反すると、エクスクルーシビティの保証を保持するためにコンパイル時エラーが発生します。