Ruby 显式指定的方法或块的数组差异

Ruby 显式指定的方法或块的数组差异,ruby,Ruby,如果我有数组a和b,表达式a-b将返回一个数组,其中包含a中不在b中的所有元素。“不在”在这里是指不合格(!=) 在我的例子中,两个数组只包含相同类型的元素(或者,从ducktyping的角度来看,只包含理解“相等”方法的元素f)。 是否有一种简单的方法可以指定此f作为相等的标准,类似地,我可以在执行排序时提供自己的比较器?目前,我明确地实现了这一点: # Get the difference a-b, based on 'f': a.select { |ael| b.all? {|bel| a

如果我有数组
a
b
,表达式
a-b
将返回一个数组,其中包含
a
中不在
b
中的所有元素。“不在”在这里是指不合格(
!=

在我的例子中,两个数组只包含相同类型的元素(或者,从ducktyping的角度来看,只包含理解“相等”方法的元素
f
)。 是否有一种简单的方法可以指定此
f
作为相等的标准,类似地,我可以在执行
排序时提供自己的比较器?目前,我明确地实现了这一点:

# Get the difference a-b, based on 'f':
a.select { |ael| b.all? {|bel| ael.f != bel.f} }
这是可行的,但我想知道是否有更简单的方法

更新:从对这个问题的评论中,我得到的印象是,如果能举一个具体的例子,我将不胜感激。那么,我们开始吧:

class Dummy; end

# Create an Array of Dummy objects.
a = Array.new(99) { Dummy.new }

# Pick some of them at random
b = Array.new(10) { a.sample }

# Now I want to get those elements from a, which are not in b.
diff = a.select { |ael| b.all? {|bel| ael.object_id != bel.object_id} }
当然,在这种情况下,我也可以说
!ael eql?bel
,但在我的一般解决方案中,情况并非如此。

例如数组上的哈希和集合操作(例如
-
操作)的“正常”对象相等使用包含对象的方法的输出以及
a.eql?(b)
比较的语义

这可以用来提高性能。Ruby在这里假设两个对象是
eql?
,如果它们各自的
hash
方法的返回值相同(因此,假设两个返回不同
hash
值的对象不是
eql?

对于正常的
a-b
操作,这可以用于首先计算每个对象的
哈希值一次,然后仅比较这些值。这相当快

现在,如果您有一个自定义等式,那么最好覆盖对象的
hash
方法,以便它们为这些语义返回合适的值

一种常见的方法是构建一个数组,其中包含参与对象标识的所有数据,并获取其哈希值,例如

class MyObject
  #...
  attr_accessor :foo, :bar 

  def hash
    [self.class, foo, bar].hash
  end
end
在对象的
hash
方法中,您将包含
f
比较方法当前考虑的所有数据。然后,您不再实际使用
f
,而是使用所有Ruby对象的默认语义,并且可以再次实现对象的快速设置操作

然而,如果这是不可行的(例如,因为您需要基于用例的不同平等语义),您可以自己模拟ruby所做的事情

使用
f
方法,您可以执行如下设置操作:

def f_difference(a, b)
  a_map = a.each_with_object({}) do |a_el, hash|
    hash[a_el.f] = a_el
  end

  b.each do |b_el|
    a_map.delete b_el.f
  end

  a_map.values
end
使用这种方法,只需计算每个对象的
f
值一次。我们首先用
a
中的所有
f
值和元素构建一个哈希映射,并根据它们的
f
值从
b
中删除匹配的元素。剩下的值就是结果

这种方法使您不必为
a
中的每个对象循环
b
,如果对象太多,循环速度可能会很慢。但是,如果每个数组上只有几个对象,那么您最初的方法应该已经很好了

让我们看一看基准测试,当我使用标准的
hash
方法代替自定义的
f
来获得可比较的结果时

require 'benchmark/ips'

def question_diff(a, b)
  a.select { |ael| b.all? {|bel| ael.hash != bel.hash} }
end

def answer_diff(a, b)
  a_map = a.each_with_object({}) do |a_el, hash|
    hash[a_el.hash] = a_el
  end

  b.each do |b_el|
    a_map.delete b_el.hash
  end

  a_map.values
end

A = Array.new(100) { rand(10_000) }
B = Array.new(10) { A.sample }

Benchmark.ips do |x|
  x.report("question") { question_diff(A, B) }
  x.report("answer") { answer_diff(A, B) }

  x.compare!
end
使用Ruby 2.7.1,我在我的机器上得到以下结果,表明问题的原始方法比我的答案中的优化版本慢5.9倍:

Warming up --------------------------------------
            question     1.304k i/100ms
              answer     7.504k i/100ms
Calculating -------------------------------------
            question     12.779k (± 2.0%) i/s -     63.896k in   5.002006s
              answer     74.898k (± 3.3%) i/s -    375.200k in   5.015239s

Comparison:
              answer:    74898.0 i/s
            question:    12779.3 i/s - 5.86x  (± 0.00) slower

为什么不直接使用
f
?(1) 创建一个哈希
h
,它将
ael.f
映射到
ael
(2)遍历
b
并从
h
中删除
bel.f
(3)返回
h.values
。我假设OP的原始
f
方法只比较值。从您的描述中,它看起来好像您假设
f
本质上返回了一个类似于我用
f\u hash
构建的值。如果是这样,OP可以使用他们的
f
方法,就像我在回答中使用的
f_hash
一样。在任何情况下,最终的哈希比较都相当快。我仍然用你的方法编辑了我的答案,这确实比我原来的方法快了一点点。尽管如此,它仍然假设一个合适的
f_hash
方法bel.f
–这看起来像一个静态值.Hmmm,现在我再次阅读了这个问题,
f
确实应该作为我的
f\u散列
方法来工作,好的,根据这些新信息再次编辑。一般的方法是一样的:)也许我真的应该阅读实际的问题,而不是假设一个更有趣的问题:)嗯。。我能看看你想处理的数据吗?所有A的F都是一样的吗?@chad\uz:实际上,我的
A
b
中的元素都是非常复杂的类。这就是为什么我把他们漏掉了。如果您需要一个具体的示例,可以将
f
简单地看作是方法
object\u id
。实际上,在我的程序的早期版本中,这里确实有
object\u id
,只是我现在切换到了一些不同的东西,这更适合我。您可以创建一个包装器,将
eql?
散列
委托给相应方法的结果。然后
map
将所有对象映射到包装器,在包装器上执行所需的计算(差异/排序),然后展开(我的意思是覆盖==数组中对象的类内部…@user1934428不,您需要添加包装器进行排序/差异,然后将其去掉。您的对象将保持不变。