Ruby的<=>运算符和排序方法

9
player1 = Player.new("moe")
player2 = Player.new("larry",60)
player3 = Player.new("curly", 125)
@players = [player1, player2, player3]

上面,我创建了一些玩家对象并将它们添加到之前为空的数组@players中。 然后,我重新定义了<=>为以下内容:
def <=>(other)
    other.score <=> score
end 

我可以运行这段代码。
@players.sort

我的@players玩家对象数组已经按照从高到低的得分排序。但是我觉得这看起来有点黑盒子。我不太清楚这里正在发生什么。我该如何找出幕后的情况呢?

我知道的是,如果你取两个值并使用spaceship运算符/通用比较运算符:

2 <=> 1   
=> 1

1 <=> 2
=> -1

1 <=> 1
=>0

有时候,似乎 Ruby 在我编程的高层面上有很多低层级的东西我看不到。这似乎是很自然的...但这种情况似乎与 sort 方法的底层操作特别无关。sort 方法如何使用太空船运算符?为什么按我们所做的方式重新定义太空船运算符现在允许我们对对象进行排序?

1
你有阅读文档吗? - mu is too short
3个回答

56
排序对象需要先了解Ruby中的.sort方法。如果要对5张带有数字的牌进行排序,您可以查看所有牌,轻松找到最小的一张,然后将其选为第一张牌(假设您是按从低到高排序的,这也是Ruby做的)。当大脑进行排序时,它可以查看所有内容并从那里排序。
这里有两个主要的疑点很少被提及:
1)Ruby无法像您想象的“排序”方式那样进行排序。Ruby只能“交换”数组元素,并且可以“比较”数组元素。
2)Ruby使用一个比较运算符,称为太空舱,来对数字进行归因以帮助它“排序”。这些数字是-1,0,1。人们错误地认为这3个数字有助于它“排序”(例如,如果有一个包含3个数字10、20、30的数组,则10将是-1,20将是0,30将是1,Ruby只是通过将其简化为-1,0,1来简化排序。这是错误的。Ruby无法“排序”。它只能进行比较。
看一下太空舱运算符。它是三个单独的运算符合并成一个的<,=和>。当Ruby比较两个变量时,会得到其中一个数字。
话虽如此,“结果”是什么意思呢?它并不意味着其中一个变量被赋予了0,1,-1。它只是一种Ruby可以使用两个变量并对它们进行某些操作的方式。现在,如果您只运行:
puts 4 <=> 5
你将得到结果 -1,因为不论比较操作符(太空船运算符)的哪一个部分(例如 <、= 或 >)为真,都会获得分配给它的数字(如上图所示)。然而,当 Ruby 看到这个 <=> 与数组一起使用时,它只会对数组执行两件事:保持数组不变或者交换数组的元素。
如果 Ruby 使用 <=> 并获得1的结果,它将交换数组的2个元素。如果 Ruby 获得-1或0的结果,它将保持数组不变。
举个例子,如果 Ruby 看到数组[2,1]。sort 方法会像 2<=>1 这样拉入这些数据。由于太空船运算符的部分(如果你想这样想)是真的是>(即 2>1 为真),Ruby 的结果是“1”。当 Ruby 从太空船运算符获得 1 的结果时,它会交换数组的 2 个元素。现在数组变成了 [1,2]。
希望此时你能看出来,Ruby 只会使用 <=> 运算符进行比较,然后交换(或保持不变)它比较的数组中的 2 个元素。
要理解.sort方法是迭代方法,这意味着它是一个运行代码块多次的方法。大多数人在接触到.each或.upto等方法之后才被引入到.sort方法(如果你没有听说过它们,你不需要知道它们做什么),但这些方法仅运行一次。.sort方法不同之处在于它会多次运行,直到数组排序完成(通过比较和交换)。
为了确保你理解 Ruby 语法:
foo = [4, 5, 6]
puts foo.sort {|a,b| a <=> b}

使用{}括起来的代码块是Ruby在从最低到最高排序时会执行的。但可以说,.sort方法的第一次迭代将把管道符号之间的变量(a,b)分配给数组的前两个元素。因此,对于第一次迭代,a=4且b=5,由于4<5,结果为-1,表示不交换数组。它在第二次迭代中执行此操作,这意味着a = 5且b = 6,看到5<6,结果为-1,并保持数组不变。由于所有<=>结果都为-1,Ruby停止循环,并认为数组已排序为[4,5,6]。

我们可以通过简单地交换变量的顺序来按高到低的顺序进行排序。

bar = [5, 1, 9]
puts bar.sort {|a,b| b <=> a}   

以下是Ruby所进行的操作:

第一轮迭代:数组[5,1,9]。a=5,b=1。Ruby 看到 b<=>a,就会问 1 < 5吗?是的。结果是-1。维持不变。

第二轮迭代:数组[5,1,9]。a=1,b=9。Ruby 看到 b<=>a,就会问 9 < 1 吗?不是。结果为1。交换2个数组元素。数组现在变成了[5,9,1]。

第三轮迭代:数组 [5,9,1]。重新开始,因为在经历全过程之前,数组中存在一个+1的结果。a=5,b=9。Ruby 看到 b<=>a,就会问9<5吗?不是。结果为1。交换。[9,5,1]

第四轮迭代:数组 [9,5,1]。a=5,b=1。Ruby 看到 b<=>a,就会问1<5吗?是的。结果是-1。因此不进行交换。完成。[9,5,1]。

想象一下一个数组,前999个元素都是50,第1000个元素是1。如果您意识到Ruby必须重复遍历数组数千次,并执行相同的简单比较和交换过程,才能将这个数字 1 移到数组的开头,您就完全了解了排序方法。

现在,我们可以终于探讨一下当涉及对象时的 .sort 方法。

def <=>(other)
    other.score <=> score
end 
这现在应该更容易理解了。当在一个对象上调用 .sort 方法时,就像你运行的那样:
@players.sort

当使用.sort方法时,它会调用参数(例如'other')的"def <=>"方法,并将当前对象从@players (例如'@players'数组的当前实例对象)传递进去。就像在类上运行puts方法时,它自动调用该类中的to_s方法一样。使用.sort方法也是同样的道理,它会自动寻找<=>方法。

查看<=>方法内部的代码,必须存在.score实例变量(具有存取器方法)或者在该类中简单地存在.score方法。而且,.score方法的结果应该(希望如此)是一个字符串或数字——这是Ruby可以“排序”的两个类型。如果是数字,则Ruby使用它的<=> 'sort'操作重新排列所有对象,因为它现在知道了要排序的对象的哪一部分(在这种情况下,是.score方法或实例变量的结果)。

最后一点,Ruby通过将字母转换为数字值来按字母顺序排序。它只是将任何字母视为分配ASCII代码(意味着由于大写字母在ASCII码图表上具有更低的数值,所以默认情况下将首先对大写字母进行排序)。

希望这可以帮助你!


4
超级好的解释,我喜欢它。 - thinkingmonster
你忘了一件事:在某些类中使用<=>方法,你应该在其中包含Comparable模块。 - kaleb4eg

3
在你的例子中。
@players.sort

等同于

@players.sort { |x, y| x <=> y }

元素根据<=>方法的返回值进行排序。如果<=>返回-1,则第一个元素在第二个元素之前排序;如果返回1,则第二个元素在第一个元素之前排序。如果更改返回值(例如交换元素),则顺序将根据返回值而改变。


2
sort”实际上是一个基于<=>实现的可枚举方法。从Ruby文档中可以看到:

如果使用了Enumerable#max、#min或#sort,则集合中的对象必须还要实现有意义的<=>运算符,因为这些方法依赖于集合成员之间的排序。

请自行尝试:
class Player
  attr_accessor :name, :score

  def initialize(name, score=0)
    @name = name
    @score = score  
  end

  def <=> other
    puts caller[0].inspect
    other.score <=> score
  end
end

player1 = Player.new("moe")
player2 = Player.new("larry",60)
player3 = Player.new("curly", 125)
@players = [player1, player2, player3]
puts @players.sort.inspect

#=> "player.rb:19:in `sort'"
#=> "player.rb:19:in `sort'"
#=> [#<Player:0x007fe87184bbb8 @name="curly", @score=125>, #<Player:0x007fe87184bc08 @name="larry", @score=60>, #<Player:0x007fe87184bc58 @name="moe", @score=0>]

你看,当我们在@players数组上使用sort时,会调用Player对象的<=>方法,如果你没有实现它,那么你可能会得到以下错误信息:

player.rb:14:in sort': comparison of Player with Player failed (ArgumentError) from player.rb:14:in'

这是有道理的,因为对象不知道如何处理<=>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接