Enumerable#inject是Ruby核心库中的一个简洁而且强大的API,今天读到一段简洁的代码之后,对这个API产生了浓厚的兴趣,索性搜寻一下资料,总结一下它的用法。
代码如下:
?
class="java">def text_at(*args) args.inject(@feed) { |s, r| s.send(:at, r)}.inner_text end
这段代码完成的功能是:取出XML文件中某子元素的文本内容,它是用nokogiri库来完成这个功能的。关于Nokogiri库API(at(), inner_text())的细节我们不谈,只是用这段代码来引起你对inject的兴趣,现在我们来看inject的用法。
?
Enumerable#inject是很多Ruby高手喜欢的API,因此将来读Ruby也会经常遇到,即使有的用法不能完全理解,也要先混个脸熟,使用场景见多了就自然理解了。
?
1. 数字求和
Inject最常见的用法就是这一个了,假定你有一个内容为数字的数组,你可以像下面这样对它求和:
irb(main):001:0> [1, 2, 3, 4, 5].inject(0) { |sum, e| sum + e } => 15
或者像这样:
irb(main):002:0> (1..5).inject(0) { |sum, e| sum + e } => 15
用区间或者数组在这里没有分别,Ruby会自动转换。Inject在这里接受一个方法参数和一个block. 对数组中的每一个元素,block都执行一次。第一次执行block的时候,inject接收的方法参数被作为block的第一个参数,而block的第二个参数则是数组的第一个元素。第二次执行block的时候,情况就有了变化,这也是inject神奇的地方。这时候block的第一个参数则是block上一次执行的返回值(block的最后一个表达式),第二个参数则是数组的第二个元素,后面的三次执行方式与第二次相同,这样我们就计算出了数组元素之和。事实上,上面的代码还可以更简洁一些:
irb(main):006:0> [1, 2, 3, 4, 5].inject { |sum, e| sum + e } => 15
这段代码可以计算出相同结果的原因是:inject的方法参数是可选的,如果不提供的话,Ruby默认将数组的第一个元素作为block第一次执行时候的第一个参数,在这种情况下,block一共需要执行4次,比传入默认参数的形式少执行一次。
?
2. 转换数据结构
2.1生成Hash:
hash = [[:first_name, 'Shane'], [:last_name, 'Harvie']].inject({}) do |result, element| result[element.first] = element.last result end
?当然这种用法也有别的形式,并不一定需要用到inject,比如:
Hash[*[[:first_name, 'Shane'], [:last_name, 'Harvie']].flatten]
?可以达到相同目的而代码更简洁。
?
2.2 过滤数组:
arr = [1, 2, 3, 4, 5, 6].inject([]) do |r, e| r << e.to_s if e % 2 == 0 r end
?
当然这种用法也有不使用inject的等价方式:
[1, 2, 3, 4, 5, 6].select { |e| e % 2 == 0 }.collect { |e| e.to_s } # => ["2", "4", "6"]
?具体用哪一种就是萝卜青菜,各有所爱了。
?
3. 更高级的用法
先看一段代码:
class Recorder # undefine any instance methods except methods start from __ # and inspect/to_str/object_id instance_methods.each do |meth| undef_method meth unless meth =~ /^(__|inspect|to_str)|object_id/ end # store messages sent to the Recorder object # that's why it's called Recorder def method_missing(sym, *args) messages << [sym, args] self end # getter of instance variable messages def messages @messages ||= [] end end
代码中比较难懂的部分已经加了注释,类Recorder完成的功能是记录所有发送给它对象的消息(Ruby中对象调用通常称作向对象发送消息)。下面我们再打开类Recorder,定义另一方法来展示这些保存的消息:
class Recorder def play_for(obj) messages.inject(obj) do |result, msg| result.send msg.first, *msg.last end end end
在这个方法中,我们用到了inject, 它向传入的对象obj调用保存的消息,并传入之前调用者传入的所有参数。下面的代码展现了它的用法:
recorder = Recorder.new recorder.methods.sort recorder.play_for(String)
它实现了对String对象(你应该可以想起来,Ruby的类也是对象)调用#methods(), 然后对#methods返回结果调用#sort().
其实上面这个Recorder示例和本文开头的那个范例原理相同,前一个调用可以响应第一个消息,返回的结果则分别可以响应接下来的消息,对比这两个示例可以对Enumerable#inject的强大之处有所体会。
?
参考:http://blog.jayfields.com/2008/03/ruby-inject.html