You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ruby MRI 3.0.0与3.0.1哈希相关行为差异问询

关于Ruby MRI 3.0.0与3.0.1哈希相关行为差异的分析

问题背景

我发现Ruby MRI 3.0.0与3.0.1之间存在哈希相关行为变化,但在3.0.0到3.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版本的测试结果

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

初步推测

我推测这可能与小哈希的数组式哈希优化有关,因为在3.0.0中,当集合元素较少时结果符合预期:

[13] pry(main)> p RUBY_VERSION; ([:en, "en"]).to_set.include?(Locale.new(code: :en))
"3.0.0"
true

不稳定输出的测试脚本

以下纯Ruby脚本在3.0.0与3.0.1上会偶尔产生不同输出:

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))

分析与见解

  1. 核心问题:哈希契约的一致性破坏
    你的Locale类违反了Ruby集合依赖的哈希契约:

    • 集合判断元素存在时,会先通过hash值定位哈希桶,再在桶内用eql?比较元素。
    • 当前eql?逻辑是非对称的:Locale.new(code: :en).eql?("en")返回true,但"en".eql?(Locale.new(code: :en))返回false(因为字符串的eql?只同类型比较)。这种不对称性会导致集合在不同遍历顺序下出现不一致结果。
  2. 版本差异的根源:哈希桶遍历逻辑修复

    • Ruby 3.0.0中,当集合元素超过小哈希阈值(比如1000个元素触发扩容),存储结构切换为哈希表。此时Locale实例、:en、"en"哈希值相同,会被放入同一桶。但集合检查时可能先遍历到:en或"en",由于它们的eql?不认可Locale实例,就直接返回false,未继续遍历桶内其他元素。
    • Ruby 3.0.1中修复了哈希桶的遍历逻辑,确保会检查桶内所有哈希值匹配的元素,直到找到符合eql?条件的对象,因此返回true。
  3. 修复建议
    要让Locale类符合哈希契约,需保证eql?的对称性,同时维持hash与eql?的关联:

    def eql?(other)
      other.is_a?(Locale) && code == other.code
    end
    alias == eql?
    

    如果确实需要让Locale与Symbol/String互认,不建议修改原生类型的eql?,应改用自定义集合逻辑替代原生Set。

内容的提问来源于stack exchange,提问作者vetements

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 04:52:44