Ruby 概念 - 单例类

2025-06-10

Ruby 概念 - 单例类

封面图片来源:Samier SaeedSitePoint

你有没有想过“单例类”是什么?你是否曾经在与人交谈或阅读博客文章时,听到有人提到“单例类”或“单例方法”,然后你只是微笑着点头,心里默默地记着以后要查一下?现在是时候了。现在是时候了。我希望用更直观的语言解释这个概念,并向你展示它是多么的实用。

附注:本文的大部分信息来自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
Enter fullscreen mode Exit fullscreen mode

你可能还见过这样的情况:

module Geometry
  class << self
    def rect_area(length, width)
      length * width
    end
  end
end

Geometry.rect_area(4, 5)
# => 20
Enter fullscreen mode Exit fullscreen mode

到目前为止,你可能已经把它们称为“类方法”。你基本上是对的。但它们为什么能起作用呢?这里发生了什么?

个性化

这是 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
Enter fullscreen mode Exit fullscreen mode

现在,当我们调用时smoldersucculent我们没有改变,事情按计划进行。

succulent.smolder
# => Generic cute pet smolder"
Enter fullscreen mode Exit fullscreen mode

我们的多肉植物

但是当我们调用smolderwillymomo,就会发生一些不同的事情!

momo.smolder
# => "sassy cat smolder"
Enter fullscreen mode Exit fullscreen mode

Momo 是我们的猫

willy.smolder
# => "well-meaning dingus smolder"
Enter fullscreen mode Exit fullscreen mode

威利是我们的狗

那么,这是怎么回事?我们是不是要为每只宠物重新定义smolder?帮我看看下面的输出:

succulent.singleton_methods
# => []
momo.singleton_methods
# => [:smolder]
willy.singleton_methods
# => [:smolder]
Enter fullscreen mode Exit fullscreen mode

没错!你正在使用单例方法!现在,我想我们可以开始讨论什么是单例类了。

什么是单例类?

首先,一个更通用、而非 Ruby 特有的问题:什么是单例?虽然针对不同情况有各种更具体的定义,但单例的核心就是只有一个。它是同类中唯一的。

这在 Ruby 的语境下意味着什么?答案是:当你在 Ruby 中实例化一个类的对象时,它知道该类赋予它的方法。它也知道如何查找该类的所有祖先。这就是继承的原理。

“哦,我的类没有这个方法?让我们检查一下它的父类。以及那个类的父类。等等。”

Ruby 的一大亮点在于其祖先链的设计非常清晰。对象搜索其祖先时遵循一套特定的规则,因此不会有任何疑问哪个方法会被调用。

除了了解其所属的类之外,每个对象在创建时都会带有一个它所了解的单例类。单例类实际上是一种“幽灵类”,或者更简单地说,是一个用来存放仅为该特定对象定义的所有方法的包。试试这个:

momo.singleton_class
# => #<Class:#<Pet:0x00007fea40060220>>
Enter fullscreen mode Exit fullscreen mode

在继承层次结构中,它位于对象实际类的前面,不可见。但是,你无法通过查看对象的祖先类来看到它。

momo.class.ancestors
# => [Pet, Object, Kernel, BasicObject]
Enter fullscreen mode Exit fullscreen mode

但是如果我们看一下单例类本身的祖先树

momo.singleton_class.ancestors
# => [#<Class:#<Pet:0x00007fea40060220>>, Pet, Object, Kernel, BasicObject]
Enter fullscreen mode Exit fullscreen mode

你可以看到它出现在最开始的位置。因此,当momo它查找smolder方法时,会首先在它的单例类中查找。由于那里有一个smolder方法,它会调用那个方法,而不是沿着树往上查找类中定义的方法Pet

这与类方法有什么关系?

现在我们开始见识单例类的威力了。别忘了,每个类都只是该类的一个对象Class。如果这句话让你感到头晕目眩,别担心,我会解释的。

Pet.class
# => Class
Enter fullscreen mode Exit fullscreen mode

Class只是一个类,为您创建的每个实例(类)提供一些方法,就像任何其他类一样。

Class.instance_methods(false)
# => [:new, :allocate, :superclass]
Enter fullscreen mode Exit fullscreen mode

因此,实际上,当您定义计划直接在类上调用的“类方法”时,您实际上是在特定的 Class 对象上定义方法 - 在其单例类中!

class Pet
  def self.random
    %w{cat dog bird fish banana}.sample
  end
end

Pet.singleton_methods
# => [:random]
Enter fullscreen mode Exit fullscreen mode

而且……如果单例类存在,它将成为从主类继承的类的 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]
Enter fullscreen mode Exit fullscreen mode

看看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
Enter fullscreen mode Exit fullscreen mode

当您重新打开常规类以添加更多功能时,您会一直这样做。

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
Enter fullscreen mode Exit fullscreen mode

该语法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]
Enter fullscreen mode Exit fullscreen mode

向一个类添加多个类方法时,常见的模式如下:

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
Enter fullscreen mode Exit fullscreen mode

也许你见过这个模式,但不知道它是什么;或者你知道它添加了类方法,但不知道为什么。现在你知道了!这都归功于单例类!

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
Enter fullscreen mode Exit fullscreen mode

那么有什么区别呢?什么时候使用其中一个?

好消息是,它们基本上都是一样的。你可以选择最合适你且符合你代码库风格的那个。唯一的区别在于 #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
Enter fullscreen mode Exit fullscreen mode

看到函数inner_max_pets可以访问Pet类内部的作用域和常量了吗?这才是唯一的区别。您可以放心地继续使用您喜欢的语法。

使用 Extend 安全地修改内置类

希望你曾经读过博客文章,或者有人警告过你重新打开 Ruby 内置类的危险。进行类似以下操作时务必非常小心。

class String
  def verbify
    self + "ify"
  end
end

"banana".verbify
# => "bananaify"
Enter fullscreen mode Exit fullscreen mode

这些危险包括意外覆盖内置方法、方法与同一项目中的其他库冲突,以及通常导致程序运行不符合预期。这个extend关键字可以帮助解决所有这些问题!

什么是扩展?

extend关键字与 非常相似,include因为它允许你将其他类/模块的功能加载到你的类/模块中。然而,不同之处在于,它将extend这些方法放到目标对象的单例类中。

module Wigglable
  def wiggle
    "*shimmy*"
  end
end

willy.extend(Wiggleable)
willy.singleton_methods
# => [:wiggle, :smolder]
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

这对我们有什么帮助?

那么,假设我们确实需要verbify在使用的字符串上使用该方法。虽然你可以创建并使用 的子类String,但另一个选择是扩展单个字符串!

module Verby
  def verbify
    self + "ify"
  end
end

noun = "pup"
noun.extend(Verby)
noun.verbify
# => "pupify"
Enter fullscreen mode Exit fullscreen mode

俗气的总结

所以请记住,单例不仅仅是一个听起来吓人但其实并不复杂的 Ruby 主题。才是真正的 单例——是的,你是一个人,但没有人能和你一样。你拥有和你自己的方法来做事,这很宝贵。现在我们又为你添加了一点功能。

class << you
  def use_singletons_for_fun_and_profit
    # ...
  end
end
Enter fullscreen mode Exit fullscreen mode
鏂囩珷鏉ユ簮锛�https://dev.to/rpalo/ruby-concepts---singleton-classes-oeb
PREV
使用 `grep` 查看上下文
NEXT
Python 有一个启动文件!