如何将“each”方法添加到Ruby对象(或者我应该扩展Array)?

时间:2010-01-17 04:58:05

标签: ruby

我有一个对象结果包含result个对象的数组,以及有关数组中对象的一些缓存统计信息。我希望Results对象能够像数组一样运行。我的第一个切入是添加这样的方法

 def <<(val)
    @result_array << val
 end

这感觉很像c,我知道Ruby有更好的方法。

我也希望能够做到这一点

 Results.each do |result|   
    result.do_stuff   
 end

但我不确定each方法究竟在做什么。

目前我只是通过一种方法返回底层数组,然后调用它们,这似乎不是最优雅的解决方案。

任何帮助都将不胜感激。

7 个答案:

答案 0 :(得分:61)

对于实现类似数组的方法的一般情况,是的,你必须自己实现它们。 Vava的答案显示了这方面的一个例子。但是,在您提供的情况下,您真正​​想要做的是将处理each(可能还有其他一些方法)的任务委托给包含的数组,并且可以自动完成。

require 'forwardable'

class Results
  include Enumerable
  extend Forwardable
  def_delegators :@result_array, :each, :<<
end

这个类将获得Array的所有Enumerable行为以及Array <<运算符,它将全部通过内部数组。


注意,当你将代码从数组继承切换到这个技巧时,你的<<方法将开始返回不是自身的对象,就像真正的数组的<<那样 - 这可能会花费你的声明每次使用<<时的另一个变量。

答案 1 :(得分:37)

each只是遍历数组并使用每个元素调用给定的块,这很简单。由于您在类中使用数组,因此您只需将each方法重定向到数组中的方法,即可快速轻松地读取/维护。

class Result
    include Enumerable

    def initialize
        @results_array = []
    end

    def <<(val)
        @results_array << val
    end

    def each(&block)
        @results_array.each(&block)
    end
end

r = Result.new

r << 1
r << 2

r.each { |v|
   p v
}

#print:
# 1
# 2

请注意,我已混入Enumerable。这将为您提供一堆免费的all?map等数组方法。

使用Ruby,你可以忘记继承。您不需要接口继承,因为duck-typing并不真正关心实际类型,并且您不需要代码继承,因为mixins对于这类事情更好。

答案 2 :(得分:9)

  

这感觉很像,我知道Ruby   有更好的方法。

如果你想让一个对象“感觉”像一个数组,而不是覆盖&lt;&lt;是一个好主意,非常'Ruby'-ish。

  

但我不确定每种方法是什么   真的是在幕后做。

Array的每个方法只循环遍历所有元素(使用for循环,我认为)。如果你想添加自己的每个方法(也非常'Ruby'-ish),你可以这样做:

def each
  0.upto(@result_array.length - 1) do |x|
    yield @result_array[x]
  end
end

答案 3 :(得分:8)

你的&lt;&lt;方法非常好,非常像Ruby。

要使类像数组一样,而不直接从Array继承,可以混合使用Enumerable模块并添加一些方法。

这是一个例子(包括Chuck关于使用Forwardable的优秀建议):

# You have to require forwardable to use it
require "forwardable"

class MyArray
  include Enumerable
  extend Forwardable

  def initialize
    @values = []
  end

  # Map some of the common array methods to our internal array
  def_delegators :@values, :<<, :[], :[]=, :last

  # I want a custom method "add" available for adding values to our internal array
  def_delegator :@values, :<<, :add

  # You don't need to specify the block variable, yield knows to use a block if passed one
  def each
    # "each" is the base method called by all the iterators so you only have to define it
    @values.each  do |value| 
      # change or manipulate the values in your value array inside this block
      yield value
    end
  end

end

m = MyArray.new

m << "fudge"
m << "icecream"
m.add("cake")

# Notice I didn't create an each_with_index method but since 
# I included Enumerable it knows how and uses the proper data.
m.each_with_index{|value, index| puts "m[#{index}] = #{value}"}

puts "What about some nice cabbage?"
m[0] = "cabbage"
puts "m[0] = #{m[0]}"

puts "No! I meant in addition to fudge"
m[0] = "fudge"
m << "cabbage"
puts "m.first = #{m.first}"
puts "m.last = #{m.last}"

哪个输出:

m[0] = fudge
m[1] = icecream
m[2] = cake
What about some nice cabbage?
m[0] = cabbage
No! I meant in addition to fudge
m.first = fudge
m.last = cabbage

答案 4 :(得分:5)

如果您创建一个继承自Array的类Results,您将继承所有功能。

然后,您可以通过重新定义它们来补充需要更改的方法,并且可以为旧功能调用super。

例如:

class Results < Array
  # Additional functionality
  def best
    find {|result| result.is_really_good? }
  end

  # Array functionality that needs change
  def compact
    delete(ininteresting_result)
    super
  end
end

或者,您可以使用内置库forwardable。如果您无法从Array继承,则此功能特别有用,因为您需要从另一个类继承:

require 'forwardable'
class Results
  extend Forwardable
  def_delegator :@result_array, :<<, :each, :concat # etc...

  def best
    @result_array.find {|result| result.is_really_good? }
  end

  # Array functionality that needs change
  def compact
    @result_array.delete(ininteresting_result)
    @result_array.compact
    self
  end
end

在这两种形式中,您可以根据需要使用它:

r = Results.new
r << some_result
r.each do |result|
  # ...
end
r.compact
puts "Best result: #{r.best}"

答案 5 :(得分:4)

不确定我是否添加了任何新内容,但我决定展示一个非常简短的代码,我希望我能在答案中找到这些代码以快速显示可用选项。这里没有@shelvacu谈论的调查员。

class Test
   def initialize
     @data = [1,2,3,4,5,6,7,8,9,0,11,12,12,13,14,15,16,172,28,38]
   end

   # approach 1
   def each_y
     @data.each{ |x| yield(x) }
   end

   #approach 2
   def each_b(&block)
     @data.each(&block)
   end  
end

让我们检查一下表现:

require 'benchmark'
test = Test.new
n=1000*1000*100
Benchmark.bm do |b|
  b.report { 1000000.times{ test.each_y{|x| @foo=x} } }
  b.report { 1000000.times{ test.each_b{|x| @foo=x} } }
end

结果如下:

       user     system      total        real
   1.660000   0.000000   1.660000 (  1.669462)
   1.830000   0.000000   1.830000 (  1.831754)

这意味着yield稍微快于阻止我们已经知道的事情。

UPDATE :这是IMO创建每个方法的最佳方式,该方法也负责返回枚举器

class Test
  def each
    if block_given?
      @data.each{|x| yield(x)}  
    else    
      return @data.each
    end  
  end  
end

答案 6 :(得分:2)

如果你真的想制作自己的#each方法,假设你不想转发,那么如果没有给出阻止,你应该返回一个Enumerator

class MyArrayLikeClass
  include Enumerable

  def each(&block)
    return enum_for(__method__) if block.nil?
    @arr.each do |ob|
      block.call(ob)
    end
  end
end

如果没有给出块,则返回一个Enumerable对象,允许Enumerable方法链接