是否可以覆盖 class 或模块之外的内置 Ruby 方法?

Is it possible to override a built-in Ruby method outside of a class or module?

我正在研究如何 fiddle 在 Ruby 中使用特殊的排序机制。我最终在 Ruby:

中重写了 this neat JavaScript solution
class SpecialStr
  include Comparable
  attr_accessor :str
  def initialize (str)
    @str = str
  end

  def <=> (other)
    self_num, self_string = @str.split(' ')
    other_num, other_string = other.str.split(' ')
    self_num > other_num ? 1 : other_num > self_num ? -1 :
      self_string > other_string ? -1 : 1
  end
end

arr = ['2 xxx', '20 axxx', '2 m', '38 xxxx', '20 bx', '8540 xxxxxx', '2 z']
arr_object = []
arr.each { |str| arr_object << SpecialStr.new(str) }
arr_object.sort! { |x, y| y <=> x }
output_arr = []
arr_object.each { |obj| output_arr << obj.str}
puts output_arr

这具有所需的输出(数字降序,然后字符串升序):

8540 xxxxxx
38 xxxx
20 axxx
20 bx
2 m
2 xxx
2 z

但代码似乎不必要地复杂。 (Ruby 应该比 JS 更简洁!)所以我问自己(现在我问你),为什么我不能这样做?

def <=> (other)
  self_num, self_string = self.split(' ')
  other_num, other_string = other.split(' ')
  self_num > other_num ? 1 : other_num > self_num ? -1 :
    self_string > other_string ? -1 : 1
end
arr = ['2 xxx', '20 axxx', '2 m', '38 xxxx', '20 bx', '8540 xxxxxx', '2 z']
arr.sort! { |x, y| y <=> x }
puts arr

这输出不正确,基于 sort 好像我没有重新定义 <=>:

8540 xxxxxx
38 xxxx
20 bx
20 axxx
2 z
2 xxx
2 m

此处的代码较短,但不起作用。它使用内置于 Ruby 的 Comparable 模块中的 <=> 版本,而不是我尝试覆盖它。为什么我不能覆盖它?只能在 类 或模块内部重写方法吗? 是否有更短的方法来编写 Ruby 中的第一个脚本? (对不起,如果这是一个菜鸟问题,我是初学者。)

写的时候

arr.sort! { |x, y| y <=> x }

相当于

arr.sort! { |x, y| y.<=>(x) }

即它正在调用 y 版本的 <=>(宇宙飞船)运算符。由于 y 只是一个 String,因此它执行字符串的默认比较。

为了更简洁地编写代码,您可以在传递给 sort!:

的块中编写自定义比较逻辑
arr.sort! do |x, y|
  x_num, x_string = x.split(' ')
  y_num, y_string = y.split(' ')
  y_num > x_num ? 1 : x_num > y_num ? -1 :
    y_string > x_string ? -1 : 1
end

或者,将其编写为独立方法:

def my_compare(x, y)
  x_num, x_string = x.split(' ')
  y_num, y_string = y.split(' ')
  y_num > x_num ? 1 : x_num > y_num ? -1 :
    y_string > x_string ? -1 : 1
end

并从 sort! 调用它:

arr.sort! { |x, y| my_compare(x, y) }

一些可能有助于澄清的事情:

在 Ruby 中,没有 free-floating 方法(即未附加到 class 或模块的方法)。当您在任何 class 或模块之外编写 def ... 时,该方法将作为实例方法添加到 Object严格来说,有unbound methods,但即使是这些也需要与对象关联才能被调用。

接下来要记住的是 <=> 的默认实现来自哪里:它在 Kernel 模块中,它包含在 class Object 中。

因此,当您在 class 之外编写 def <=>(other)... 时,您将覆盖 Object:

的方法
[1] pry(main)> method(:<=>).owner
=> Kernel
[2] pry(main)> def <=>(other)
[2] pry(main)*   puts "overridden!"
[2] pry(main)* end
=> :<=>
[3] pry(main)> method(:<=>).owner
=> Object

但是,String class 会覆盖 <=> 本身。为了将字符串与另一个对象进行比较,String 的实现将优先于 Object 中的实现使用,即使 您已经覆盖了 Object.

但是,如果您的 class 没有自己的 <=>(或者它与 class 层次结构中的 Object 之间的重写实现)那么你在 Object 上重写的方法确实会被使用:

[6] pry(main)> class Something; end
=> nil
[7] pry(main)> s1 = Something.new
=> #<Something:0x007fddb4431ba8>
[8] pry(main)> s2 = Something.new
=> #<Something:0x007fddb4469760>
[9] pry(main)> s1 <=> s2
overridden!
=> nil

pry

中演示内容的解释

第一个片段是使用 method 方法获取一个方法,然后使用 owner 找出在 class 层次结构中定义该方法的位置。

再举个例子:

class Animal
  def eat
    puts "Munch!"
  end
end

class Dog < Animal
  def bark
    puts "yap!"
  end
end

所以如果我们有一只狗:

buddy = Dog.new

我们可以找出它的方法来自哪里:

[10] pry(main)> buddy.method(:eat).owner
=> Animal
[11] pry(main)> buddy.method(:bark).owner
=> Dog

所以在原始示例中我们可以看到 <=> 开始引用 Kernel 模块中的方法,但是当我们这样做时 def <=>... 这添加了一个方法直接到 Object 现在覆盖了包含的方法。

第二个示例展示了当最小 class 没有自己实现 <=> 时会发生什么。 instance_methods(false) 可以向我们展示直接在 class 上实现的实例方法。空的 Something class 没有任何 :)

[14] pry(main)> Something.instance_methods(false)
=> []

因此它将使用继承的 <=> 方法。

你的问题是:

y <=> x

只是一种花哨的(和human-friendly)的写法:

y.<=>(x)

所以 <=> 运算符不是函数调用,它是运算符左侧的 方法 调用。该方法调用不会使用您的 def <=> 因为您的比较器方法未在您正在排序的数组中的对象上定义,您已经在其他一些 [=69= 上创建了 <=> 方法].

在JavaScript中,你是这样说的:

a.sort(function(a, b) { ... })

或更近代:

a.sort((a, b) => ...)

所以你要交给 sort 一个使用比较器的函数,你没有在任何地方定义比较器运算符,只是一个接受两个参数和 returns 所需值的函数。

在Ruby中,您通常使用块作为"callbacks":

arr.sort! do |a, b|
  a_num, a_string = a.split(' ')
  b_num, b_string = b.split(' ')
  a_num > b_num ? 1 : b_num > a_num ? -1 : a_string > b_string ? -1 : 1
end

在我们继续之前,您的比较器逻辑有问题,因为 Enumerable#sort 的块应该

return -1, 0, or +1 depending on the comparison between a and b.

并且您的块不处理 0(相等)情况。此外,您的 _num 仍然是字符串,因此它们不会像数字一样进行比较。第一个问题可以通过使用 Array#<=>(逐个元素比较数组)来解决,然后第二个问题可以通过简单的 to_i 调用来解决:

arr.sort! do |a, b|
  a_num, a_string = a.split(' ')
  b_num, b_string = b.split(' ')
  [a_num.to_i, a_string] <=> [b_num.to_i, b_string]
end

您可以更进一步切换到 sort_by!:

arr.sort_by! do |e|
  i, s = e.split(' ')
  [i.to_i, s]
end

如果你想在多个地方使用块的逻辑,你可以使用 lambda 更接近 JavaScript 版本:

cmp = ->(a, b) do
  a_num, a_string = a.split(' ')
  b_num, b_string = b.split(' ')
  [a_num.to_i, a_string] <=> [b_num.to_i, b_string]
end
arr1.sort!(&cmp)
arr2.sort!(&cmp)

natural = ->(e) do
  i, s = e.split(' ')
  [i.to_i, s]
end
arr1.sort_by!(&natural)
arr2.sort_by!(&natural)

或单独的方法:

def cmp(a, b)
  a_num, a_string = a.split(' ')
  b_num, b_string = b.split(' ')
  [a_num.to_i, a_string] <=> [b_num.to_i, b_string]
end

def some_other_method
  arr1.sort!(&method(:cmp))
  arr2.sort!(&method(:cmp))
end

def natural(e)
  i, s = e.split(' ')
  [i.to_i, s]
end

def some_other_other_method
  arr1.sort_by!(&method(:natural))
  arr2.sort_by!(&method(:natural))
end

如果您真的想将 self_numberother_number 值作为字符串进行比较,则省去 to_i 调用并进一步简化 blocks/lambdas:

arr.sort! { |a, b| a.split(' ') <=> b.split(' ') }
arr.sort_by! { |e| e.split(' ') }

最简单的方法是将字符串拆分为数字和单词,然后按负数数组(以获得递减的数字)和单词排序:

arr = ['2 xxx', '20 axxx', '2 m', '38 xxxx', '20 bx', '8540 xxxxxx', '2 z']

arr.sort_by! do |number_word|
  number, word = number_word.split
  [ -number.to_i, word ]
end

puts arr
# =>
# 8540 xxxxxx
# 38 xxxx
# 20 axxx
# 20 bx
# 2 m
# 2 xxx
# 2 z

数组排序时,第一个元素(-number)优先。如果两个第一个元素相同,则排序使用第二个元素 (word).