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))
分析与见解
核心问题:哈希契约的一致性破坏
你的Locale类违反了Ruby集合依赖的哈希契约:- 集合判断元素存在时,会先通过
hash值定位哈希桶,再在桶内用eql?比较元素。 - 当前
eql?逻辑是非对称的:Locale.new(code: :en).eql?("en")返回true,但"en".eql?(Locale.new(code: :en))返回false(因为字符串的eql?只同类型比较)。这种不对称性会导致集合在不同遍历顺序下出现不一致结果。
- 集合判断元素存在时,会先通过
版本差异的根源:哈希桶遍历逻辑修复
- Ruby 3.0.0中,当集合元素超过小哈希阈值(比如1000个元素触发扩容),存储结构切换为哈希表。此时
Locale实例、:en、"en"哈希值相同,会被放入同一桶。但集合检查时可能先遍历到:en或"en",由于它们的eql?不认可Locale实例,就直接返回false,未继续遍历桶内其他元素。 - Ruby 3.0.1中修复了哈希桶的遍历逻辑,确保会检查桶内所有哈希值匹配的元素,直到找到符合
eql?条件的对象,因此返回true。
- Ruby 3.0.0中,当集合元素超过小哈希阈值(比如1000个元素触发扩容),存储结构切换为哈希表。此时
修复建议
要让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
相关产品推荐
相关产品推荐

