Ruby 2.2 Hash#拒绝为继承类返回Hash



在从Ruby 1.9.3升级到Ruby 2.2.3(MRI)的过程中,我发现了一个影响从Hash继承的任何类的问题。如果对从Hash继承的类的实例调用#reject,它将始终返回Hash,而不是调用它的类的一个实例。

例如,给定以下代码:

class CustomHash < Hash
  def count_in_english
    "There are #{self.count} items in this hash."
  end
end

对于Ruby 1.9.3,以下操作成功:

1.9.3-p547 :060 > hash = CustomHash.new
 => {} 
1.9.3-p547 :061 > hash[1] = 'a'
 => "a" 
1.9.3-p547 :062 > hash[2] = 'b'
 => "b" 
1.9.3-p547 :063 > hash[3] = 'c'
 => "c" 
1.9.3-p547 :064 > odds_only_hash = hash.reject { |k,v| k % 2 == 0 }
 => {1=>"a", 3=>"c"} 
1.9.3-p547 :065 > odds_only_hash.count_in_english
 => "There are 2 items in this hash."
1.9.3-p547 :066 > odds_only_hash.class
 => CustomHash

但在Ruby 2.2.3中:

2.2.3 :019 > hash = CustomHash.new
 => {} 
2.2.3 :020 > hash[1] = 'a'
 => "a" 
2.2.3 :021 > hash[2] = 'b'
 => "b" 
2.2.3 :022 > hash[3] = 'c'
 => "c" 
2.2.3 :023 > odds_only_hash = hash.reject { |k,v| k % 2 == 0 }
 => {1=>"a", 3=>"c"} 
2.2.3 :024 > odds_only_hash.count_in_english
NoMethodError: undefined method `count_in_english' for {1=>"a", 3=>"c"}:Hash
    from (irb):24
    from /Users/davidelner/.rvm/rubies/ruby-2.2.3/bin/irb:15:in `<main>'
2.2.3 :025 > odds_only_hash.class
 => Hash

经过一点搜索,Ruby开发人员似乎已经知道了这一点,在这篇博客文章中进行了一些讨论和详细介绍。根据这个问题,这个变化也破坏了Rails的HashWithIndifferentAccess,为此他们发布了Rails4的pull请求(但在Rails3.2.22中仍然被破坏?)

很明显,这种行为让很多人措手不及,考虑到它如何打破了已知的Ruby gem宇宙(包括Rails、Hashie等),这种改变听起来很可笑,因为这些宇宙依赖于对象不应该意外地改变类型这一基本思想。

对于一些见多识广的Ruby开发人员来说,我的问题是:

  • Ruby是否承诺在Ruby 2.2.3和所有未来版本中,Hash#reject将始终返回Hash?(与调用此函数的类的实例相反?例如1.9.3 C源return rb_hash_delete_if(rb_obj_dup(hash));
  • 如果是,为什么现在是默认行为?这难道不是有效地"密封"了#reject,并打破了使用这些Enumerable函数仍然会返回相同类型对象的合理预期吗
  • 此外,如果是这样的话,开发人员应该如何适应这种行为上的变化?(我们是否都应该像Rails团队那样做?)

CAVEAT 我不是一个"见多识广的Ruby开发人员",这是我第一次看到这个问题。

从提交历史来看,这是一个深思熟虑的改变。

commit 740535f843d65be45732e45b9fc07eadc4d63ba7
Author: nobu <nobu@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>
Date:   Wed Dec 11 07:01:29 2013 +0000
    hash.c: reject should return a plain hash
    * hash.c (rb_hash_reject): return a plain hash, without copying
      the class, default value, instance variables, and taintedness.
      they had been copied just by accident.
      [ruby-core:59045] [Bug #9223]

Bug#9223让Matz接受Hash#reject的更改。

我接受这种行为的改变#reject不应该复制实例变量等,就像#select一样。

它似乎是为2.2设计的,但从2.1中删除却失败了。

这似乎是抄袭了这门课,其余的都是意外。进行此更改是为了使Hash方法之间更加一致。


开发人员应如何适应这种行为变化?(我们都应该像Rails团队那样做吗?)

简单的答案是切换到hash.dup.delete_if以在所有版本中保持相同的行为。

或者,您可以覆盖子类中的Hash#reject以保留旧的行为,但随后您的哈希子类将破坏新的Hash#reject接口。


IMO开发人员犯了一个错误。CCD_ 15的行为是令人满意的。方法不应该对自己的硬编码类名进行方法调用。方法应该努力保留其调用者的类。否则就会出现这样的情况,即子类必须围绕所有内容编写包装器,以避免意外地将父对象返回给不知情的用户。

不管是isa还是hasa关系。这是一个内部实现问题,对于对象的外部用户来说应该是不可见的。

如果需要一致性,则Hash#select的行为应该更改为匹配。