2008年1月13日星期日

HashWithIndifferentAccess

Rails里,常用params[:sym]来取页面的参数,其实从页面上传过来的都是字符串(hash的key和value都是字符串),Rails为了让我们能够按照习惯用symbol来取值,就做了一些改变
在activesupport/lib/active_support/core_ext/hash/indifferent_access.rb里。
所有的key都被强制转换成了string,也就是说:
h = HashWithIndifferentAccess.new  #=>  {}
h[:a] = "a" #=> "a"
h #=> {"a" => "a"}

具体的转换体现在下面的代码:
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
alias_method :regular_update, :update unless method_defined?(:regular_update)

def []=(key, value)
regular_writer(convert_key(key), convert_value(value))
end

def update(other_hash)
other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
self
end
先给父类的[]=方法和update方法取个别名,然后再覆盖这两个方法。

然而进一步查看其源代码,却发现它对stringify_keys!和symbolize_keys!这两个方法的测试不正确,没有覆盖到。因为我把对应现代码注释掉,再运行测试,居然都通过了,没有任何报错提示。
源代码如下:
def stringify_keys!; self end
def symbolize_keys!; self end
测试代码如下:
def test_stringify_and_symbolize_keys_on_indifferent_preserves_hash
h = HashWithIndifferentAccess.new
h[:first] = 1
h.stringify_keys!
assert_equal 1, h['first']
h = HashWithIndifferentAccess.new
h['first'] = 1
h.symbolize_keys!
assert_equal 1, h[:first]
end

应该改为:
def test_stringify_and_symbolize_keys_on_indifferent_preserves_hash
h = HashWithIndifferentAccess.new
h[:first] = 1
h.stringify_keys!
assert_equal 1, h['first']
h = HashWithIndifferentAccess.new
h['first'] = 1
h.symbolize_keys!
assert_not_equal :first, h.index(1)
# assert_equal 1, h[:first]
end

个人认为stringify_keys!方法在这里是无法测试的,因为该类里key全都转换成string了,那么这个"Destructively convert all keys to strings."(父类方法的注释说明)的方法,其调用前后,对象没有任何改变,无法编写对应的测试。这里覆盖父类方法,可能的原因是基于效率的考虑,让其不再调用父类的stringify_keys!方法(因为已经全都是string了,再调用一次,再判断一次是浪费)。

还有个问题,HashWithIndifferentAccess类只保证了key都是按string来存储的,但为什么h[:a]这样用symbol还能取到呢?它并没有覆盖父类的[]方法。
经过跟同事的讨论,有了结果:
def default(key = nil)
if key.is_a?(Symbol) && include?(key = key.to_s)
self[key]
else
super
end
end
它覆盖了default方法,而ruby实现里[]方法会去调用default方法。具体的ruby底层实现机制我并不了解,只是通过改源代码和其测试代码,发现了default方法跟[]方法相关。

没有评论: