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

时间:2017-01-14 17:36:00

标签: ruby sorting override mixins

我正在研究如何在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中编写第一个脚本的方法更短吗? (对不起,如果这是一个菜鸟问题,我是初学者。)

3 个答案:

答案 0 :(得分:3)

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

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)。

答案 1 :(得分:1)

写作时

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中,没有自由浮动的方法(即没有附加到类或模块的方法)。当您在任何类或模块之外编写def ...时,该方法将作为实例方法添加到Object严格来说,有unbound methods,但即使这些也需要在被调用之前与对象相关联。

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

所以当你在课堂外写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类会覆盖<=>本身。为了将字符串与另一个对象String进行比较,将优先使用Object中的实现,即使您已覆盖{{1}中的方法}。

但是,如果您的类没有自己的Object(或者在类层次结构中它与<=>之间的重写实现),那么您在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 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实现的最小类时会发生什么。 <=>可以向我们展示直接在类上实现的实例方法。空instance_methods(false)类没有任何:)

Something

所以它将使用继承的[14] pry(main)> Something.instance_methods(false) => [] 方法。

答案 2 :(得分:1)

你的问题是:

y <=> x

只是一种花哨的(人性化的)写作方式:

y.<=>(x)

因此<=>运算符不是函数调用,它是运算符左侧的方法调用。该方法调用不会使用您的def <=>,因为您的比较方法未在您要排序的数组中的对象上定义,您已创建了<=>其他一些课程的方法。

在JavaScript中,你会这样说:

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

或在更现代的时代:

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

所以你将sort函数交给使用比较器,你没有在任何地方定义比较器运算符,只是一个带有两个参数并返回所需值的函数。

在Ruby中,您通常使用块作为&#34;回调&#34;:

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的块应该是

  

返回-1,0或+1,具体取决于ab之间的比较。

并且您的块不处理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(' ') }