2015-11-10 36 views
0

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

例如,给定下面的代码:

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

对于红宝石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 

经过一番搜索后,它看起来像this is known by the Ruby devs,是discussed a little bit,详细我n this blog post。这种变化也打破Rails的HashWithIndifferentAccessthis issue,为此,他们发出a pull request for Rails 4(但仍然在Rails的3.2.22坏了?)

显然,这种行为抓了很多人猝不及防,并改变声音可笑考虑如何它打破了已知的Ruby gem宇宙(包括Rails,Hashie等),它们依赖于这个基本思想,即对象不应该意外地改变类型。

我的问题,对于一些更明智的Ruby开发者的是:

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

我认为这属于不从核心类继承的普遍共识。 [Nice Article](http://words.steveklabnik.com/beware-subclassing-ruby-core-classes) – engineersmnky

+0

对链接的Ruby错误的评论不是第一个回答的两个问题吗? 1.是的,2.因为之前的行为是错误的。查看其对应的'Hash#select',它总是返回一个“Hash”实例 - 即使在1.9.3和以前的版本中。 – cremno

+0

不要错,这不是'Hash'的专业化。你可以告诉这种情况,因为派生类只是使用底层的'Hash'实例,并且与它没有其他关系。我认为这里的根本问题是你的'Hash'的子类并不真正和'Hash'有一个“是-α”的关系。它只是用'Hash'来实现的,显然你希望它有一个类似于'Hash'的接口,但是单凭这一点不足以使用继承。这显然是一种“一种”的关系。 –

回答

2

CAVEAT!我不是“更好的知情的Ruby开发者”,这是我看过这个问题的第一个。

看着提交历史,这是一个有意的改变。

commit 740535f843d65be45732e45b9fc07eadc4d63ba7 
Author: nobu <[email protected]> 
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 #9223Matz accepting the change of Hash#reject

我接受此行为更改。 #reject不应像#select那样复制实例变量等。

它看起来是用于2.2,但是从2.1擦洗它已经被破坏了。

它似乎抄袭了课堂,其余都是意外。改变是为了使哈希方法更加一致。


如何开发人员预计,以适应此更改的行为? (我们都在预料之中的事完全一样Rails的团队做?)

答案很简单,切换到hash.dup.delete_if保留所有版本相同的行为。

或者,您可以在您的子类中覆盖Hash#reject以保留旧的行为,但那么您的散列子类将打破新的Hash#reject接口。


海事组织的开发者犯了一个错误。 Hash#reject的行为是可取的。方法不应该对它们自己的硬编码类名进行方法调用。方法应该努力保留他们的调用者的阶级。否则,你有这样的情况:子类必须围绕所有东西编写包装,以避免不小心将一个父对象返回给不知情的用户。

无论它是isa还是hasa关系都没关系。这是一个内部实现问题,对于您的对象的外部用户应该是不可见的。

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