Ruby“CONSTANTS”似乎是不可改变的?

时间:2014-10-23 21:12:20

标签: ruby class-constants

我理解"常数"在Ruby中按惯例称为常量,但实际上是可变的。然而,我的印象是当他们发生变异时#34;有一个警告:

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

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

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

Z2::M # => [0,2] and no warning

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

3 个答案:

答案 0 :(得分:5)

TL; DR

缺少猴子修补内核#警告(请参阅https://stackoverflow.com/a/662436/1301972)以引发异常,您将无法阻止重新分配常量本身。在惯用的Ruby代码中,这通常不是一个实用的问题,其中一个期望能够执行重新打开类的操作,即使类名也是常量。

Ruby常量实际上不是不可变的,你不能freeze变量。但是,当某些内容尝试修改常量引用的冻结对象的内容时,可以获取异常。

使用纯Ruby深度冻结对象

冻结阵列非常简单:

CONSTANT_ONE = %w[one two three].freeze

但是存储在此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

使用Gem递归冻结对象

由于冻结递归引用可能有点笨拙,所以最好知道there's a gem for that。您可以使用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

使用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"

答案 1 :(得分:1)

没有差距,因为你没有改变常数。事实上,Ruby常量只是带有额外警告的变量。

常量,就像每个变量一样,只是指向内存中对象的指针。执行M = [0,3]时,您正在创建一个新数组,并将常量重新指向此新对象,从而触发警告。

但是,当您运行M[0] = 1时,您只是修改引用的对象,但是您不会更改常量,因为它仍然指向同一个对象。

要实现的重要一点是Ruby中的所有类都只是内存中的对象,用常量引用,所以当你这样做时:

class Z2
end

它相当于(如果Z2未定义或未指向类对象):

Z2 = Class.new

自然类是一个非常动态的对象,因为我们不断添加方法等等 - 我们绝对不希望这会触发任何警告。

答案 2 :(得分:0)

如果你Z2::M[1] = 2,你也不会收到消息。我认为缺少警告因为您要更改Array本身而不是参考Z2::M。 如果M是一个整数,例如:

class Z2
  M = 1
end

a = Z2::M
a = 2
a # => 2
Z2::M # => 1

要从常量修改Array而不修改您可以执行的原始操作:

class Z2
  M = [0,1]
end

a = Z2::M.dup
a[0] = 1
a # => [1,1]
Z2::M # => [0,1]