在 Ruby 中, coerce() 是如何工作的?

Posted

技术标签:

【中文标题】在 Ruby 中, coerce() 是如何工作的?【英文标题】:In Ruby, how does coerce() actually work? 【发布时间】:2011-02-17 11:30:20 【问题描述】:

据说当我们有一个类Point并且知道如何执行point * 3如下:

class Point
  def initialize(x,y)
    @x, @y = x, y
  end

  def *(c)
    Point.new(@x * c, @y * c)
  end
end

point = Point.new(1,2)
p point
p point * 3

输出:

#<Point:0x336094 @x=1, @y=2>
#<Point:0x335fa4 @x=3, @y=6>

但是,

3 * point

不明白:

Point 不能被强制转换成Fixnum (TypeError)

所以我们需要进一步定义一个实例方法coerce

class Point
  def coerce(something)
    [self, something]
  end
end

p 3 * point

输出:

#<Point:0x3c45a88 @x=3, @y=6>

所以说3 * point3.*(point)是一样的。也就是说,实例方法* 接受一个参数point 并在对象3 上调用。

现在,由于*这个方法不知道如何乘以一个点,所以

point.coerce(3)

将被调用,并取回一个数组:

[point, 3]

然后* 再次应用于它,是这样吗?

现在,这已被理解,我们现在有了一个新的Point 对象,由Point 类的实例方法* 执行。

问题是:

    谁调用了point.coerce(3)?是自动使用Ruby,还是通过捕获异常在Fixnum* 方法中的一些代码?还是通过case 声明,当它不知道其中一种已知类型时,调用coerce

    coerce 是否总是需要返回一个包含 2 个元素的数组?可以没有数组吗?还是可以是3个元素的数组?

    那么规则是,原始运算符(或方法)* 将在元素 0 上被调用,并带有元素 1 的参数? (元素 0 和元素 1 是 coerce 返回的该数组中的两个元素。)谁做的?它是由 Ruby 完成的,还是由 Fixnum 中的代码完成的?如果是通过Fixnum中的代码来做的话,那么就是大家在做强制时都遵循的“约定”?

    那么可能是Fixnum* 中的代码做这样的事情:

    class Fixnum
      def *(something)
        if (something.is_a? ...)
        else if ...  # other type / class
        else if ...  # other type / class
        else
        # it is not a type / class I know
          array = something.coerce(self)
          return array[0].*(array[1])   # or just return array[0] * array[1]
        end
      end
    end
    

    那么在Fixnum的实例方法coerce上加点东西真的很难吗?它已经有很多代码,我们不能只添加几行来增强它(但我们会想要吗?)

    Point 类中的coerce 非常通用,它可以与*+ 一起使用,因为它们是可传递的。如果它不具有传递性,比如我们将 Point 减去 Fixnum 定义为:

    point = Point.new(100,100)
    point - 20  #=> (80,80)
    20 - point  #=> (-80,-80)
    

【问题讨论】:

这是一个很好的问题!我很高兴我找到了它,因为这一直困扰着我,直到现在我才认为它是可以解决的! 一个很好的问题。谢谢你把它。我敢肯定,这将节省许多工程师的困惑时间。 【参考方案1】:

我发现自己在处理可交换性时经常按照这种模式编写代码:

class Foo
  def initiate(some_state)
     #...
  end
  def /(n)
   # code that handles Foo/n
  end

  def *(n)
    # code that handles Foo * n 
  end

  def coerce(n)
      [ReverseFoo.new(some_state),n]
  end

end

class ReverseFoo < Foo
  def /(n)
    # code that handles n/Foo
  end
  # * commutes, and can be inherited from Foo
end

【讨论】:

对于任何将来阅读本文的人:如果可以轻松地将数字直接转换为您的格式,我认为更建议使用[Foo.instantiate(n), self]【参考方案2】:

简答:查看how Matrix is doing it。

这个想法是coerce 返回[equivalent_something, equivalent_self],其中equivalent_something 是一个基本上等同于something 的对象,但它知道如何对Point 类进行操作。在Matrix 库中,我们从任何Numeric 对象构造一个Matrix::Scalar,并且该类知道如何对MatrixVector 执行操作。

解决您的问题:

    是的,它直接是 Ruby(检查对 rb_num_coerce_bin in the source 的调用),尽管如果您希望您的代码可以被其他人扩展,您自己的类型也应该这样做。例如,如果您的Point#* 被传递了一个它无法识别的参数,您可以通过调用arg.coerce(self) 来将该参数传递给coerce 本身到Point

    是的,它必须是一个包含 2 个元素的数组,例如 b_equiv, a_equiv = a.coerce(b)

    是的。 Ruby 对内置类型执行此操作,如果您想要可扩展,您也应该使用自己的自定义类型:

    def *(arg)
      if (arg is not recognized)
        self_equiv, arg_equiv = arg.coerce(self)
        self_equiv * arg_equiv
      end
    end
    

    这个想法是你不应该修改Fixnum#*。如果它不知道该怎么做,例如因为参数是 Point,那么它会通过调用 Point#coerce 来询问您。

    传递性(或实际上可交换性)不是必需的,因为始终以正确的顺序调用运算符。只有对coerce 的调用会暂时恢复接收到的参数。没有内置机制可以确保 +== 等运算符的交换性......

如果有人能提出简洁、准确和清晰的描述来改进官方文档,请发表评论!

【讨论】:

嗯,传递性实际上没有影响吗?例如,请参阅***.com/questions/2801241/… 不,传递性没有任何作用,Ruby 不认为a - b-(b - a) 或类似的东西相同,甚至a + b == b + a 也没有。是什么让你相信我错了?你检查过核磁共振的来源吗?为什么不尝试按照我指示的方向? 我认为 OP 的意思是“对称”而不是“传递”。在任何情况下,都想知道您如何编写coerce,这样非对称运算符(如-)只能在一个方向上实现,同时保持对称运算符双向工作。换句话说,a + 3 == 3 + a3 + a - 3 == a3 - a 会引发错误。 @OldPro 通过“对称”我假设您的意思是“交换”。但我同意,OP 可能意味着可交换,而不是传递。传递性与关系(例如相等)有关,而不是加法/减法之类的操作。 没有什么可以保证交换性。即使不涉及强制。参见例如blog.marc-andre.ca/2009/05/02/schizo-ruby-puzzle

以上是关于在 Ruby 中, coerce() 是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

在 Ruby 中,“=>”是啥意思,它是如何工作的? [复制]

Pandas:errors=‘coerce‘ & infer_datetime_format=True

继承在 Ruby 中是如何工作的?

在 Ruby 中定义 [方括号] 方法是如何工作的?

python 内建函数功能函数 abs() coerce() divmod() round() pow()

数据清洗pd.to_numeric() & errors=‘coerce‘