Techioz Blog

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 回答を読んでください。