`Hash#keys.each` を `Hash#each_key` に置き換えてメモリ割り当てを削減
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.rb と activerecord/lib/active_record/attribute_methods/dirty.rb の2箇所」と説明しています。一方、最終的なDiffはこれらに加えてテストファイル2つ(activerecord/test/cases/connection_adapters/connection_handler_test.rb、activesupport/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.rb、strong_parameters.rb、routing/mapper.rb)。これらはループ内で delete を使ってハッシュを変更するため、イテレーション前にキーのスナップショットを取る keys が必要であり、今回の対象から意図的に除外されています。
設計判断
変更対象を「イテレーション中にハッシュを変更しない箇所」に限定したことが、この変更の重要な設計上の判断です。
ループ内で delete などハッシュを破壊的に操作する場合、Hash#each_key に置き換えると反復の挙動が変化し、バグを引き起こす恐れがあります。PRはこの区別を明示的に記述しており、安全に置き換えできる箇所のみを厳選しています。機械的な一括置換ではなく、各呼び出し箇所のセマンティクスを確認した上での変更である点が、このPRの信頼性を高めています。
まとめ
今回の変更は1行ずつの小さな修正ですが、Hash#each_key が存在する理由を改めて示す好例です。ループ内でハッシュを変更しないという事前条件を確認してから置き換えるという判断は、同様のパターンを自分のコードで見つけた際にも直接応用できます。