Подтвердить что ты не робот

Как добавить метод "каждый" к объекту Ruby (или я должен расширить Array)?

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

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

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

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

 Results.each do |result|   
    result.do_stuff   
 end

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

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

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

4b9b3361

Ответ 1

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

require 'forwardable'

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

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


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

Ответ 2

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

Ответ 3

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

Если вы хотите, чтобы объект "чувствовал", как массив, чем переопределение < < это хорошая идея и очень "Руби-иш".

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

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

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

Ответ 4

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

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

Вот пример (в том числе Чак превосходное предложение использовать переадресацию):

# 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

Если вы создадите класс 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}"

Ответ 6

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

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

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

Ответ 7

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