匿名splat・転送構文への置き換えによるアロケーション削減
SafeBuffer の引数転送を *args, &block から ... や匿名 *・** へ置き換えることで、不要なオブジェクトアロケーションを削減しつつ、コードの可読性も向上させました。
背景
Rubyには引数を転送する際に名前付きの配列変数を省略できる匿名splat構文(*・**)と、引数・ブロックをまとめて転送する ...(endless forwarding) が存在します。これらは単なる記法の問題にとどまらず、引数を収集するための中間配列オブジェクトの生成を抑制する効果があります。
ActiveSupport::SafeBuffer は String を継承したHTMLエスケープ管理クラスであり、多くのメソッドで引数をそのままスーパークラスや to_str に転送していました。これらのメソッドが *args を使用していた箇所では、転送のたびに Array オブジェクトが生成されていました。PR の説明にある「minor allocation reduction」はこの中間配列生成の抑制を指しています。
技術的な変更
変更は activesupport/lib/active_support/core_ext/string/output_safety.rb に集中しており、3つのパターンで匿名構文が適用されています。
パターン1: 匿名 * による単純な引数転送
[] メソッドでは、引数を変数名なしで受け取り、そのまま to_str へ転送する形に変わりました。
変更前:
def [](*args)
if html_safe?
# ...
else
to_str[*args]
end
end
変更後:
def [](*)
if html_safe?
# ...
else
to_str[*]
end
end
パターン2: ... による引数+ブロックの一括転送
slice! と UNSAFE_STRING_METHODS の各メソッドでは ... が採用されました。... は引数・キーワード引数・ブロックをすべてまとめて転送できるため、*args と &block の2変数を1つの記号で置き換えられます。
変更前:
def #{unsafe_method}(*args, &block) # def capitalize(*args, &block)
to_str.#{unsafe_method}(*args, &block) # to_str.capitalize(*args, &block)
end
変更後:
def #{unsafe_method}(...) # def capitalize(...)
to_str.#{unsafe_method}(...) # to_str.capitalize(...)
end
パターン3: (*, **, &block) による選択的転送
UNSAFE_STRING_METHODS_WITH_BACKREF(gsub 等)は、ブロックが存在する場合に $~(マッチデータ)を手動でセットする特殊な処理が必要なため、ブロック変数は名前付きのまま残し、位置引数とキーワード引数のみを匿名化した (*, **, &block) の形が採用されています。
変更前:
def #{unsafe_method}(*args, &block) # def gsub(*args, &block)
if block
to_str.#{unsafe_method}(*args) { |*params| # to_str.gsub(*args) { |*params|
変更後:
def #{unsafe_method}(*, **, &block) # def gsub(*, **, &block)
if block
to_str.#{unsafe_method}(*, **) { |*params| # to_str.gsub(*, **) { |*params|
ブロック内で block.call(*params) と set_block_back_references を呼ぶ必要があるため、... で一括転送できないこのケースでは * と ** を個別に匿名化する方法が選ばれています。
設計判断
状況に応じて3つの匿名構文を使い分けた 点が、この変更の核心です。
単純に「すべて ... にする」のではなく、各メソッドの転送の性質によって構文を選択しています:
- ブロックを使わない転送 → 匿名 *
- 引数・ブロックをそのまま転送 → ...
- ブロックの中身を書き換える必要がある転送 → *, ** + 名前付き &block
この使い分けにより、「変数を宣言しない」という原則を最大限に適用しながら、gsub のようなブロック内処理の書き換えが必要なケースの正確さも維持しています。
まとめ
この変更は、Ruby の匿名splat・転送構文を SafeBuffer 全体に一貫して適用したものです。中間配列オブジェクトのアロケーション削減という実利に加え、「引数を使わないなら名前をつけない」というコード上の意図の明確化も同時に達成しています。