为什么在ruby中使用undef取消定义一个包含类方法后,我无法重新定义它?

atmip9wb  于 12个月前  发布在  Ruby
关注(0)|答案(2)|浏览(95)

我正在升级一个Rails项目,在早期版本中包含了Mocha。它定义了一个any_instance方法。我正在升级的新版本Rspec也在所有类上包含了一个any_instance方法。在升级所有内容的过程中,我需要坚持使用Mocha一段时间,这样我就不必更改一堆测试,所以我想使用Mocha版本的any_instance
要做到这一点,使用Rspec,我 * 首先 * 删除它自己的monkey与disable_monkey_patching!的打包。然后有一个对Rspec的config.mock_with :mocha调用,这将导致mocha在Class上定义any_instance。我知道monkey修补所有类是不好的,但这个问题实际上是一个很好的教训,为什么,但我对我看到的结果很好奇。
以上是我为什么要这么做的一些背景。下面是一个最小的可重复的例子,我不能解释,希望你能有深入的了解。

# Define a class
class A; end

# Define a module whose class methods I'd like to include in every class
module B
  module ClassMethods
    def a; end
  end
end

# Include method "a" in all classes
Class.send :include, B::ClassMethods

# Try it out!
A.a # <- works

字符串
现在我将使用undef来删除它,因为这就是disable_monkey_patching!所做的:

Class.class_exec { undef a }
A.a # <- undefined method `a' for A:Class (NoMethodError) -- that's expected


但是现在我需要为Class定义一个不同的方法“a”,我将在模块C中定义它。

module C
  module ClassMethods
    def a; end
  end
end

Class.send :include, C::ClassMethods


这是让我困惑的部分:

A.a # <- undefined method `a' for A:Class (NoMethodError)


这使得undef看起来会永久地取消对它的定义,但是当任何人试图定义一个最终无法使用的方法时,不会警告他们。
在MRI 3.2.2和2.7.2上尝试

xvw2m8pv

xvw2m8pv1#

调用include不会将方法复制到接收方,它只是将包含的模块添加到方法查找所遍历的模块列表中。
我们可以通过检查A的singleton类的ancestors来看到这个列表:

class A; end

A.singleton_class.ancestors
#=> [#<Class:A>, #<Class:Object>, #<Class:BasicObject>,
#    Class, Module, Object, Kernel, BasicObject]

字符串
当将B::ClassMethods包含到Class中时,此列表会相应更改:

Class.include(B::ClassMethods)

A.singleton_class.ancestors
#=> [#<Class:A>, #<Class:Object>, #<Class:BasicObject>,
#    Class, B::ClassMethods, Module, Object, Kernel, BasicObject]
#           ^^^^^^^^^^^^^^^


请注意,B::ClassMethods是在 * Class之后 * 添加的。
现在,如果您通过undef/undef_method“取消定义”方法a,并将Class作为其接收器,则会导致Class通过引发(人工)NoMethodError来阻止对该方法的调用,这也会结束进一步的方法查找:(我说“人工”是因为该方法仍然存在)

#<Class:A>  →  ...  →  Class  →  B::ClassMethods  →  ...
                         |             |
                       undef a         a
                                (never gets here)


如果在Class中包含另一个模块C::ClassMethods,它将被添加到列表中,在B::ClassMethods之前,但仍然在 * Class之后:

Class.include(C::ClassMethods)

A.singleton_class.ancestors
#=> [#<Class:A>, #<Class:Object>, #<Class:BasicObject>,
#    Class, C::ClassMethods, B::ClassMethods, Module, Object, Kernel, BasicObject]
#           ^^^^^^^^^^^^^^^


由于Class仍然会阻止a被调用,因此也无法访问新的a方法:

#<Class:A>  →  ...  →  Class  →  C::ClassMethods  →  B::ClassMethods  →  ...
                         |             |                   |
                       undef a         a                   a
                                (still not gets here)


对于实际问题(Mocha),您应该首先检查对象的祖先,并确定两个any_instance方法的定义位置。
然后,您可以通过include/prepend将猴子补丁添加到正确的位置。

i2byvkas

i2byvkas2#

undef记录如下:
undef关键字阻止当前类响应对命名方法的调用。

undef my_method

字符串
[...]
您可以在任何作用域中使用undef。参见模块#undef_method
因此,它修改了它被“调用”的模块,不仅删除了它定义的方法,而且标记了它,使它永远不会响应这个方法,即使它可能是在父类或继承链中的任何其他地方定义的。
唯一的方法(我知道)删除这个标记是在模块(或类)中显式定义一个方法,在此之前未定义该方法。这是因为通过在Class上定义方法a,我们恢复了undefModule#undef_method添加的“标记”。
以你的例子为例,这可以通过以下方式实现:

class Class
  def a(...)
    super
  end
end


在这里,我们定义了一个方法a,它接受任何参数,只调用super来将调用沿着祖先链转发,在本例中,要么转发到B::ClassMethods#a,要么转发到C::ClassMethods#a
现在,我们甚至可以将其添加到一个新的级别,并再次删除这个新创建的方法a,但是这次使用Module#remove_method,它被记录为:
从当前类中删除由符号标识的方法。
这将导致一种状态,好像该方法从未存在过(并且从未定义过):

Class.remove_method :a


最后,您可以通过在先前未定义的Module中定义一个新方法(带有任何可接受的参数和任何方法体),然后再次删除它来恢复undefModule#undef_method的效果。

相关问题