Ruby"CONSTANTS"似乎是不可改变的?

TJC*_*ers 3 ruby class-constants

据我所知,Ruby中的"常量"按惯例称为常量,但实际上是可变的.然而,我的印象是,当他们"变异"时,有一个警告:

class Z2
 M = [0,1]
end

Z2::M # => [0, 1]
Z2::M = [0,3]
(irb):warning: already initialized constant Z2::M
(irb):warning: previous definition of M was here
Run Code Online (Sandbox Code Playgroud)

但是我发现事实并非如此:

a = Z2::M
a[1] = 2

Z2::M # => [0,2] and no warning
Run Code Online (Sandbox Code Playgroud)

这是"警告"系统中的一个缺口吗?我推断一个常量的赋值会复制它,但我想这不是真的,因为看起来常量和变量指向同一个对象?这是否意味着需要冻结所有所谓的"常量"以防止它们在没有警告的情况下被更改?

Tod*_*obs 5

TL; DR

猴子修补内核#warn(请参阅/sf/answers/46370551/)提出异常,您将无法阻止重新分配常量本身.在惯用的Ruby代码中,这通常不是一个实用的问题,人们期望能够执行重新打开类的操作,即使类名也是常量.

Ruby常量实际上不是不可变的,您不能冻结变量.但是,当某些内容尝试修改常量引用的冻结对象的内容时,可能会引发异常.

用纯Ruby深度冻结对象

冻结阵列很容易:

CONSTANT_ONE = %w[one two three].freeze
Run Code Online (Sandbox Code Playgroud)

但存储在此Array中的字符串实际上是对String对象的引用.因此,虽然您无法修改此数组,但您仍然可以(例如)修改索引0引用的String对象.要解决此问题,您不仅要冻结数组,还要冻结它所拥有的对象.例如:

CONSTANT = %w[one two three].map(&:freeze).freeze

CONSTANT[2] << 'four'
# RuntimeError: can't modify frozen String

CONSTANT << 'five'
# RuntimeError: can't modify frozen Array
Run Code Online (Sandbox Code Playgroud)

用宝石递归冻结物体

由于冻结递归引用可能有点笨拙,因此很高兴知道它有一个宝石.您可以使用ice_nine来深度冻结大多数对象:

require 'ice_nine'
require 'ice_nine/core_ext/object'

OTHER_CONST = %w[a b c]
OTHER_CONST.deep_freeze

OTHER_CONST << 'd'
# RuntimeError: can't modify frozen Array

OTHER_CONST[2] = 'z'
# RuntimeError: can't modify frozen Array
Run Code Online (Sandbox Code Playgroud)

使用Ruby常量的更好方法

另一个需要考虑的选择是在将常量的值赋给另一个变量时调用Object#dup,例如类初始值设定项中的实例变量,以确保不会意外地改变常量的引用.例如:

class Foo
  CONSTANT = 'foo'
  attr_accessor :variable

  def initialize
    @variable = CONSTANT.dup
  end
end

foo = Foo.new
foo.variable << 'bar'
#=> "foobar"

Foo::CONSTANT
#=> "foo"
Run Code Online (Sandbox Code Playgroud)