Ruby 概念 - 单例类
封面图片来源:Samier Saeed和SitePoint。
你有没有想过“单例类”是什么?你是否曾经在与人交谈或阅读博客文章时,听到有人提到“单例类”或“单例方法”,然后你只是微笑着点头,心里默默地记着以后要查一下?现在是时候了。现在是时候了。我希望用更直观的语言解释这个概念,并向你展示它是多么的实用。
附注:本文的大部分信息来自David A. Black所著的《The Well-Grounded Rubyist》。这本书内容丰富,目前是我最喜欢的 Ruby 书籍之一。
附注#2:至少,滚动到底部可以看到一些基于单例的俗气而健康的积极性。😁
代码
如果你写过很多 Ruby 代码,你肯定在不知不觉中就用过这些“单例类”了!首先,我会给你展示一些你可能已经写过的代码,让你对它有个大概的了解。
class Config
def self.from_file(filename)
Config.new(YAML.load_file(filename))
end
end
dev_config = Config.from_file("config.dev.yaml")
# => Config object with dev settings
你可能还见过这样的情况:
module Geometry
class << self
def rect_area(length, width)
length * width
end
end
end
Geometry.rect_area(4, 5)
# => 20
到目前为止,你可能已经把它们称为“类方法”。你基本上是对的。但它们为什么能起作用呢?这里发生了什么?
个性化
这是 Ruby 如此出色的核心概念。即使是同一个类,各个对象之间也存在差异,它们可以定义不同的方法。我将毫不掩饰地使用我们的宠物来辅助说明这个例子。
class Pet
def smolder
"Generic cute pet smolder"
end
end
succulent = Pet.new
momo = Pet.new
willy = Pet.new
def momo.smolder
"sassy cat smolder"
end
def willy.smolder
"well-meaning dingus smolder"
end
现在,当我们调用时smolder
,succulent
我们没有改变,事情按计划进行。
succulent.smolder
# => Generic cute pet smolder"
但是当我们调用smolder
或willy
时momo
,就会发生一些不同的事情!
momo.smolder
# => "sassy cat smolder"
willy.smolder
# => "well-meaning dingus smolder"
那么,这是怎么回事?我们是不是要为每只宠物重新定义smolder
?帮我看看下面的输出:
succulent.singleton_methods
# => []
momo.singleton_methods
# => [:smolder]
willy.singleton_methods
# => [:smolder]
没错!你正在使用单例方法!现在,我想我们可以开始讨论什么是单例类了。
什么是单例类?
首先,一个更通用、而非 Ruby 特有的问题:什么是单例?虽然针对不同情况有各种更具体的定义,但单例的核心就是只有一个。它是同类中唯一的。
这在 Ruby 的语境下意味着什么?答案是:当你在 Ruby 中实例化一个类的对象时,它知道该类赋予它的方法。它也知道如何查找该类的所有祖先。这就是继承的原理。
“哦,我的类没有这个方法?让我们检查一下它的父类。以及那个类的父类。等等。”
Ruby 的一大亮点在于其祖先链的设计非常清晰。对象搜索其祖先时遵循一套特定的规则,因此不会有任何疑问哪个方法会被调用。
除了了解其所属的类之外,每个对象在创建时都会带有一个它所了解的单例类。单例类实际上是一种“幽灵类”,或者更简单地说,是一个用来存放仅为该特定对象定义的所有方法的包。试试这个:
momo.singleton_class
# => #<Class:#<Pet:0x00007fea40060220>>
在继承层次结构中,它位于对象实际类的前面,不可见。但是,你无法通过查看对象的祖先类来看到它。
momo.class.ancestors
# => [Pet, Object, Kernel, BasicObject]
但是如果我们看一下单例类本身的祖先树:
momo.singleton_class.ancestors
# => [#<Class:#<Pet:0x00007fea40060220>>, Pet, Object, Kernel, BasicObject]
你可以看到它出现在最开始的位置。因此,当momo
它查找smolder
方法时,会首先在它的单例类中查找。由于那里有一个smolder
方法,它会调用那个方法,而不是沿着树往上查找类中定义的方法Pet
。
这与类方法有什么关系?
现在我们开始见识单例类的威力了。别忘了,每个类都只是该类的一个对象Class
。如果这句话让你感到头晕目眩,别担心,我会解释的。
Pet.class
# => Class
它Class
只是一个类,为您创建的每个实例(类)提供一些方法,就像任何其他类一样。
Class.instance_methods(false)
# => [:new, :allocate, :superclass]
因此,实际上,当您定义计划直接在类上调用的“类方法”时,您实际上是在特定的 Class 对象上定义方法 - 在其单例类中!
class Pet
def self.random
%w{cat dog bird fish banana}.sample
end
end
Pet.singleton_methods
# => [:random]
而且……如果单例类存在,它将成为从主类继承的类的 singleton_classes 的父类。下面这个例子应该能帮你理解。
class Pet
def self.random
%w{cat dog bird fish banana}.sample
end
end
class Reptile < Pet
def self.types
%w{lizard snake other}
end
end
Reptile.singleton_methods
# => [:types, :random]
Reptile.singleton_class.ancestors
# => [#<Class:Reptile>, #<Class:Pet>, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
看看Reptile
的单例类是如何从Pet
的单例类继承的,并且可用的“类方法”Pet
也对可用Reptile
?
其他信息
到目前为止,我们基本上已经涵盖了所有重要的部分。不过,还有一些更有趣的细节,我觉得它们很酷,但与上下文有点无关:有点难以理解的class << self
语法、创建类方法的不同方式,以及 的使用extend
。如果你感兴趣,可以继续阅读。
Class << self
实际上,该关键字有两种使用方式class
:直接跟一个常量(例如 la class Gelato
),或者跟一个“铲子操作符”和一个对象(例如 la class << momo
)。你已经了解第一种——这是你通常声明类的方式!让我们集中讨论第二种,即直接打开对象的单例类的语法。你可以把它想象成与我们上面定义的方法本质上相同。
我的意思是:
# This:
def momo.snug
"*snug*"
end
# is the same (pretty much) as this:
class << momo
def snug
"*snug*"
end
end
当您重新打开常规类以添加更多功能时,您会一直这样做。
class Gelato
attr_reader :solidity
def initialize
@solidity = 100
end
def melt
@solidity -= 10
end
end
# And re-open it to add one more method
class Gelato
def refreeze
@solidity = 100
end
end
dessert = Gelato.new
5.times { dessert.melt }
dessert.solidity
# => 50
dessert.refreeze
# => 100
该语法class << object; end
只是重新打开对象单例类的另一种方式。这样做的好处是,你可以一次性定义常量和多个方法,而不是一次定义一个。
# Instead of:
def momo.pounce
"pounce!"
end
def momo.hiss
"HISS"
end
def momo.lives
9
end
# We can do
class << momo
def pounce
"pounce!"
end
def hiss
"HISS"
end
def lives
9
end
end
momo.singleton_methods
# => [:pounce, :hiss, :lives, :smolder]
向一个类添加多个类方法时,常见的模式如下:
class Pet
class << self
def random
%w{cat dog bird fish banana}.sample
end
end
end
# Which, since "self" is inside of the class
# declaration, means that 'self == Pet', so you could
# also do this:
class Pet
class << Pet
def random
# ...
end
end
end
也许你见过这个模式,但不知道它是什么;或者你知道它添加了类方法,但不知道为什么。现在你知道了!这都归功于单例类!
class << self
对比def self.method
对比def Pet.method
声明类方法有几种不同的方法。
# 1. In global scope
def Pet.random
%w{cat dog bird fish banana}.sample
end
# 2. Inside the class definition, using 'self'
class Pet
def self.random
%w{cat dog bird fish banana}.sample
end
end
# 3. Inside the class definition, using the shovel
class Pet
class << self
def random
%w{cat dog bird fish banana}.sample
end
end
end
# 4. Outside the class definition, using the shovel
class << Pet
def random
%w{cat dog bird fish banana}.sample
end
end
那么有什么区别呢?什么时候使用其中一个?
好消息是,它们基本上都是一样的。你可以选择最合适你且符合你代码库风格的那个。唯一的区别在于 #3,以及它如何处理常量和作用域。
MAX_PETS = 3
def Pet.outer_max_pets
MAX_PETS
end
class Pet
MAX_PETS = 1000
class << self
def inner_max_pets
MAX_PETS
end
end
end
Pet.outer_max_pets
# => 3
Pet.inner_max_pets
# => 1000
看到函数inner_max_pets
可以访问Pet
类内部的作用域和常量了吗?这才是唯一的区别。您可以放心地继续使用您喜欢的语法。
使用 Extend 安全地修改内置类
希望你曾经读过博客文章,或者有人警告过你重新打开 Ruby 内置类的危险。进行类似以下操作时务必非常小心。
class String
def verbify
self + "ify"
end
end
"banana".verbify
# => "bananaify"
这些危险包括意外覆盖内置方法、方法与同一项目中的其他库冲突,以及通常导致程序运行不符合预期。这个extend
关键字可以帮助解决所有这些问题!
什么是扩展?
该extend
关键字与 非常相似,include
因为它允许你将其他类/模块的功能加载到你的类/模块中。然而,不同之处在于,它将extend
这些方法放到目标对象的单例类中。
module Wigglable
def wiggle
"*shimmy*"
end
end
willy.extend(Wiggleable)
willy.singleton_methods
# => [:wiggle, :smolder]
extend
因此,如果在类定义中使用而不是include
,则方法将作为类方法添加到类的单例类中,而不是作为实例方法添加到类本身。
module Hissy
def hiss
"HISS"
end
end
class Reptile
extend Hissy
end
snek = Reptile.new
snek.hiss
# => Error! Undefined method hiss for 'snek'
Reptile.hiss
# => "HISS"
这对我们有什么帮助?
那么,假设我们确实需要verbify
在使用的字符串上使用该方法。虽然你可以创建并使用 的子类String
,但另一个选择是扩展单个字符串!
module Verby
def verbify
self + "ify"
end
end
noun = "pup"
noun.extend(Verby)
noun.verbify
# => "pupify"
俗气的总结
所以请记住,单例不仅仅是一个听起来吓人但其实并不复杂的 Ruby 主题。你才是真正的 单例——是的,你是一个人,但没有人能和你一样。你拥有类和你自己的方法来做事,这很宝贵。现在我们又为你添加了一点功能。
class << you
def use_singletons_for_fun_and_profit
# ...
end
end