Как добавить метод «каждый» к объекту Ruby (или я должен расширить массив)? - PullRequest
40 голосов
/ 17 января 2010

У меня есть объект Results, который содержит массив объектов result вместе с некоторой кэшированной статистикой об объектах в массиве.Мне бы хотелось, чтобы объект Results мог вести себя как массив.Моим первым шагом было добавление таких методов

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

Это похоже на c, и я знаю, что у Ruby есть лучший способ.

Я также хотел бы иметь возможность делатьэто

 Results.each do |result|   
    result.do_stuff   
 end

, но я не уверен, что на самом деле делает метод each.

В настоящее время я просто возвращаю базовый массив с помощью метода и вызываю каждый из них, что не кажется самым элегантным решением.

Любая помощь будет принята.

Ответы [ 7 ]

61 голосов
/ 17 января 2010

Для общего случая реализации массивоподобных методов, да, вы должны реализовать их самостоятельно. Ответ Вавы показывает один пример этого. Однако в случае, который вы дали, вам действительно нужно делегировать задачу обработки each (и, возможно, некоторых других методов) содержащемуся массиву, и это можно автоматизировать.

require 'forwardable'

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

Этот класс получит все перечислимое поведение Array, а также оператор Array << и будет проходить через внутренний массив.


Обратите внимание, что когда вы переключаете свой код с наследования Array на этот трюк, ваши << методы начнут возвращать не сам объект, как это сделал настоящий 1015 * Array - это может стоить вам объявлять другую переменную каждый раз, когда вы используйте <<.

37 голосов
/ 17 января 2010

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 вы можете забыть о наследовании. Вам не нужно наследование интерфейса, потому что типизация утилит на самом деле не заботится о реальном типе, и вам не нужно наследование кода, потому что миксины лучше для такого рода вещей.

9 голосов
/ 17 января 2010

Это очень похоже на C, и я знаю Ruby есть лучший способ.

Если вы хотите, чтобы объект «чувствовал» себя как массив, то переопределение

но я не уверен, что каждый метод действительно делает под капотом.

Каждый метод для Array просто проходит по всем элементам (используя цикл for, Я думаю ). Если вы хотите добавить свой собственный метод для каждого (который также очень похож на Ruby), вы можете сделать что-то вроде этого:

def each
  0.upto(@result_array.length - 1) do |x|
    yield @result_array[x]
  end
end
8 голосов
/ 01 декабря 2010

Ваш метод << отлично подходит и очень похож на Ruby. </p>

Чтобы класс действовал как массив, фактически не наследуя напрямую от Array, вы можете добавить модуль Enumerable и добавить несколько методов.

Вот пример (включая превосходное предложение Чака использовать 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
5 голосов
/ 17 января 2010

Если вы создадите класс Results, который наследуется от Array, вы унаследуете все функции.

Затем вы можете дополнить методы, которые нужно изменить, переопределив их, и вы можете вызвать 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}"
4 голосов
/ 01 августа 2014

Не уверен, что я добавляю что-то новое, но решил показать очень короткий код, который я хотел бы найти в ответах, чтобы быстро показать доступные варианты. Здесь @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 незначительно быстрее, чем & блокирует то, что мы уже знаем.

ОБНОВЛЕНИЕ : это IMO лучший способ создать каждый метод, который также заботится о возврате перечислителя

class Test
  def each
    if block_given?
      @data.each{|x| yield(x)}  
    else    
      return @data.each
    end  
  end  
end
2 голосов
/ 21 мая 2013

Если вы действительно хотите создать свой собственный метод #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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...