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

时间:2022-06-01 17:05:32

I was researching how to fiddle with special sorting mechanisms in Ruby. I ended up rewriting this neat JavaScript solution in Ruby:

我正在研究如何摆弄Ruby中的特殊排序机制。最后我用Ruby重写了这个简洁的JavaScript解决方案:

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

This has the desired output (numbers descending, then strings ascending):

它有期望的输出(数字降序,然后字符串升序):

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

But the code seemed unnecessarily complicated. (Ruby's supposed to be more concise than JS!) So I asked myself (and now I ask you), why can't I just do this?

但代码似乎不必要地复杂。(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

This outputs incorrectly, based on sort as if I had not redefined <=>:

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

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

The code here is shorter, but doesn't work. It uses the version of <=> built into Ruby's Comparable module, rather than my attempt to override it. Why wasn't I able to override it? Can methods be overridden only inside of classes or modules? Is there a shorter way to write that first script in Ruby? (Sorry if this is a noob question, I'm a beginner.)

这里的代码比较短,但是不能工作。它使用Ruby的可比模块内置的<=>版本,而不是我试图覆盖它。为什么我不能推翻它?方法只能在类或模块内部重写吗?用Ruby编写第一个脚本有更短的方法吗?(对不起,如果这是一个不能回答的问题,我是初学者。)

3 个解决方案

#1


3  

The easiest way would be to split the string in a number and word, and sort by an Array of minus number (to get decreasing numbers) and word :

最简单的方法是将字符串分成一个数字和一个单词,然后按一个负数数组(以得到负数)和单词进行排序:

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

When sorting arrays, the first element (-number) has priority. If both first elements are the same, the sort uses the second element (word).

在排序数组时,第一个元素(-number)具有优先级。如果第一个元素都是相同的,排序将使用第二个元素(word)。

#2


1  

Your problem is that this:

你的问题是:

y <=> x

is just a fancy (and human-friendly) way of writing:

是一种奇特的(对人类友好的)写作方式:

y.<=>(x)

so the <=> operator isn't a function call, it is a method call on the operator's left hand side. That method call won't use your def <=> because your comparator method isn't defined on the objects in the array you're sorting, you've created your <=> method on some other class.

所以<=>操作符不是函数调用,而是操作符左手边的方法调用。该方法调用不会使用def <=>,因为比较器方法没有在要排序的数组中的对象上定义,而是在其他类上创建了<=>方法。

In JavaScript, you say things like this:

在JavaScript中,你会这样说:

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

or in more modern times:

或者在现代社会:

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

so you're handing sort a function to use a comparator, you're not defining a comparator operator anywhere, just a function that takes two arguments and returns the desired value.

你给排序一个函数来使用一个比较器,你不是在任何地方定义一个比较器运算符,只是一个函数,它接受两个参数并返回想要的值。

In Ruby, you generally use blocks as "callbacks":

在Ruby中,通常使用块作为“回调”:

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

Before we continue, you have a problem with your comparator logic because the block for Enumerable#sort is supposed to

在继续之前,您对比较器逻辑有一个问题,因为枚举#sort的块应该是这样的

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

根据a和b之间的比较返回-1、0或+1。

and your block doesn't handle the 0 (equality) case. Also, your _nums are still strings so they won't compare like numbers. The first problem can be solved by using Array#<=> (which compares arrays element by element) and then second can be fixed with a simple to_i call:

你的block不能处理0(等式)的情况。而且,你的_nums仍然是字符串,所以它们不会像数字那样进行比较。第一个问题可以通过使用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

You can go a step further by switching to sort_by!:

通过切换到sort_by,您可以更进一步!

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

If you want to use the block's logic in multiple places, you can get closer to the JavaScript version using a lambda:

如果您想要在多个位置使用block的逻辑,那么您可以使用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)

or a separate method:

或一个单独的方法:

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

If you really mean to compare the self_number and other_number values as strings, then leave out the to_i calls and further simplify the blocks/lambdas:

如果您真的想将self_number和other_number值作为字符串进行比较,那么省略to_i调用,进一步简化块/lambdas:

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

#3


1  

When you write

当你写

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

that is equivalent to

这相当于

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

i.e. it is calling y's version of the <=> (spaceship) operator. Since y is just a String this performs the default comparison for strings.

也就是说,它调用的是y版本的<=>(太空船)操作符。因为y只是一个字符串,所以它执行字符串的默认比较。

To write your code more concisely you can just write the custom comparison logic in the block being passed to sort!:

要更简洁地编写代码,只需在传递给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

or alternatively, write it as a standalone method:

或者也可以写成独立的方法:

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

and call that from sort!:

这可以从sort中调用!

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

A couple of things that might help clarify:

有两件事可能有助于澄清:

In Ruby, there are no free-floating methods (i.e. methods not attached to a class or module). When you write def ... outside of any class or module the method is added to Object as an instance method. Strictly, there are unbound methods but even these need associating with an object before they can be called.

在Ruby中,没有*浮动的方法(即不附加到类或模块的方法)。当你写def的时候……在任何类或模块之外,该方法作为实例方法添加到对象中。严格地说,有一些未绑定的方法,但在调用它们之前,甚至这些方法都需要与对象相关联。

Next thing to keep in mind is where the default implementation of <=> comes from: it's on the Kernel module which is included by class Object.

接下来要记住的是<=>的默认实现来自何处:它位于类对象包含的内核模块上。

So when you write def <=>(other)... outside of a class you are overriding the method for Object:

所以当你写def <=>(其他)…在类之外重写对象的方法:

[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

However, the String class overrides <=> itself. For comparing a string with another object String's implementation will be used in preference to the implementation in Object, even if you've overridden the method in Object.

但是,String类重写了<=>本身。对于将字符串与另一个对象字符串的实现进行比较,将优先使用对象中的实现,即使您重写了对象中的方法。

However, if you have a class that doesn't have its own <=> (or an overriding implementation between it and Object in the class hierarchy) then your overridden method on Object will indeed be used:

但是,如果您的类没有自己的<=>(或在类层次结构中它与对象之间的重写实现),那么您将使用被重写的对象方法:

[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

Explanation of what was being demonstrated in pry

解释在pry中演示的内容

The first snippet is using the method method to grab hold of a method and then using owner to find out where in the class hierarchy that method is defined.

第一个代码片段使用方法方法获取方法,然后使用owner查找方法在类层次结构中定义的位置。

So another example:

另一个例子:

class Animal
  def eat
    puts "Munch!"
  end
end

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

So if we've got a dog:

如果我们有一只狗

buddy = Dog.new

we can find out where its methods come from:

我们可以知道它的方法从何而来:

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

so in the original example we could see that <=> started out referring to the method from the Kernel module, but when we did def <=>... this added a method directly to Object which was now overriding the included method.

在最初的例子中,我们可以看到<=>一开始是指内核模块中的方法,但是当我们定义def <=>…这直接向对象添加了一个方法,对象现在重写了包含的方法。

The second example was showing what happens when there's a minimal class without its own implementation of <=>. instance_methods(false) can show us the instance methods that are directly implemented on a class. The empty Something class doesn't have any :)

第二个例子展示了在没有实现<=>的情况下有一个最小的类时发生了什么。instance_methods(false)可以显示直接在类上实现的实例方法。空的某某类没有:)

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

so it will be using the inherited <=> method.

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

#1


3  

The easiest way would be to split the string in a number and word, and sort by an Array of minus number (to get decreasing numbers) and word :

最简单的方法是将字符串分成一个数字和一个单词,然后按一个负数数组(以得到负数)和单词进行排序:

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

When sorting arrays, the first element (-number) has priority. If both first elements are the same, the sort uses the second element (word).

在排序数组时,第一个元素(-number)具有优先级。如果第一个元素都是相同的,排序将使用第二个元素(word)。

#2


1  

Your problem is that this:

你的问题是:

y <=> x

is just a fancy (and human-friendly) way of writing:

是一种奇特的(对人类友好的)写作方式:

y.<=>(x)

so the <=> operator isn't a function call, it is a method call on the operator's left hand side. That method call won't use your def <=> because your comparator method isn't defined on the objects in the array you're sorting, you've created your <=> method on some other class.

所以<=>操作符不是函数调用,而是操作符左手边的方法调用。该方法调用不会使用def <=>,因为比较器方法没有在要排序的数组中的对象上定义,而是在其他类上创建了<=>方法。

In JavaScript, you say things like this:

在JavaScript中,你会这样说:

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

or in more modern times:

或者在现代社会:

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

so you're handing sort a function to use a comparator, you're not defining a comparator operator anywhere, just a function that takes two arguments and returns the desired value.

你给排序一个函数来使用一个比较器,你不是在任何地方定义一个比较器运算符,只是一个函数,它接受两个参数并返回想要的值。

In Ruby, you generally use blocks as "callbacks":

在Ruby中,通常使用块作为“回调”:

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

Before we continue, you have a problem with your comparator logic because the block for Enumerable#sort is supposed to

在继续之前,您对比较器逻辑有一个问题,因为枚举#sort的块应该是这样的

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

根据a和b之间的比较返回-1、0或+1。

and your block doesn't handle the 0 (equality) case. Also, your _nums are still strings so they won't compare like numbers. The first problem can be solved by using Array#<=> (which compares arrays element by element) and then second can be fixed with a simple to_i call:

你的block不能处理0(等式)的情况。而且,你的_nums仍然是字符串,所以它们不会像数字那样进行比较。第一个问题可以通过使用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

You can go a step further by switching to sort_by!:

通过切换到sort_by,您可以更进一步!

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

If you want to use the block's logic in multiple places, you can get closer to the JavaScript version using a lambda:

如果您想要在多个位置使用block的逻辑,那么您可以使用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)

or a separate method:

或一个单独的方法:

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

If you really mean to compare the self_number and other_number values as strings, then leave out the to_i calls and further simplify the blocks/lambdas:

如果您真的想将self_number和other_number值作为字符串进行比较,那么省略to_i调用,进一步简化块/lambdas:

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

#3


1  

When you write

当你写

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

that is equivalent to

这相当于

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

i.e. it is calling y's version of the <=> (spaceship) operator. Since y is just a String this performs the default comparison for strings.

也就是说,它调用的是y版本的<=>(太空船)操作符。因为y只是一个字符串,所以它执行字符串的默认比较。

To write your code more concisely you can just write the custom comparison logic in the block being passed to sort!:

要更简洁地编写代码,只需在传递给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

or alternatively, write it as a standalone method:

或者也可以写成独立的方法:

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

and call that from sort!:

这可以从sort中调用!

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

A couple of things that might help clarify:

有两件事可能有助于澄清:

In Ruby, there are no free-floating methods (i.e. methods not attached to a class or module). When you write def ... outside of any class or module the method is added to Object as an instance method. Strictly, there are unbound methods but even these need associating with an object before they can be called.

在Ruby中,没有*浮动的方法(即不附加到类或模块的方法)。当你写def的时候……在任何类或模块之外,该方法作为实例方法添加到对象中。严格地说,有一些未绑定的方法,但在调用它们之前,甚至这些方法都需要与对象相关联。

Next thing to keep in mind is where the default implementation of <=> comes from: it's on the Kernel module which is included by class Object.

接下来要记住的是<=>的默认实现来自何处:它位于类对象包含的内核模块上。

So when you write def <=>(other)... outside of a class you are overriding the method for Object:

所以当你写def <=>(其他)…在类之外重写对象的方法:

[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

However, the String class overrides <=> itself. For comparing a string with another object String's implementation will be used in preference to the implementation in Object, even if you've overridden the method in Object.

但是,String类重写了<=>本身。对于将字符串与另一个对象字符串的实现进行比较,将优先使用对象中的实现,即使您重写了对象中的方法。

However, if you have a class that doesn't have its own <=> (or an overriding implementation between it and Object in the class hierarchy) then your overridden method on Object will indeed be used:

但是,如果您的类没有自己的<=>(或在类层次结构中它与对象之间的重写实现),那么您将使用被重写的对象方法:

[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

Explanation of what was being demonstrated in pry

解释在pry中演示的内容

The first snippet is using the method method to grab hold of a method and then using owner to find out where in the class hierarchy that method is defined.

第一个代码片段使用方法方法获取方法,然后使用owner查找方法在类层次结构中定义的位置。

So another example:

另一个例子:

class Animal
  def eat
    puts "Munch!"
  end
end

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

So if we've got a dog:

如果我们有一只狗

buddy = Dog.new

we can find out where its methods come from:

我们可以知道它的方法从何而来:

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

so in the original example we could see that <=> started out referring to the method from the Kernel module, but when we did def <=>... this added a method directly to Object which was now overriding the included method.

在最初的例子中,我们可以看到<=>一开始是指内核模块中的方法,但是当我们定义def <=>…这直接向对象添加了一个方法,对象现在重写了包含的方法。

The second example was showing what happens when there's a minimal class without its own implementation of <=>. instance_methods(false) can show us the instance methods that are directly implemented on a class. The empty Something class doesn't have any :)

第二个例子展示了在没有实现<=>的情况下有一个最小的类时发生了什么。instance_methods(false)可以显示直接在类上实现的实例方法。空的某某类没有:)

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

so it will be using the inherited <=> method.

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