`Hash#keys.each` を `Hash#each_key` に置き換えてメモリ割り当てを削減

rails/rails

Hash#keys.each が生成する中間配列を排除するため、Railsコードベース内の複数箇所で Hash#each_key に置き換えました。同様のクリーンアップはかつて #17099 でも行われており、今回はその継続作業に当たります。

背景

Hash#keys.each は全キーの中間配列を生成してからイテレートするため、不要なメモリ割り当てが発生します。一方、Hash#each_key はハッシュの内部構造を直接走査するため、この余分な割り当てが不要です。

#17099 では同様の問題が認識され、コードベース全体にわたって Hash#each_key への置き換えが実施されました。今回の #57129 は、その後に追加されたコードに残っていた keys.each を対象とした追加クリーンアップです。

技術的な変更

PRのDescriptionでは変更箇所を「activemodel/lib/active_model/schematized_json.rbactiverecord/lib/active_record/attribute_methods/dirty.rb の2箇所」と説明しています。一方、最終的なDiffはこれらに加えてテストファイル2つ(activerecord/test/cases/connection_adapters/connection_handler_test.rbactivesupport/test/env_configuration_test.rb)を含む計3ファイルの変更となっており、説明と実際の変更に差異があります。

activemodel/lib/active_model/schematized_json.rb では、has_delegated_json メソッド内でスキーマキーごとにアクセサメソッドを定義するループが変更されました。

変更前:

schema.keys.each do |schema_key|
  define_method(schema_key)       { public_send(attr).public_send(schema_key) }
  define_method("#{schema_key}?") { public_send(attr).public_send("#{schema_key}?") }
  define_method("#{schema_key}=") { |value| send(attr).public_send("#{schema_key}=", value) }
end

変更後:

schema.each_key do |schema_key|
  define_method(schema_key)       { public_send(attr).public_send(schema_key) }
  define_method("#{schema_key}?") { public_send(attr).public_send("#{schema_key}?") }
  define_method("#{schema_key}=") { |value| send(attr).public_send("#{schema_key}=", value) }
end

テストファイルでも同様の置き換えが行われています。activerecord/test/cases/connection_adapters/connection_handler_test.rb ではDB設定ハッシュのキー型検証ループが、activesupport/test/env_configuration_test.rb では環境変数の後処理ループがそれぞれ each_key に変更されました。

なお、コードベースには残り3箇所の keys.each 呼び出しが存在します(hash/keys.rbstrong_parameters.rbrouting/mapper.rb)。これらはループ内で delete を使ってハッシュを変更するため、イテレーション前にキーのスナップショットを取る keys が必要であり、今回の対象から意図的に除外されています。

設計判断

変更対象を「イテレーション中にハッシュを変更しない箇所」に限定したことが、この変更の重要な設計上の判断です。

ループ内で delete などハッシュを破壊的に操作する場合、Hash#each_key に置き換えると反復の挙動が変化し、バグを引き起こす恐れがあります。PRはこの区別を明示的に記述しており、安全に置き換えできる箇所のみを厳選しています。機械的な一括置換ではなく、各呼び出し箇所のセマンティクスを確認した上での変更である点が、このPRの信頼性を高めています。

まとめ

今回の変更は1行ずつの小さな修正ですが、Hash#each_key が存在する理由を改めて示す好例です。ループ内でハッシュを変更しないという事前条件を確認してから置き換えるという判断は、同様のパターンを自分のコードで見つけた際にも直接応用できます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
cefbe1bf

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
3回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→セクション群(背景、技術的変更、設計判断)(各論)→まとめ(結論)の3部構成が明確に適用されており、模範的な記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライトやGitHubのPR/Issueリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

メモリ割り当てやイテレータの内部挙動といったトピックを扱い、専門知識を持つエンジニアという対象読者に適合した内容です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論・各論の構成になっており、各パラグラフはトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。非常に読みやすいです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードはDiff内容と正確に一致しています。PRのDescriptionと実際のDiffの不一致(dirty.rbが含まれず、テストファイルが変更されている点)を指摘している点も評価できます。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「中間配列」「メモリ割り当て」「スナップショット」など、技術用語が正確かつ効果的に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

`keys.each`と`each_key`の挙動の違いや、イテレーション中のハッシュ変更に関する注意点など、技術的な説明は正確で論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのDescription、Diff、または関連PR番号によって裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#57129)、関連PR番号(#17099)、ファイルパスなどの数値・固有名詞はすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRのタイトル(Use Hash#each_key instead of Hash#keys.each)の内容を汲み取りつつ、「メモリ割り当てを削減」という目的を加えており、PRの内容を的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に含まれないバージョンサポート状況やリリース日程などの外部知識は含まれておらず、事実に基づいた記述が徹底されています。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「かつて...行われており」「その後に追加された」といった時間に関する表現は、PRの文脈と一致しており正確です。