Ruby MRI 3.0.0 と 3.0.1 の間のハッシュ関連の動作の不一致
概要
Ruby MRI 3.0.0 と 3.0.1 の間で動作が変更されたことに気付きましたが、変更ログ (https://github.com/ruby/ruby/compare/v3_0_0…v3_0_1) で理由がわかりません。
次の単純な「値オブジェクト」クラスを考えてみましょう。
# frozen_string_literal: true
class Locale
attr_reader :code
delegate :to_s, :to_sym, :hash, to: :code
def initialize(code:)
@code = code
end
def eql?(other)
other.respond_to?(:to_sym) && to_sym == other.to_sym
end
alias == eql?
end
Ruby 3.0.0:
[9] pry(main)> p RUBY_VERSION; ((1..1000).to_a + [:en, "en"]).to_set.include?(Locale.new(code: :en))
"3.0.0"
false
Ruby 3.0.1:
[6] pry(main)> p RUBY_VERSION; ((1..1000).to_a + [:en, "en"]).to_set.include?(Locale.new(code: :en))
"3.0.1"
true
小さなハッシュの配列としてのハッシュの最適化に関連している可能性があると思います。
[13] pry(main)> p RUBY_VERSION; ([:en, "en"]).to_set.include?(Locale.new(code: :en))
"3.0.0"
true
以下は、3.0.0 と 3.0.1 で異なる結果を生成する単純な Ruby スクリプトです (場合によっては不安定です)。
raise unless RUBY_VERSION == "3.0.0" || RUBY_VERSION == "3.0.1"
require "set"
class Locale
attr_reader :code
def initialize(code:)
@code = code
end
def eql?(other)
other.respond_to?(:to_sym) && to_sym == other.to_sym
end
alias == eql?
def to_sym
code.to_sym
end
def hash
code.hash
end
end
p RUBY_VERSION
p Set.new(((1..1000).to_a + [:ru, :en])).include?(Locale.new(code: :en))
これについて何か考えはありますか?
解決策
この動作の変化は、リンク先ページ b2beb8586e930c168af434d6545f75d76123192b の 2 番目のコミットによって引き起こされる可能性があります。
コミットメッセージはバグ #17488 を参照しています。タイトルは「Ruby 3 の回帰: Hash#key?」です。引数が DelegateClass を使用する場合、は非決定的です。したがって、デリゲート、ハッシュ、および非決定的 (不安定な) 結果がすでに得られています。そして、メモによると、修正は 3.0、特に 3.0.1 にバックポートされたため、バージョン 3.0.0 にのみ影響し、2.7 以前は影響を受けないようです。それはあなたの観察のようですね!
バグを発見して修正した ノブ (中田信義) は次のように書きました。
そのため、実際の理由はまだ不明ですが、以前はコードが間違っており、2.7.0 と 3.0.0 の間のこの変更によってそれが明らかになりました。
いわゆる fixnum は実装の詳細です (現在では、Ruby の世界に対して隠蔽する方が適切です。以前は Fixnum クラスがありました)。これらは基本的に 31 ビットまたは 63 ビットの符号付き整数ですが、それらについて詳しく知りたい場合は、たとえば、この SO 回答を読んでください。