我们最近发布了一个记忆化的精品--MemoWise!我们已经写了关于它的起源故事和性能。在这篇文章中,我们将讨论我们在编写MemoWise时遇到的Ruby的一些深奥的角落。
用prepend对冻结对象进行记忆
在创建这个 gem 时,我们需要支持的功能之一是对冻结或不可变的对象进行备忘。具体来说,我们使用Values gem来创建不可变的实例。一旦一个对象被冻结,我们就不能分配它的任何实例变量:
class Example
attr_writer :value
end
Example.new.freeze.value = true # FrozenError (can't modify frozen Example)
这与记忆化有什么关系?大多数记忆化工具的工作原理是创建一个哈希值来存储记忆化的值。这个哈希值通常是对象本身的一个实例变量。(我们称我们的 @_memo_wise.)因此,如果对象是不可变的,这个实例变量在对象被冻结后不能被分配。(然而,它可以被变异)。
这就是为什么我们prepend MemoWise模块来实现记忆化。 prepend的知名度低于 include或 extend的知名度较低,其工作方式也与这两者略有不同。
每个Ruby类都有一个祖先的列表。这个列表包含了所有包含或预设的模块,按继承顺序排列。我们可以看一下Array ,作为一个例子:
Array.ancestors
=> [Array, Enumerable, Object, Kernel, BasicObject]
当一个方法在一个对象上被调用时,Ruby会依次查看每个祖先,看这个方法是否在这个对象的任何一个祖先上被定义。当我们include 一个模块时,该模块被插入到类的后面:
module Example; end
class Array
include Example
end
Array.ancestors
=> [Array, Example, Enumerable, Object, Kernel, BasicObject]
当我们extend 一个模块时,该模块的方法被导入为类上的方法,并且该模块不会被插入到祖先链中:
class Array
extend Example
end
Array.ancestors
=> [Array, Enumerable, Object, Kernel, BasicObject]
当我们prepend 一个模块时,该模块被插入到类之前 prepending 它的祖先链中。模块的方法将优先于类的方法:
class Array
prepend Example
end
Array.ancestors
=> [Example, Array, Enumerable, Object, Kernel, BasicObject]
这反过来允许我们覆盖initialize 方法,并在对象被冻结之前创建记忆化哈希。这就是MemoWise如何允许冻结对象的记忆化。
确定方法的可见性
MemoWise的另一个重要功能是保留方法的可见性。如果有人使用我们的gem将一个私有方法备忘化,我们要保证备忘化的方法仍然是私有的。
实际上,并没有一个内置的Ruby方法来获取一个方法的可见性。然而,我们可以通过结合各种内置方法来确定可见性:
if private_method_defined?(method_name)
:private
elsif protected_method_defined?(method_name)
:protected
elsif public_method_defined?(method_name)
:public
else
raise NoMethodError
end
使用这个:private,:protected 或:public 符号,我们就可以动态地设置我们的新方法的可见性,以匹配原来的方法。
支持用allocate以及new创建的对象
在测试这个gem的早期版本时,我们遇到了对ActiveRecord 类的记忆错误。这些错误表明我们的@_memo_wise 实例变量没有被设置,这让我们很惊讶,因为正如前面提到的,我们在initialize 中设置了它。
经过大量的调试,我们了解到在Ruby中有一个鲜为人知的方法,可以在不执行initialize 方法的情况下初始化一个对象。它被称为allocate ,它被Rails的ActiveRecord 使用。
当我们在一个类上调用new ,在引擎盖下发生的事情看起来是这样的:
class Example
def self.new(...)
allocate.tap { |instance| instance.initialize(...) }
end
end
通过使用allocate 而不是new ,Rails绕过了我们对@_memo_wise 的初始化。为了解决这个问题,我们必须重写allocate 来执行这个初始化。
展望未来
这些是我们在开发MemoWise时学到的关于Ruby的一些有趣的东西,我们计划在将来再写一些其他的东西同时,请尝试一下或在GitHub上阅读代码,我们很乐意接受贡献。