2008年1月16日星期三

Hash中去掉不需要的key

Ruby中,Hash本身的delete是很方便,但也有不足的地方:
一次只能删掉一个key,而且返回值是对应key的value,而不是hash,无法做这样的链式操作:hash.delete(:key_1).delete(:key_2)
delete本身是破坏性的方法,会改变hash的值,而有时候我不想改变,只想得到一个新的hash。

于是自己实现一个:
def remove_keys(hash, *keys)
new_hash = hash.dup
keys.each {|k| new_hash.delete(k)}
new_hash
end

后来去查了一下Rails源代码,发现已经有现成的方法了,在activesupport/lib/active_support/core_ext/hash/except.rb中
# Returns a new hash without the given keys.
def except(*keys)
rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
reject { |key,| rejected.include?(key) }
end

# Replaces the hash without only the given keys.
def except!(*keys)
replace(except(*keys))
end

感觉写框架的就是不一样,考虑到了很多,不像我那个,自己写自己用,确保按照正确方式用就行了,异常使用方式就没去处理(其实我那个方法一般是不会有什么异常使用方式的,因为dup和delete方法在HashWithIndifferentAccess类中已经被重写了,DHH已经帮我预先处理了异常使用方式)。
这里为什么要先判断一下能否响应convert_key方法呢?这就涉及到上次讲的HashWithIndifferentAccess,convert_key源代码如下:
def convert_key(key)
key.kind_of?(Symbol) ? key.to_s : key
end
像params这种,里面的key存的全都是string,你指定删除symbol的key,如果程序实现直接去删的话,就会不起作用(没有去掉指定的key)
然而,我将源代码改为:
def except(*keys)
rejected = keys
reject { |key,| rejected.include?(key) }
end
运行测试,还是全都通过了,看来测试代码没有覆盖到这种情况。

找到测试代码:
def test_except
original = { :a => 'x', :b => 'y', :c => 10 }
expected = { :a => 'x', :b => 'y' }

# Should return a new hash with only the given keys.
assert_equal expected, original.except(:c)
assert_not_equal expected, original

# Should replace the hash with only the given keys.
assert_equal expected, original.except!(:c)
assert_equal expected, original
end

改为:
def test_except
original = { :a => 'x', :b => 'y', :c => 10 }
expected = { :a => 'x', :b => 'y' }

# Should return a new hash with only the given keys.
assert_equal expected, original.except(:c)
assert_not_equal expected, original

# Should replace the hash with only the given keys.
assert_equal expected, original.except!(:c)
assert_equal expected, original

original = HashWithIndifferentAccess.new(:a => 'x', :b => 'y', :c => 10)
expected = HashWithIndifferentAccess.new(expected)
assert_equal expected, original.except(:c)
end
哈哈,测试报错了,看来我们刚才添加的测试代码起作用了,TDD的第一步,即写一段测试,使测试failed,已经完成了。
然后要做的就是完成代码使测试通过。这个很简单,把刚才改的代码恢复就可以了。
恢复后,再运行测试,通过了。看来我们刚才加的测试覆盖到了这行代码了:
  rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)

让我们再多想一下,如果去掉上面那行代码,不考虑HashWithIndifferentAccess的情况,那么就下面这一行代码就实现了删除指定key的功能了。
def except(*keys)
reject { |key,| keys.include?(key) }
end
晕死,原来就这么简单呀,到处去找不改变hash本身的delete方法,其实reject就是:

Same as Hash#delete_if, but works on (and returns) a copy of the hsh. Equivalent to hsh.dup.delete_if.


最后查了一下项目,发现在Rails 1.2.5里面,except方法是在vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb中定义的。
准确说是Rails 1.2.5里没有这个方法,而是由will_paginate这个分页的plugin提供的。Rails 2.0把它移到了activesupport里。
以后关于Rails源代码,没有说明具体版本,都默认为Rails 2.0

没有评论: